收口工作台资源 tab 与本地文件树 P1
This commit is contained in:
@@ -43,6 +43,7 @@ struct LocalFolderMetadata {
|
||||
page_options: BTreeMap<String, Value>,
|
||||
trash_entries: BTreeMap<String, LocalTrashEntry>,
|
||||
uploaded_assets: BTreeMap<String, LocalUploadedAssetEntry>,
|
||||
file_order: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -3300,6 +3301,7 @@ fn rename_local_markdown_page(
|
||||
})?;
|
||||
|
||||
let new_relative_path = normalize_relative_path(root, &target)?;
|
||||
apply_file_order_path_rewrite(root, &old_relative_path, &new_relative_path)?;
|
||||
let new_document_id = local_markdown_path_page_id(&new_relative_path);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
@@ -3384,6 +3386,13 @@ fn rename_nested_bundle_markdown_page(
|
||||
}
|
||||
}
|
||||
let new_relative_path = normalize_relative_path(root, &new_markdown)?;
|
||||
let old_bundle_relative_path = markdown_file
|
||||
.path
|
||||
.parent()
|
||||
.and_then(|parent| normalize_relative_path(root, parent).ok())
|
||||
.unwrap_or_else(|| old_relative_path.clone());
|
||||
let new_bundle_relative_path = normalize_relative_path(root, &new_bundle_dir)?;
|
||||
apply_file_order_path_rewrite(root, &old_bundle_relative_path, &new_bundle_relative_path)?;
|
||||
let new_document_id = local_markdown_path_page_id(&new_relative_path);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
@@ -3416,6 +3425,8 @@ fn rename_local_directory(root: &Path, directory: &Path, title: &str) -> Result<
|
||||
)
|
||||
})?;
|
||||
let new_relative_path = normalize_relative_path(root, &target)?;
|
||||
let old_relative_path = normalize_relative_path(root, directory)?;
|
||||
apply_file_order_path_rewrite(root, &old_relative_path, &new_relative_path)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": local_directory_group_id(&new_relative_path),
|
||||
@@ -3524,19 +3535,36 @@ fn move_local_entry(
|
||||
parent_id: Option<&str>,
|
||||
sort_order: Option<i64>,
|
||||
) -> Result<Value, WebError> {
|
||||
let original_relative_path =
|
||||
if let Some(directory) = resolve_local_directory_id(root, document_id)? {
|
||||
Some(normalize_relative_path(root, &directory)?)
|
||||
} else {
|
||||
load_local_folder_metadata(root)
|
||||
.ok()
|
||||
.and_then(|metadata| {
|
||||
find_markdown_by_page_id(root, &metadata, document_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.and_then(|entry| {
|
||||
markdown_page_bundle_directory(&entry.path)
|
||||
.and_then(|directory| normalize_relative_path(root, &directory).ok())
|
||||
.or(Some(entry.relative_path))
|
||||
})
|
||||
};
|
||||
let result = if let Some(directory) = resolve_local_directory_id(root, document_id)? {
|
||||
move_local_directory(root, &directory, parent_id)?
|
||||
} else {
|
||||
move_local_markdown_page(root, document_id, parent_id)?
|
||||
};
|
||||
if let Some(sort_order) = sort_order.filter(|value| *value >= 0) {
|
||||
if let Some(object) = result.as_object() {
|
||||
let mut enriched = object.clone();
|
||||
enriched.insert(
|
||||
"_unsupportedFields".to_string(),
|
||||
json!({ "sortOrder": sort_order }),
|
||||
);
|
||||
return Ok(Value::Object(enriched));
|
||||
if let Some(relative_path) = result.get("orderRelativePath").and_then(Value::as_str) {
|
||||
update_local_file_order_after_move(
|
||||
root,
|
||||
original_relative_path.as_deref(),
|
||||
relative_path,
|
||||
sort_order,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
@@ -3571,6 +3599,7 @@ fn move_local_markdown_page(
|
||||
"id": document_id,
|
||||
"documentId": document_id,
|
||||
"relativePath": markdown_file.relative_path,
|
||||
"orderRelativePath": markdown_file.relative_path,
|
||||
"action": "move",
|
||||
"sourceKind": "local_folder",
|
||||
}));
|
||||
@@ -3588,6 +3617,7 @@ fn move_local_markdown_page(
|
||||
"id": document_id,
|
||||
"documentId": document_id,
|
||||
"relativePath": markdown_file.relative_path,
|
||||
"orderRelativePath": markdown_file.relative_path,
|
||||
"action": "move",
|
||||
"sourceKind": "local_folder",
|
||||
}));
|
||||
@@ -3615,6 +3645,7 @@ fn move_local_markdown_page(
|
||||
"id": new_document_id,
|
||||
"documentId": new_document_id,
|
||||
"relativePath": new_relative_path,
|
||||
"orderRelativePath": new_relative_path,
|
||||
"previousDocumentId": document_id,
|
||||
"previousRelativePath": old_relative_path,
|
||||
"action": "move",
|
||||
@@ -3635,6 +3666,7 @@ fn move_local_markdown_bundle(
|
||||
"id": document_id,
|
||||
"documentId": document_id,
|
||||
"relativePath": markdown_file.relative_path,
|
||||
"orderRelativePath": normalize_relative_path(root, bundle_dir)?,
|
||||
"action": "move",
|
||||
"sourceKind": "local_folder",
|
||||
}));
|
||||
@@ -3680,12 +3712,14 @@ fn move_local_markdown_bundle(
|
||||
})?;
|
||||
}
|
||||
let new_relative_path = normalize_relative_path(root, &target_markdown)?;
|
||||
let new_bundle_relative_path = normalize_relative_path(root, &target_bundle)?;
|
||||
let new_document_id = local_markdown_path_page_id(&new_relative_path);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": new_document_id,
|
||||
"documentId": new_document_id,
|
||||
"relativePath": new_relative_path,
|
||||
"orderRelativePath": new_bundle_relative_path,
|
||||
"previousDocumentId": document_id,
|
||||
"previousRelativePath": markdown_file.relative_path,
|
||||
"action": "move",
|
||||
@@ -3717,6 +3751,7 @@ fn move_local_directory(
|
||||
"ok": true,
|
||||
"id": local_directory_group_id(&normalize_relative_path(root, directory)?),
|
||||
"relativePath": normalize_relative_path(root, directory)?,
|
||||
"orderRelativePath": normalize_relative_path(root, directory)?,
|
||||
"action": "move",
|
||||
"sourceKind": "local_folder",
|
||||
}));
|
||||
@@ -3738,6 +3773,7 @@ fn move_local_directory(
|
||||
"ok": true,
|
||||
"id": local_directory_group_id(&new_relative_path),
|
||||
"relativePath": new_relative_path,
|
||||
"orderRelativePath": new_relative_path,
|
||||
"action": "move",
|
||||
"sourceKind": "local_folder",
|
||||
}))
|
||||
@@ -3838,6 +3874,7 @@ fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
markdown_file.relative_path.clone()
|
||||
};
|
||||
let trash_relative_path = normalize_relative_path(root, &target)?;
|
||||
remove_file_order_path(&mut metadata.file_order, &original_relative_path);
|
||||
metadata.trash_entries.insert(
|
||||
document_id.to_string(),
|
||||
LocalTrashEntry {
|
||||
@@ -3855,6 +3892,7 @@ fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
},
|
||||
);
|
||||
write_trash_index_metadata(root, &metadata.trash_entries)?;
|
||||
write_file_order_metadata(root, &metadata.file_order)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": document_id,
|
||||
@@ -3896,6 +3934,7 @@ fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Valu
|
||||
let trash_relative_path = normalize_relative_path(root, &target)?;
|
||||
let trash_entry_id = local_file_trash_entry_id(&relative_path);
|
||||
let now = now_ms();
|
||||
remove_file_order_path(&mut metadata.file_order, &relative_path);
|
||||
metadata.trash_entries.insert(
|
||||
trash_entry_id.clone(),
|
||||
LocalTrashEntry {
|
||||
@@ -3913,6 +3952,7 @@ fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Valu
|
||||
},
|
||||
);
|
||||
write_trash_index_metadata(root, &metadata.trash_entries)?;
|
||||
write_file_order_metadata(root, &metadata.file_order)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
@@ -3967,6 +4007,7 @@ fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Resul
|
||||
let trash_relative_path = normalize_relative_path(root, &target)?;
|
||||
let trash_entry_id = local_directory_trash_entry_id(&relative_path);
|
||||
let now = now_ms();
|
||||
remove_file_order_path(&mut metadata.file_order, &relative_path);
|
||||
metadata.trash_entries.insert(
|
||||
trash_entry_id.clone(),
|
||||
LocalTrashEntry {
|
||||
@@ -3984,6 +4025,7 @@ fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Resul
|
||||
},
|
||||
);
|
||||
write_trash_index_metadata(root, &metadata.trash_entries)?;
|
||||
write_file_order_metadata(root, &metadata.file_order)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
@@ -4276,6 +4318,9 @@ fn restore_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebErro
|
||||
fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
if let Some(markdown_file) = find_markdown_by_page_id(root, &metadata, document_id)? {
|
||||
let order_relative_path = markdown_page_bundle_directory(&markdown_file.path)
|
||||
.and_then(|bundle_dir| normalize_relative_path(root, &bundle_dir).ok())
|
||||
.unwrap_or_else(|| markdown_file.relative_path.clone());
|
||||
if let Some(bundle_dir) = markdown_page_bundle_directory(&markdown_file.path) {
|
||||
fs::remove_dir_all(&bundle_dir).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -4294,6 +4339,8 @@ fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
)
|
||||
})?;
|
||||
}
|
||||
remove_file_order_path(&mut metadata.file_order, &order_relative_path);
|
||||
write_file_order_metadata(root, &metadata.file_order)?;
|
||||
return Ok(json!({
|
||||
"ok": true,
|
||||
"id": document_id,
|
||||
@@ -4308,6 +4355,10 @@ fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
"找不到要永久删除的本地 Markdown 页面",
|
||||
)
|
||||
})?;
|
||||
remove_file_order_path(
|
||||
&mut metadata.file_order,
|
||||
&trash_entry.original_relative_path,
|
||||
);
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
|
||||
if trash_path.exists() {
|
||||
let remove_result = if trash_path.is_dir() {
|
||||
@@ -4326,6 +4377,7 @@ fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
|
||||
})?;
|
||||
}
|
||||
write_trash_index_metadata(root, &metadata.trash_entries)?;
|
||||
write_file_order_metadata(root, &metadata.file_order)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": document_id,
|
||||
@@ -4349,6 +4401,10 @@ fn purge_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError>
|
||||
"找不到要永久删除的本地文件夹回收站记录",
|
||||
)
|
||||
})?;
|
||||
remove_file_order_path(
|
||||
&mut metadata.file_order,
|
||||
&trash_entry.original_relative_path,
|
||||
);
|
||||
let trash_relative_path = local_trash_file_path(&trash_entry);
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
|
||||
if trash_path.exists() {
|
||||
@@ -4363,6 +4419,7 @@ fn purge_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError>
|
||||
})?;
|
||||
}
|
||||
write_trash_index_metadata(root, &metadata.trash_entries)?;
|
||||
write_file_order_metadata(root, &metadata.file_order)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"id": entry_id,
|
||||
@@ -4390,6 +4447,10 @@ fn purge_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError>
|
||||
"找不到要永久删除的本地资源回收站记录",
|
||||
)
|
||||
})?;
|
||||
remove_file_order_path(
|
||||
&mut metadata.file_order,
|
||||
&trash_entry.original_relative_path,
|
||||
);
|
||||
let trash_relative_path = local_trash_file_path(&trash_entry);
|
||||
let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
|
||||
if trash_path.exists() {
|
||||
@@ -4740,6 +4801,7 @@ fn load_local_folder_metadata(root: &Path) -> Result<LocalFolderMetadata, WebErr
|
||||
uploaded_assets: load_uploaded_asset_index_map(
|
||||
&root.join(".mnote").join("uploaded-assets.json"),
|
||||
)?,
|
||||
file_order: load_file_order_metadata(&root.join(".mnote").join("file-order.json"))?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4815,6 +4877,25 @@ fn load_uploaded_asset_index_map(
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn load_file_order_metadata(path: &Path) -> Result<BTreeMap<String, Vec<String>>, WebError> {
|
||||
if !path.exists() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
let value = read_metadata_json(path)?;
|
||||
let source = value.get("parents").unwrap_or(&value);
|
||||
let orders: BTreeMap<String, Vec<String>> = serde_json::from_value(source.clone())
|
||||
.map_err(|error| metadata_invalid(path, format!("文件树排序索引损坏: {error}")))?;
|
||||
Ok(orders
|
||||
.into_iter()
|
||||
.map(|(parent, children)| {
|
||||
(
|
||||
normalize_file_order_parent_key(&parent),
|
||||
normalize_file_order_children(children),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn metadata_invalid(path: &Path, message: impl Into<String>) -> WebError {
|
||||
WebError::bad_request_code(
|
||||
"local_metadata_invalid",
|
||||
@@ -4907,6 +4988,25 @@ fn write_uploaded_asset_index_metadata(
|
||||
write_json_atomic(&path, &value)
|
||||
}
|
||||
|
||||
fn write_file_order_metadata(
|
||||
root: &Path,
|
||||
file_order: &BTreeMap<String, Vec<String>>,
|
||||
) -> Result<(), WebError> {
|
||||
let mnote_dir = root.join(".mnote");
|
||||
fs::create_dir_all(&mnote_dir).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"local_metadata_write_failed",
|
||||
format!("无法创建本地元数据目录 {}: {error}", mnote_dir.display()),
|
||||
)
|
||||
})?;
|
||||
let path = mnote_dir.join("file-order.json");
|
||||
let value = json!({
|
||||
"version": 1,
|
||||
"parents": file_order,
|
||||
});
|
||||
write_json_atomic(&path, &value)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn write_json_atomic(path: &Path, value: &Value) -> Result<(), WebError> {
|
||||
let tmp_path = path.with_extension("json.tmp");
|
||||
@@ -4937,7 +5037,9 @@ fn scan_directory(
|
||||
metadata: &LocalFolderMetadata,
|
||||
rows: &mut Vec<LocalFolderRow>,
|
||||
) -> Result<(), WebError> {
|
||||
let entries = read_sorted_entries(directory, root)?;
|
||||
let mut entries = read_sorted_entries(directory, root)?;
|
||||
let parent_key = file_order_parent_key_for_directory(root, directory)?;
|
||||
sort_entries_with_file_order(&mut entries, metadata, &parent_key);
|
||||
let entry_count = entries.len();
|
||||
for (position, entry) in entries.into_iter().enumerate() {
|
||||
let node_id = local_node_id(if entry.relative_path.is_empty() {
|
||||
@@ -5119,6 +5221,199 @@ fn compare_local_entries(a: &LocalFolderEntry, b: &LocalFolderEntry) -> Ordering
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_file_order_parent_key(parent: &str) -> String {
|
||||
let trimmed = parent.trim().trim_matches('/');
|
||||
if trimmed.is_empty() || trimmed == "." {
|
||||
".".to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_file_order_children(children: Vec<String>) -> Vec<String> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
children
|
||||
.into_iter()
|
||||
.map(|child| child.trim().trim_matches('/').to_string())
|
||||
.filter(|child| !child.is_empty() && seen.insert(child.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn file_order_parent_key_for_directory(root: &Path, directory: &Path) -> Result<String, WebError> {
|
||||
if directory == root {
|
||||
return Ok(".".to_string());
|
||||
}
|
||||
Ok(normalize_file_order_parent_key(&normalize_relative_path(
|
||||
root, directory,
|
||||
)?))
|
||||
}
|
||||
|
||||
fn parent_key_for_relative_path(relative_path: &str) -> String {
|
||||
Path::new(relative_path)
|
||||
.parent()
|
||||
.and_then(|parent| {
|
||||
let value = parent
|
||||
.components()
|
||||
.map(|component| component.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
})
|
||||
.map(|value| normalize_file_order_parent_key(&value))
|
||||
.unwrap_or_else(|| ".".to_string())
|
||||
}
|
||||
|
||||
fn sort_entries_with_file_order(
|
||||
entries: &mut [LocalFolderEntry],
|
||||
metadata: &LocalFolderMetadata,
|
||||
parent_key: &str,
|
||||
) {
|
||||
let Some(order) = metadata.file_order.get(parent_key) else {
|
||||
return;
|
||||
};
|
||||
let index = order
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(position, relative_path)| (relative_path.as_str(), position))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
entries.sort_by(|a, b| {
|
||||
let a_index = index.get(a.relative_path.as_str()).copied();
|
||||
let b_index = index.get(b.relative_path.as_str()).copied();
|
||||
match (a_index, b_index) {
|
||||
(Some(left), Some(right)) => left.cmp(&right),
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(None, None) => compare_local_entries(a, b),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn ordered_child_paths_for_parent(
|
||||
root: &Path,
|
||||
parent_directory: &Path,
|
||||
) -> Result<Vec<String>, WebError> {
|
||||
let mut entries = read_sorted_entries(parent_directory, root)?;
|
||||
let parent_key = file_order_parent_key_for_directory(root, parent_directory)?;
|
||||
let metadata = load_local_folder_metadata(root)?;
|
||||
sort_entries_with_file_order(&mut entries, &metadata, &parent_key);
|
||||
Ok(entries
|
||||
.into_iter()
|
||||
.map(|entry| entry.relative_path)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn reorder_child_paths(children: &mut Vec<String>, child_path: &str, sort_order: i64) {
|
||||
children.retain(|candidate| candidate != child_path);
|
||||
let index = usize::try_from(sort_order)
|
||||
.unwrap_or(usize::MAX)
|
||||
.min(children.len());
|
||||
children.insert(index, child_path.to_string());
|
||||
}
|
||||
|
||||
fn update_local_file_order_after_move(
|
||||
root: &Path,
|
||||
original_relative_path: Option<&str>,
|
||||
new_relative_path: &str,
|
||||
sort_order: i64,
|
||||
) -> Result<(), WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
if let Some(original) = original_relative_path
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let original_parent = parent_key_for_relative_path(original);
|
||||
if original_parent != parent_key_for_relative_path(new_relative_path) {
|
||||
if let Some(children) = metadata.file_order.get_mut(&original_parent) {
|
||||
children.retain(|candidate| candidate != original);
|
||||
}
|
||||
}
|
||||
}
|
||||
let parent_key = parent_key_for_relative_path(new_relative_path);
|
||||
let parent_directory = if parent_key == "." {
|
||||
root.to_path_buf()
|
||||
} else {
|
||||
resolve_metadata_relative_path(root, &parent_key)?
|
||||
};
|
||||
let mut children = ordered_child_paths_for_parent(root, &parent_directory)?;
|
||||
reorder_child_paths(&mut children, new_relative_path, sort_order);
|
||||
metadata.file_order.insert(parent_key, children);
|
||||
write_file_order_metadata(root, &metadata.file_order)
|
||||
}
|
||||
|
||||
fn rewrite_file_order_path(
|
||||
file_order: &mut BTreeMap<String, Vec<String>>,
|
||||
old_relative_path: &str,
|
||||
new_relative_path: &str,
|
||||
) {
|
||||
for children in file_order.values_mut() {
|
||||
for child in children.iter_mut() {
|
||||
if child == old_relative_path {
|
||||
*child = new_relative_path.to_string();
|
||||
} else if child.starts_with(&format!("{old_relative_path}/")) {
|
||||
*child = format!("{}{}", new_relative_path, &child[old_relative_path.len()..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let old_parent_prefix = format!("{old_relative_path}/");
|
||||
let parent_rewrites = file_order
|
||||
.keys()
|
||||
.filter_map(|parent| {
|
||||
if parent == old_relative_path {
|
||||
Some((parent.clone(), new_relative_path.to_string()))
|
||||
} else if parent.starts_with(&old_parent_prefix) {
|
||||
Some((
|
||||
parent.clone(),
|
||||
format!(
|
||||
"{}{}",
|
||||
new_relative_path,
|
||||
&parent[old_relative_path.len()..]
|
||||
),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (old_parent, new_parent) in parent_rewrites {
|
||||
if let Some(children) = file_order.remove(&old_parent) {
|
||||
file_order.insert(new_parent, children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_file_order_path(file_order: &mut BTreeMap<String, Vec<String>>, relative_path: &str) {
|
||||
let child_prefix = format!("{relative_path}/");
|
||||
for children in file_order.values_mut() {
|
||||
children.retain(|child| child != relative_path && !child.starts_with(&child_prefix));
|
||||
}
|
||||
let parent_keys = file_order
|
||||
.keys()
|
||||
.filter(|parent| parent.as_str() == relative_path || parent.starts_with(&child_prefix))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for parent in parent_keys {
|
||||
file_order.remove(&parent);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_file_order_path_rewrite(
|
||||
root: &Path,
|
||||
old_relative_path: &str,
|
||||
new_relative_path: &str,
|
||||
) -> Result<(), WebError> {
|
||||
let mut metadata = load_local_folder_metadata(root)?;
|
||||
rewrite_file_order_path(
|
||||
&mut metadata.file_order,
|
||||
old_relative_path,
|
||||
new_relative_path,
|
||||
);
|
||||
write_file_order_metadata(root, &metadata.file_order)
|
||||
}
|
||||
|
||||
fn scan_markdown_page_tree(
|
||||
root: &Path,
|
||||
directory: &Path,
|
||||
@@ -5130,7 +5425,9 @@ fn scan_markdown_page_tree(
|
||||
workspace_id: &str,
|
||||
root_source_uri: &str,
|
||||
) -> Result<bool, WebError> {
|
||||
let entries = read_sorted_entries(directory, root)?;
|
||||
let mut entries = read_sorted_entries(directory, root)?;
|
||||
let parent_key = file_order_parent_key_for_directory(root, directory)?;
|
||||
sort_entries_with_file_order(&mut entries, metadata, &parent_key);
|
||||
let mut directory_rows = Vec::<LocalFolderRow>::new();
|
||||
let mut contains_markdown = false;
|
||||
for (position, entry) in entries.into_iter().enumerate() {
|
||||
@@ -6677,7 +6974,7 @@ mod tests {
|
||||
use axum::extract::{Extension, Path as AxumPath, Query};
|
||||
use axum::http::{HeaderMap, Method, StatusCode};
|
||||
use axum::Json;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::Value;
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
@@ -8297,18 +8594,41 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_tree_command_move_with_sort_order_annotates_unsupported() {
|
||||
fn local_tree_command_move_with_sort_order_persists_file_order() {
|
||||
let root = temp_root("mnote-local-move-sort-order");
|
||||
init_workspace(&root);
|
||||
std::fs::write(root.join("alpha.md"), "# Alpha\n").expect("write alpha");
|
||||
std::fs::write(root.join("beta.md"), "# Beta\n").expect("write beta");
|
||||
std::fs::write(root.join("source.md"), "# Source\n").expect("write source");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let doc_id = "local-md:source.md";
|
||||
|
||||
let result =
|
||||
execute_local_tree_command_with_sort(&root_uri, "move", doc_id, None, None, Some(42))
|
||||
execute_local_tree_command_with_sort(&root_uri, "move", doc_id, None, None, Some(0))
|
||||
.expect("move with sort_order should succeed");
|
||||
assert_eq!(result["_unsupportedFields"]["sortOrder"], json!(42));
|
||||
assert!(result.get("_unsupportedFields").is_none());
|
||||
assert!(root.join("source.md").is_file());
|
||||
let order_index =
|
||||
std::fs::read_to_string(root.join(".mnote").join("file-order.json")).expect("order");
|
||||
assert!(
|
||||
order_index.contains("\"source.md\""),
|
||||
"file-order 应记录排序后的 source.md: {order_index}"
|
||||
);
|
||||
let snapshot =
|
||||
load_local_folder_file_tree_snapshot(&root_uri).expect("file tree snapshot after move");
|
||||
let items = snapshot.projection["items"]
|
||||
.as_array()
|
||||
.expect("projection items");
|
||||
let root_rows = items
|
||||
.iter()
|
||||
.filter(|item| item["parentNodeId"].is_null())
|
||||
.map(|item| {
|
||||
item["resourceMeta"]["extra"]["source"]["relativePath"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(root_rows.first().copied(), Some("source.md"));
|
||||
|
||||
let result_without_sort = execute_local_tree_command(&root_uri, "move", doc_id, None, None)
|
||||
.expect("move without sort_order");
|
||||
|
||||
Reference in New Issue
Block a user