feat(tree): complete rust family runtime checklist
- add tree shell runtime artifact contracts and page/filetree/picker runtime reducers - sink tree.subtree.move write operation through Rust and formalize command event plans - harden file tree search projection contract and route thin-proxy boundaries - record completed harness tasks and move design docs into process/done
This commit is contained in:
@@ -3,12 +3,15 @@ pub mod drag_drop_state;
|
||||
pub mod dispatcher;
|
||||
pub mod expansion_state;
|
||||
pub mod filetree_renderer;
|
||||
pub mod filetree_runtime;
|
||||
pub mod filetree_selection;
|
||||
pub mod focus_state;
|
||||
pub mod keyboard_state;
|
||||
pub mod loader;
|
||||
pub mod page_renderer;
|
||||
pub mod page_runtime;
|
||||
pub mod picker_renderer;
|
||||
pub mod picker_runtime;
|
||||
pub mod picker_state;
|
||||
pub mod protocol;
|
||||
pub mod renderer_input;
|
||||
|
||||
@@ -78,8 +78,19 @@ fn render_page_row(
|
||||
.find(|source| source.node_id == row.node_id)
|
||||
.map(|source| source.expanded)
|
||||
.unwrap_or(false);
|
||||
let toggle_html = if row.expandable {
|
||||
format!(
|
||||
r#"<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="{node_id}" aria-label="{label} {title}">{marker}</button>"#,
|
||||
node_id = escape_html(&row.node_id),
|
||||
label = if expanded { "折叠" } else { "展开" },
|
||||
title = escape_html(&row.title),
|
||||
marker = if expanded { "▾" } else { "▸" },
|
||||
)
|
||||
} else {
|
||||
r#"<span class="tree-spacer" aria-hidden="true"></span>"#.to_string()
|
||||
};
|
||||
html.push_str(&format!(
|
||||
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="{test_id}" data-node-id="{node_id}" data-shell-mode="page" data-active="{active}" data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="{node_id}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作">…</button></div></div>"#,
|
||||
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="{test_id}" data-node-id="{node_id}" data-shell-mode="page" data-active="{active}" data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}">{toggle_html}<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="{node_id}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作">…</button></div></div>"#,
|
||||
node_id = escape_html(&row.node_id),
|
||||
aria_level = row.depth + 1,
|
||||
expanded_attr = if row.expandable && expanded { "true" } else { "false" },
|
||||
@@ -87,6 +98,7 @@ fn render_page_row(
|
||||
active = active,
|
||||
focused = focused,
|
||||
tab_index = if focused { "0" } else { "-1" },
|
||||
toggle_html = toggle_html,
|
||||
title = escape_html(&row.title),
|
||||
));
|
||||
if row.expandable && expanded {
|
||||
@@ -206,6 +218,8 @@ mod tests {
|
||||
assert!(html.contains("data-rust-rendered-row=\"page\""));
|
||||
assert!(html.contains("data-shell-mode=\"page\""));
|
||||
assert!(html.contains("data-rust-action=\"open\""));
|
||||
assert!(html.contains("data-rust-action=\"toggle\""));
|
||||
assert!(html.contains("data-testid=\"tree-node-toggle\""));
|
||||
assert!(html.contains("data-rust-action=\"create\""));
|
||||
assert!(html.contains("draggable=\"true\""));
|
||||
assert!(html.contains("tree-children"));
|
||||
|
||||
@@ -52,11 +52,12 @@ fn render_picker_row(
|
||||
children_by_parent: &BTreeMap<Option<String>, Vec<PickerRenderRow>>,
|
||||
) {
|
||||
html.push_str(&format!(
|
||||
r#"<li class="tree-node" data-node-id="{node_id}"><button type="button" class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="picker" data-testid="tree-picker-row" data-node-id="{node_id}" data-shell-mode="picker" data-focused="{active}" data-rust-action="pick"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">{title}</span></span></button>"#,
|
||||
r#"<li class="tree-node" data-node-id="{node_id}"><button type="button" class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="picker" data-testid="tree-picker-row" data-node-id="{node_id}" data-shell-mode="picker" data-focused="{active}" data-rust-action="pick" tabindex="{tabindex}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">{title}</span></span></button>"#,
|
||||
node_id = escape_html(&row.node_id),
|
||||
aria_level = row.depth + 1,
|
||||
expanded_attr = if row.expandable && row.expanded { "true" } else { "false" },
|
||||
active = row.active,
|
||||
tabindex = if row.active { "0" } else { "-1" },
|
||||
title = escape_html(&row.title),
|
||||
));
|
||||
if row.expandable && row.expanded {
|
||||
@@ -77,8 +78,9 @@ pub fn render_initial_picker_html(input: &PickerInitialRenderInput) -> String {
|
||||
);
|
||||
if input.allow_root_pick {
|
||||
html.push_str(&format!(
|
||||
r#"<li class="tree-node" data-node-id="__root__"><button type="button" class="tree-row" data-testid="tree-picker-root" data-rust-rendered-row="picker-root" data-rust-action="pick-root" data-focused="{focused}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">根目录</span></span></button></li>"#,
|
||||
r#"<li class="tree-node" data-node-id="__root__"><button type="button" class="tree-row" data-testid="tree-picker-root" data-rust-rendered-row="picker-root" data-rust-action="pick-root" data-focused="{focused}" tabindex="{tabindex}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">根目录</span></span></button></li>"#,
|
||||
focused = input.root_active,
|
||||
tabindex = if input.root_active { "0" } else { "-1" },
|
||||
));
|
||||
}
|
||||
|
||||
@@ -180,6 +182,14 @@ mod tests {
|
||||
expandable: false,
|
||||
expanded: false,
|
||||
active: true,
|
||||
}, PickerRenderRow {
|
||||
node_id: "page_other".into(),
|
||||
parent_node_id: None,
|
||||
title: "其他页面".into(),
|
||||
depth: 0,
|
||||
expandable: false,
|
||||
expanded: false,
|
||||
active: false,
|
||||
}],
|
||||
});
|
||||
|
||||
@@ -190,5 +200,7 @@ mod tests {
|
||||
assert!(html.contains("data-testid=\"tree-picker-row\""));
|
||||
assert!(html.contains("首页 <安全>"));
|
||||
assert!(html.contains("data-focused=\"true\""));
|
||||
assert!(html.contains("tabindex=\"0\""));
|
||||
assert!(html.contains("tabindex=\"-1\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct TreeShellRendererInput {
|
||||
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)]
|
||||
@@ -35,6 +36,41 @@ pub struct TreeShellCommandDispatcher {
|
||||
pub command_names: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TreeShellRuntimeArtifactBoundary {
|
||||
pub contract_name: &'static str,
|
||||
pub input_fields: BTreeSet<&'static str>,
|
||||
pub output_channels: BTreeSet<&'static str>,
|
||||
pub event_kinds: BTreeSet<&'static str>,
|
||||
}
|
||||
|
||||
impl Default for TreeShellRuntimeArtifactBoundary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
contract_name: "rust_tree_shell_runtime_artifact_v1",
|
||||
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",
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeShellRendererInput {
|
||||
pub fn page(input: PageTreeRendererInput) -> Self {
|
||||
Self {
|
||||
@@ -49,6 +85,7 @@ impl TreeShellRendererInput {
|
||||
excluded_picker_ids: BTreeSet::new(),
|
||||
picker_state_reducer: None,
|
||||
command_dispatcher: input.command_dispatcher,
|
||||
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +102,7 @@ impl TreeShellRendererInput {
|
||||
excluded_picker_ids: BTreeSet::new(),
|
||||
picker_state_reducer: None,
|
||||
command_dispatcher: input.command_dispatcher,
|
||||
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +119,7 @@ impl TreeShellRendererInput {
|
||||
excluded_picker_ids: input.excluded_picker_ids,
|
||||
picker_state_reducer: Some(PickerStateReducerContract::default()),
|
||||
command_dispatcher: input.command_dispatcher,
|
||||
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,8 +152,8 @@ pub struct PickerRendererInput {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput, TreeShellCommandDispatcher,
|
||||
TreeShellRendererInput, TreeShellRendererMode,
|
||||
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput,
|
||||
TreeShellCommandDispatcher, TreeShellRendererInput, TreeShellRendererMode,
|
||||
};
|
||||
use crate::tree_shell::filetree_selection::FileTreeSelectionState;
|
||||
use std::collections::BTreeSet;
|
||||
@@ -158,7 +197,10 @@ mod tests {
|
||||
});
|
||||
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
|
||||
.filetree_selection
|
||||
.selected_row_ids
|
||||
.contains("asset:a"));
|
||||
assert!(filetree.page_focus_keyboard_reducer.is_none());
|
||||
assert_eq!(
|
||||
filetree
|
||||
@@ -188,4 +230,36 @@ mod tests {
|
||||
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!(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!(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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user