Files
mnote/rust/crates/mnote-web/src/tree_shell/renderer_input.rs
T

391 lines
15 KiB
Rust

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<String>,
pub expanded_ids: BTreeSet<String>,
pub focused_id: Option<String>,
pub page_focus_keyboard_reducer: Option<PageFocusKeyboardReducerContract>,
pub filetree_selection: FileTreeSelectionState,
pub filetree_selection_reducer: Option<FileTreeSelectionReducerContract>,
pub active_picker_item: Option<String>,
pub excluded_picker_ids: BTreeSet<String>,
pub picker_state_reducer: Option<PickerStateReducerContract>,
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<String>,
}
#[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<String>,
pub expanded_ids: BTreeSet<String>,
pub focused_id: Option<String>,
pub command_dispatcher: TreeShellCommandDispatcher,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileTreeRendererInput {
pub projection_item_ids: Vec<String>,
pub expanded_ids: BTreeSet<String>,
pub filetree_selection: FileTreeSelectionState,
pub command_dispatcher: TreeShellCommandDispatcher,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PickerRendererInput {
pub projection_item_ids: Vec<String>,
pub expanded_ids: BTreeSet<String>,
pub active_picker_item: Option<String>,
pub excluded_picker_ids: BTreeSet<String>,
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<String> {
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"));
}
}