Fix starred scope page tree and local edit save

This commit is contained in:
lix-2026
2026-05-27 13:50:31 +08:00
parent c01dc2dea2
commit bd7470d03e
8 changed files with 414 additions and 5 deletions
@@ -2,9 +2,11 @@ import {
conflictDetectionKeyFromBody,
editorDocumentFromTiptapDocument,
flattenText,
hydrateMindmapAttrsFromDom,
legacyBlocksFromEditorDocument,
pageBodyTiptapDocument,
revisionFromConflictKey,
textToTiptapDocument,
toTiptapDocument,
} from './document-tiptap-conversion-runtime.js';
@@ -687,6 +687,23 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return root;
}
function ensureStarredFolderPageTreeHost(workspaceId) {
var root = document.getElementById('sidebar-tree-root');
if (root instanceof HTMLElement) return root;
var panel = document.getElementById('wolai-sidebar-page-tree-panel') || document.querySelector('[data-mnote-sidebar-tree-panel="page"]');
if (!(panel instanceof HTMLElement)) return null;
var section = document.createElement('div');
section.className = 'sidebar-tree-section';
root = document.createElement('div');
root.id = 'sidebar-tree-root';
root.className = 'sidebar-tree';
root.setAttribute('data-tree-shell-mode', 'page');
if (workspaceId) root.setAttribute('data-workspace-id', workspaceId);
section.appendChild(root);
panel.appendChild(section);
return root;
}
function persistStarredFolderScope(workspaceId, rootUri, relativePath) {
var targetUrl = new URL(window.location.href);
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
@@ -719,17 +736,23 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var tab = document.querySelector('[data-mnote-sidebar-tree-tab="filetree"]');
if (tab instanceof HTMLElement) switchSidebarTreeTab(tab);
var root = ensureStarredFolderFileTreeHost(workspaceId);
var pageRoot = ensureStarredFolderPageTreeHost(workspaceId);
if (root instanceof HTMLElement) {
root.setAttribute('data-workspace-id', workspaceId);
root.setAttribute('data-mnote-filetree-scope', relativePath);
root.setAttribute('data-mnote-filetree-scope-title', row.textContent ? row.textContent.trim() : relativePath);
}
if (pageRoot instanceof HTMLElement) {
pageRoot.setAttribute('data-workspace-id', workspaceId);
pageRoot.setAttribute('data-mnote-page-tree-scope', relativePath);
}
persistStarredFolderScope(workspaceId, rootUri, relativePath);
document.documentElement.setAttribute('data-mnote-filetree-scope', relativePath);
var sidebarUrl = new URL('/api/tree/projections/sidebar', window.location.origin);
sidebarUrl.searchParams.set('workspaceId', workspaceId);
sidebarUrl.searchParams.set('sourceKind', 'local_folder');
sidebarUrl.searchParams.set('rootUri', rootUri);
sidebarUrl.searchParams.set('parentRelativePath', relativePath);
var url = new URL('/api/tree/projections/file', window.location.origin);
url.searchParams.set('workspaceId', workspaceId);
url.searchParams.set('sourceKind', 'local_folder');
+49 -1
View File
@@ -4,7 +4,7 @@ use crate::error::WebError;
use crate::routes::local_folder_source::{
ensure_local_workspace_read_access_with_state, load_local_folder_file_tree_children_snapshot,
load_local_folder_file_tree_snapshot, load_local_folder_file_tree_snapshot_with_reveal,
load_local_folder_page_tree_snapshot,
load_local_folder_page_tree_scope_snapshot, load_local_folder_page_tree_snapshot,
};
use crate::routes::query_support::resolve_effective_workspace_id;
use crate::routes::snapshot_support::{
@@ -120,6 +120,8 @@ async fn project_projection(
} else {
load_local_folder_file_tree_snapshot(&root_uri)
}
} else if let Some(parent_relative_path) = parent_relative_path.as_deref() {
load_local_folder_page_tree_scope_snapshot(&root_uri, parent_relative_path)
} else {
load_local_folder_page_tree_snapshot(&root_uri)
}
@@ -422,6 +424,52 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn local_folder_page_projection_can_scope_by_parent_path() {
let root = std::env::temp_dir().join(format!(
"mnote-local-kernel-page-scope-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("design")).expect("create design");
std::fs::write(root.join("Home.md"), "# Home\n").expect("write home");
std::fs::write(root.join("Other.md"), "# Other\n").expect("write other");
std::fs::write(root.join("design").join("Brief.md"), "# Brief\n").expect("write brief");
let root_uri = format!("file://{}", root.display());
initialize_local_workspace_for_actor("dev-user", &root_uri).expect("init local workspace");
let response = app()
.oneshot(
Request::builder()
.uri(format!(
"/api/tree/projections/sidebar?workspaceId=local-ws:dev-user:my-space&sourceKind=local_folder&rootUri={root_uri}&parentRelativePath=design"
))
.header("x-mnote-actor-id", "dev-user")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: Value = serde_json::from_slice(&body).expect("json");
let items = payload["result"]["items"].as_array().expect("items");
let titles = items
.iter()
.filter_map(|item| item["title"].as_str())
.collect::<Vec<_>>();
assert_eq!(payload["result"]["parentRelativePath"], "design");
assert!(titles.contains(&"Brief"));
assert!(!titles.contains(&"Home"));
assert!(!titles.contains(&"Other"));
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn local_folder_file_projection_can_load_direct_children_by_parent_path() {
let root = std::env::temp_dir().join(format!(
@@ -2767,6 +2767,20 @@ fn load_local_folder_file_tree_scope_snapshot(
pub fn load_local_folder_page_tree_snapshot(
root_uri: &str,
) -> Result<ProjectionSnapshot, WebError> {
load_local_folder_page_tree_snapshot_for_scope(root_uri, None)
}
pub fn load_local_folder_page_tree_scope_snapshot(
root_uri: &str,
parent_relative_path: &str,
) -> Result<ProjectionSnapshot, WebError> {
load_local_folder_page_tree_snapshot_for_scope(root_uri, Some(parent_relative_path))
}
fn load_local_folder_page_tree_snapshot_for_scope(
root_uri: &str,
parent_relative_path: Option<&str>,
) -> Result<ProjectionSnapshot, WebError> {
#[cfg(test)]
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
@@ -2787,9 +2801,25 @@ pub fn load_local_folder_page_tree_snapshot(
let workspace_id = local_workspace_id(&canonical_root);
let root_source_uri = file_uri_for_path(&canonical_root);
let parent_relative_path = parent_relative_path
.map(str::trim)
.filter(|value| !value.is_empty() && *value != ".")
.unwrap_or("");
let scan_root = if parent_relative_path.is_empty() {
canonical_root.clone()
} else {
resolve_metadata_relative_path(&canonical_root, parent_relative_path)?
};
if !scan_root.is_dir() {
return Err(WebError::bad_request_code(
"local_folder_parent_not_directory",
"PageTree scope parentRelativePath 必须指向目录",
));
}
let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
let cache_key = format!("{root_source_uri}\n{parent_relative_path}");
if let Ok(cache) = local_page_tree_snapshot_cache().lock() {
if let Some(entry) = cache.get(&root_source_uri) {
if let Some(entry) = cache.get(&cache_key) {
if entry.watch_revision.revision == watch_revision.revision
&& entry.watch_revision.entry_count == watch_revision.entry_count
&& entry.watch_revision.latest_modified_ms == watch_revision.latest_modified_ms
@@ -2804,7 +2834,7 @@ pub fn load_local_folder_page_tree_snapshot(
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
scan_markdown_page_tree(
&canonical_root,
&canonical_root,
&scan_root,
None,
0,
None,
@@ -2859,6 +2889,7 @@ pub fn load_local_folder_page_tree_snapshot(
"projection": "page_tree",
"sourceKind": "local_folder",
"rootUri": root_source_uri,
"parentRelativePath": parent_relative_path,
"watchRevision": watch_revision,
"items": items,
}),
@@ -2868,7 +2899,7 @@ pub fn load_local_folder_page_tree_snapshot(
cache.clear();
}
cache.insert(
root_source_uri,
cache_key,
LocalPageTreeSnapshotCacheEntry {
watch_revision,
snapshot: snapshot.clone(),