chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -46,6 +46,64 @@ pub struct WorkspaceShellEntry {
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
/// 使用页面树投影生成顶栏页面祖先链。
|
||||
pub fn render_page_breadcrumb_html(
|
||||
projection: &WorkspaceShellProjection,
|
||||
active_page_id: Option<&str>,
|
||||
) -> String {
|
||||
let Some(active_page_id) = active_page_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return String::new();
|
||||
};
|
||||
let by_id = projection
|
||||
.my_page_items
|
||||
.iter()
|
||||
.map(|item| (item.id.as_str(), item))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
let mut chain = Vec::new();
|
||||
let mut cursor = Some(active_page_id);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
while let Some(id) = cursor {
|
||||
if !seen.insert(id) {
|
||||
break;
|
||||
}
|
||||
let Some(item) = by_id.get(id) else {
|
||||
break;
|
||||
};
|
||||
chain.push(*item);
|
||||
cursor = item.parent_id.as_deref();
|
||||
if chain.len() >= 64 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
chain.reverse();
|
||||
chain
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let separator = if index == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
r#"<span class="wolai-breadcrumb-separator" aria-hidden="true">›</span>"#.to_string()
|
||||
};
|
||||
let title = escape_html(&item.title);
|
||||
let id = escape_html(&item.id);
|
||||
if index + 1 == chain.len() {
|
||||
format!(
|
||||
r#"{separator}<span class="wolai-breadcrumb-current" data-breadcrumb-document-id="{id}"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{title}</span></span>"#
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"{separator}<a class="wolai-breadcrumb-link" data-breadcrumb-document-id="{id}" href="{}">{title}</a>"#,
|
||||
escape_html(&item.href)
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn build_workspace_shell_projection(
|
||||
dataset: &Value,
|
||||
workspace_id: &str,
|
||||
@@ -553,6 +611,32 @@ mod tests {
|
||||
.any(|entry| entry.label == "模板中心"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_breadcrumb_renders_all_ancestors_as_links() {
|
||||
let projection = build_workspace_shell_projection(
|
||||
&json!({
|
||||
"workspaces": [{ "id": "ws_demo", "name": "我的空间" }],
|
||||
"documents": [
|
||||
{ "id": "root", "workspace_id": "ws_demo", "title": "个人", "parent_id": null },
|
||||
{ "id": "parent", "workspace_id": "ws_demo", "title": "密码", "parent_id": "root" },
|
||||
{ "id": "current", "workspace_id": "ws_demo", "title": "deepseek ai", "parent_id": "parent" }
|
||||
]
|
||||
}),
|
||||
"ws_demo",
|
||||
Some("current"),
|
||||
"我的空间",
|
||||
);
|
||||
|
||||
let html = render_page_breadcrumb_html(&projection, Some("current"));
|
||||
assert!(html.contains(r#"data-breadcrumb-document-id="root""#));
|
||||
assert!(html.contains(r#"data-breadcrumb-document-id="parent""#));
|
||||
assert!(html.contains(r#"data-breadcrumb-document-id="current""#));
|
||||
assert!(html.contains(r#"href="/documents/root?workspaceId=ws_demo""#));
|
||||
assert!(html.contains(r#"href="/documents/parent?workspaceId=ws_demo""#));
|
||||
assert!(html.contains("deepseek ai"));
|
||||
assert_eq!(html.matches("wolai-breadcrumb-separator").count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_shell_sidebar_html_outputs_projection_rows_with_active_and_parent() {
|
||||
let dataset = json!({
|
||||
@@ -652,6 +736,7 @@ mod tests {
|
||||
assert!(html.contains(r#"data-mnote-sidebar-tree-panel="filetree" hidden"#));
|
||||
assert!(html.contains(r#"id="sidebar-tree-root""#));
|
||||
assert!(html.contains(r#"id="sidebar-file-tree-root""#));
|
||||
assert!(!html.contains(r#"data-filetree-ssr="pending""#));
|
||||
assert!(!html.contains("wolai-file-tree-section"));
|
||||
}
|
||||
|
||||
@@ -687,6 +772,27 @@ mod tests {
|
||||
assert!(html.contains(r#"data-mnote-sidebar-tree-panel="filetree"><div"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_shell_sidebar_html_propagates_pending_filetree_shell_marker() {
|
||||
let dataset = json!({
|
||||
"workspaces": [{ "id": "ws_demo", "name": "开发用户 的工作区" }],
|
||||
"documents": []
|
||||
});
|
||||
let projection =
|
||||
build_workspace_shell_projection(&dataset, "ws_demo", None, "开发用户 的工作区");
|
||||
let html = render_workspace_shell_sidebar_html(
|
||||
&projection,
|
||||
Some(r#"<ul data-rust-page-renderer="initial_v1"></ul>"#),
|
||||
Some(r#"<ul class="tree-root" role="tree" data-rust-filetree-renderer="pending_shell_v1" data-filetree-ssr="pending" aria-busy="true"></ul>"#),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(html.contains(r#"id="sidebar-file-tree-root""#));
|
||||
assert!(html.contains(r#"data-filetree-ssr="pending""#));
|
||||
assert!(html.contains(r#"data-rust-filetree-renderer="pending_shell_v1""#));
|
||||
assert!(html.contains(r#"aria-busy="true""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_shell_sidebar_html_marks_filetree_scope() {
|
||||
let dataset = json!({
|
||||
@@ -805,8 +911,17 @@ pub fn render_workspace_shell_sidebar_html(
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Propagate shell-first pending marker onto the FileTree root so the client
|
||||
// can hydrate without scanning for nested placeholders.
|
||||
let pending_attr = if html.contains("data-filetree-ssr=\"pending\"")
|
||||
|| html.contains("data-rust-filetree-renderer=\"pending_shell_v1\"")
|
||||
{
|
||||
r#" data-filetree-ssr="pending" aria-busy="true""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
r#"<div class="sidebar-tree-section sidebar-file-tree-section"><div id="sidebar-file-tree-root" class="sidebar-tree" data-tree-shell-mode="filetree" data-workspace-id="{}"{file_tree_scope_attr}>{html}</div></div>"#,
|
||||
r#"<div class="sidebar-tree-section sidebar-file-tree-section"><div id="sidebar-file-tree-root" class="sidebar-tree" data-tree-shell-mode="filetree" data-workspace-id="{}"{file_tree_scope_attr}{pending_attr}>{html}</div></div>"#,
|
||||
escape_html(&projection.workspace_id),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user