use super::filetree_selection::{FileTreeSelectionReducerContract, FileTreeSelectionState}; use super::focus_state::PageFocusKeyboardReducerContract; use super::picker_state::PickerStateReducerContract; use serde::Serialize; use std::collections::BTreeSet; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub enum TreeShellRendererMode { Page, FileTree, Picker, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TreeShellRendererInput { pub mode: TreeShellRendererMode, pub projection_item_ids: Vec, pub expanded_ids: BTreeSet, pub focused_id: Option, pub page_focus_keyboard_reducer: Option, pub filetree_selection: FileTreeSelectionState, pub filetree_selection_reducer: Option, pub active_picker_item: Option, pub excluded_picker_ids: BTreeSet, pub picker_state_reducer: Option, pub command_dispatcher: TreeShellCommandDispatcher, pub runtime_artifact: TreeShellRuntimeArtifactBoundary, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TreeShellCommandDispatcher { pub channel: String, pub command_names: BTreeSet, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TreeShellRuntimeArtifactBoundary { pub contract_name: &'static str, pub family: &'static str, pub version: u8, pub execution_strategy: &'static str, pub browser_bridge: &'static str, pub wasm_module_url: Option<&'static str>, pub js_glue_url: Option<&'static str>, pub input_fields: BTreeSet<&'static str>, pub output_channels: BTreeSet<&'static str>, pub event_kinds: BTreeSet<&'static str>, pub runtime_api: TreeShellRuntimeApiBoundary, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TreeShellRuntimeApiBoundary { pub request_contract: &'static str, pub result_contract: &'static str, pub reduce_endpoint: &'static str, pub state_snapshots: BTreeSet<&'static str>, pub dom_patch_kinds: BTreeSet<&'static str>, pub host_event_kinds: BTreeSet<&'static str>, pub command_event_kinds: BTreeSet<&'static str>, } impl Default for TreeShellRuntimeArtifactBoundary { fn default() -> Self { Self { contract_name: "rust_tree_shell_runtime_artifact_v1", family: "rust_family", version: 1, execution_strategy: "browser_bridge", browser_bridge: "iframe_srcdoc", wasm_module_url: Some("/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm"), js_glue_url: Some("/api/tree-shell-runtime/mnote-tree-shell-runtime.js"), input_fields: BTreeSet::from([ "rendererInput", "projectionItems", "expandedIds", "selectedRowIds", "activePickerItem", "focusedId", ]), output_channels: BTreeSet::from(["domPatch", "intentEvent", "commandDispatchEvent"]), event_kinds: BTreeSet::from([ "focus", "keyboard", "expandCollapse", "selection", "contextMenu", "dragDrop", "pick", ]), runtime_api: TreeShellRuntimeApiBoundary { request_contract: "TreeShellRuntimeRequest", result_contract: "TreeShellRuntimeResult", reduce_endpoint: "/api/tree/runtime/reduce", state_snapshots: BTreeSet::from(["page", "fileTree", "picker"]), dom_patch_kinds: BTreeSet::from(["pageState", "fileTreeState", "pickerState"]), host_event_kinds: BTreeSet::from([ "pageOpen", "pageContextMenu", "fileTreeOpen", "fileTreeContextMenu", "fileTreeInternalDrop", "fileTreeExternalDrop", "fileTreeKeyboardCommand", "pickerPickRoot", "pickerPickDocument", ]), command_event_kinds: BTreeSet::from([ "createNode", "renameNode", "moveSubtree", "copyResource", "moveResource", "uploadResource", ]), }, } } } impl TreeShellRendererInput { pub fn page(input: PageTreeRendererInput) -> Self { Self { mode: TreeShellRendererMode::Page, projection_item_ids: input.projection_item_ids, expanded_ids: input.expanded_ids, focused_id: input.focused_id, page_focus_keyboard_reducer: Some(PageFocusKeyboardReducerContract::default()), filetree_selection: FileTreeSelectionState::default(), filetree_selection_reducer: None, active_picker_item: None, excluded_picker_ids: BTreeSet::new(), picker_state_reducer: None, command_dispatcher: input.command_dispatcher, runtime_artifact: TreeShellRuntimeArtifactBoundary::default(), } } pub fn filetree(input: FileTreeRendererInput) -> Self { Self { mode: TreeShellRendererMode::FileTree, projection_item_ids: input.projection_item_ids, expanded_ids: input.expanded_ids, focused_id: input.filetree_selection.focused_row_id.clone(), page_focus_keyboard_reducer: None, filetree_selection: input.filetree_selection, filetree_selection_reducer: Some(FileTreeSelectionReducerContract::default()), active_picker_item: None, excluded_picker_ids: BTreeSet::new(), picker_state_reducer: None, command_dispatcher: input.command_dispatcher, runtime_artifact: TreeShellRuntimeArtifactBoundary::default(), } } pub fn picker(input: PickerRendererInput) -> Self { Self { mode: TreeShellRendererMode::Picker, projection_item_ids: input.projection_item_ids, expanded_ids: input.expanded_ids, focused_id: input.active_picker_item.clone(), page_focus_keyboard_reducer: None, filetree_selection: FileTreeSelectionState::default(), filetree_selection_reducer: None, active_picker_item: input.active_picker_item, excluded_picker_ids: input.excluded_picker_ids, picker_state_reducer: Some(PickerStateReducerContract::default()), command_dispatcher: input.command_dispatcher, runtime_artifact: TreeShellRuntimeArtifactBoundary::default(), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PageTreeRendererInput { pub projection_item_ids: Vec, pub expanded_ids: BTreeSet, pub focused_id: Option, pub command_dispatcher: TreeShellCommandDispatcher, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct FileTreeRendererInput { pub projection_item_ids: Vec, pub expanded_ids: BTreeSet, pub filetree_selection: FileTreeSelectionState, pub command_dispatcher: TreeShellCommandDispatcher, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PickerRendererInput { pub projection_item_ids: Vec, pub expanded_ids: BTreeSet, pub active_picker_item: Option, pub excluded_picker_ids: BTreeSet, pub command_dispatcher: TreeShellCommandDispatcher, } #[cfg(test)] mod tests { use super::{ FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput, TreeShellCommandDispatcher, TreeShellRendererInput, TreeShellRendererMode, }; use crate::tree_shell::filetree_selection::FileTreeSelectionState; use std::collections::BTreeSet; fn set(values: &[&str]) -> BTreeSet { values.iter().map(|value| (*value).to_string()).collect() } fn dispatcher() -> TreeShellCommandDispatcher { TreeShellCommandDispatcher { channel: "mnote.tree.shell".into(), command_names: set(&["tree.node.create", "tree.resource.move"]), } } #[test] fn tree_shell_renderer_input_contract_covers_page_filetree_and_picker_state() { let page = TreeShellRendererInput::page(PageTreeRendererInput { projection_item_ids: vec!["doc:root".into()], expanded_ids: set(&["doc:root"]), focused_id: Some("doc:root".into()), command_dispatcher: dispatcher(), }); assert_eq!(page.mode, TreeShellRendererMode::Page); assert_eq!(page.focused_id.as_deref(), Some("doc:root")); assert_eq!(page.command_dispatcher.channel, "mnote.tree.shell"); assert_eq!( page.page_focus_keyboard_reducer .as_ref() .map(|contract| contract.contract_name), Some("rust_page_focus_keyboard_reducer_v1") ); let mut selection = FileTreeSelectionState::from_selected(&["asset:a".into()]); selection.focused_row_id = Some("asset:a".into()); let filetree = TreeShellRendererInput::filetree(FileTreeRendererInput { projection_item_ids: vec!["doc:root".into(), "asset:a".into()], expanded_ids: set(&["doc:root"]), filetree_selection: selection, command_dispatcher: dispatcher(), }); assert_eq!(filetree.mode, TreeShellRendererMode::FileTree); assert_eq!(filetree.focused_id.as_deref(), Some("asset:a")); assert!(filetree .filetree_selection .selected_row_ids .contains("asset:a")); assert!(filetree.page_focus_keyboard_reducer.is_none()); assert_eq!( filetree .filetree_selection_reducer .as_ref() .map(|contract| contract.contract_name), Some("rust_filetree_selection_reducer_v1") ); let picker = TreeShellRendererInput::picker(PickerRendererInput { projection_item_ids: vec!["doc:root".into(), "doc:child".into()], expanded_ids: set(&["doc:root"]), active_picker_item: Some("doc:child".into()), excluded_picker_ids: set(&["doc:archived"]), command_dispatcher: dispatcher(), }); assert_eq!(picker.mode, TreeShellRendererMode::Picker); assert_eq!(picker.focused_id.as_deref(), Some("doc:child")); assert!(picker.excluded_picker_ids.contains("doc:archived")); assert!(picker.page_focus_keyboard_reducer.is_none()); assert!(picker.filetree_selection_reducer.is_none()); assert_eq!( picker .picker_state_reducer .as_ref() .map(|contract| contract.contract_name), Some("rust_picker_state_reducer_v1") ); } #[test] fn tree_shell_renderer_input_exposes_runtime_artifact_boundary() { let page = TreeShellRendererInput::page(PageTreeRendererInput { projection_item_ids: vec!["doc:root".into()], expanded_ids: set(&["doc:root"]), focused_id: Some("doc:root".into()), command_dispatcher: dispatcher(), }); let artifact = page.runtime_artifact; assert_eq!( artifact.contract_name, "rust_tree_shell_runtime_artifact_v1" ); assert_eq!(artifact.family, "rust_family"); assert_eq!(artifact.version, 1); assert_eq!(artifact.execution_strategy, "browser_bridge"); assert_eq!(artifact.browser_bridge, "iframe_srcdoc"); assert_eq!( artifact.wasm_module_url, Some("/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm") ); assert_eq!( artifact.js_glue_url, Some("/api/tree-shell-runtime/mnote-tree-shell-runtime.js") ); assert!(artifact.input_fields.contains("rendererInput")); assert!(artifact.input_fields.contains("projectionItems")); assert!(artifact.input_fields.contains("expandedIds")); assert!(artifact.input_fields.contains("selectedRowIds")); assert!(artifact.input_fields.contains("activePickerItem")); assert!(artifact.input_fields.contains("focusedId")); assert!(artifact.output_channels.contains("domPatch")); assert!(artifact.output_channels.contains("intentEvent")); assert!(artifact.output_channels.contains("commandDispatchEvent")); assert_eq!( artifact.runtime_api.request_contract, "TreeShellRuntimeRequest" ); assert_eq!( artifact.runtime_api.result_contract, "TreeShellRuntimeResult" ); assert_eq!( artifact.runtime_api.reduce_endpoint, "/api/tree/runtime/reduce" ); assert!(artifact.runtime_api.state_snapshots.contains("page")); assert!(artifact.runtime_api.state_snapshots.contains("fileTree")); assert!(artifact.runtime_api.state_snapshots.contains("picker")); assert!(artifact.runtime_api.dom_patch_kinds.contains("pageState")); assert!(artifact .runtime_api .dom_patch_kinds .contains("fileTreeState")); assert!(artifact.runtime_api.dom_patch_kinds.contains("pickerState")); assert!(artifact.runtime_api.host_event_kinds.contains("pageOpen")); assert!(artifact .runtime_api .host_event_kinds .contains("fileTreeOpen")); assert!(artifact .runtime_api .host_event_kinds .contains("fileTreeInternalDrop")); assert!(artifact .runtime_api .host_event_kinds .contains("fileTreeExternalDrop")); assert!(artifact .runtime_api .host_event_kinds .contains("pickerPickDocument")); assert!(artifact .runtime_api .command_event_kinds .contains("createNode")); assert!(artifact .runtime_api .command_event_kinds .contains("renameNode")); assert!(artifact .runtime_api .command_event_kinds .contains("moveSubtree")); assert!(artifact .runtime_api .command_event_kinds .contains("copyResource")); assert!(artifact .runtime_api .command_event_kinds .contains("moveResource")); assert!(artifact .runtime_api .command_event_kinds .contains("uploadResource")); assert!(artifact.event_kinds.contains("focus")); assert!(artifact.event_kinds.contains("keyboard")); assert!(artifact.event_kinds.contains("expandCollapse")); assert!(artifact.event_kinds.contains("selection")); assert!(artifact.event_kinds.contains("contextMenu")); assert!(artifact.event_kinds.contains("dragDrop")); assert!(artifact.event_kinds.contains("pick")); } }