收敛树命令与投影切换
This commit is contained in:
@@ -771,7 +771,7 @@ struct DocumentMoveSnapshotDocument {
|
|||||||
#[serde(default, alias = "parent_id")]
|
#[serde(default, alias = "parent_id")]
|
||||||
parent_id: Option<String>,
|
parent_id: Option<String>,
|
||||||
#[serde(default, alias = "sort_order")]
|
#[serde(default, alias = "sort_order")]
|
||||||
sort_order: Option<i64>,
|
sort_order: Option<f64>,
|
||||||
#[serde(default, alias = "created_at")]
|
#[serde(default, alias = "created_at")]
|
||||||
created_at: Option<String>,
|
created_at: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -904,12 +904,18 @@ fn normalize_parent_id(value: &Option<String>) -> Option<String> {
|
|||||||
|
|
||||||
fn document_move_sort_key(document: &DocumentMoveSnapshotDocument) -> (i64, String, String) {
|
fn document_move_sort_key(document: &DocumentMoveSnapshotDocument) -> (i64, String, String) {
|
||||||
(
|
(
|
||||||
document.sort_order.unwrap_or(i64::MAX),
|
normalize_document_move_sort_order(document.sort_order).unwrap_or(i64::MAX),
|
||||||
document.created_at.clone().unwrap_or_default(),
|
document.created_at.clone().unwrap_or_default(),
|
||||||
document.id.clone(),
|
document.id.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_document_move_sort_order(value: Option<f64>) -> Option<i64> {
|
||||||
|
value
|
||||||
|
.filter(|number| number.is_finite())
|
||||||
|
.map(|number| number.floor() as i64)
|
||||||
|
}
|
||||||
|
|
||||||
fn clamp_document_move_index(sort_order: i64, max: usize) -> usize {
|
fn clamp_document_move_index(sort_order: i64, max: usize) -> usize {
|
||||||
if sort_order < 0 {
|
if sort_order < 0 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -927,7 +933,7 @@ fn append_document_move_order_patches(
|
|||||||
let sort_order = index as i64;
|
let sort_order = index as i64;
|
||||||
let moved = document.id == moved_document_id;
|
let moved = document.id == moved_document_id;
|
||||||
if normalize_parent_id(&document.parent_id) == parent_id
|
if normalize_parent_id(&document.parent_id) == parent_id
|
||||||
&& document.sort_order == Some(sort_order)
|
&& normalize_document_move_sort_order(document.sort_order) == Some(sort_order)
|
||||||
&& !moved
|
&& !moved
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -2286,11 +2292,15 @@ fn materialize_tree_domain_event_plans(
|
|||||||
.map(|event_plans| {
|
.map(|event_plans| {
|
||||||
event_plans
|
event_plans
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|event_plan| materialize_tree_domain_event_value(event_plan, plan, result))
|
.filter_map(|event_plan| {
|
||||||
|
materialize_tree_domain_event_value(event_plan, plan, result)
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.filter(|event_plans: &Vec<(String, Value)>| !event_plans.is_empty())
|
.filter(|event_plans: &Vec<(String, Value)>| !event_plans.is_empty())
|
||||||
.or_else(|| materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan]))
|
.or_else(|| {
|
||||||
|
materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan])
|
||||||
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2467,36 +2477,36 @@ pub fn build_runtime_command_artifact_plan(
|
|||||||
let domain_events: Vec<RuntimeDomainEventArtifactPlan> = materialized_event_plans
|
let domain_events: Vec<RuntimeDomainEventArtifactPlan> = materialized_event_plans
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, (event_type, domain_event_plan))| {
|
.map(
|
||||||
RuntimeDomainEventArtifactPlan {
|
|(index, (event_type, domain_event_plan))| RuntimeDomainEventArtifactPlan {
|
||||||
workspace_id: workspace_id.into(),
|
workspace_id: workspace_id.into(),
|
||||||
id: tree_domain_event_artifact_id(
|
id: tree_domain_event_artifact_id(
|
||||||
&command.command_id,
|
&command.command_id,
|
||||||
event_type,
|
event_type,
|
||||||
index,
|
index,
|
||||||
materialized_event_plans.len(),
|
materialized_event_plans.len(),
|
||||||
),
|
),
|
||||||
request_id: context.request_id.clone(),
|
request_id: context.request_id.clone(),
|
||||||
trace_id: context.trace_id.clone(),
|
trace_id: context.trace_id.clone(),
|
||||||
command_id: command.command_id.clone(),
|
command_id: command.command_id.clone(),
|
||||||
command_log_id: command_log_id.clone(),
|
command_log_id: command_log_id.clone(),
|
||||||
event_type: event_type.clone(),
|
event_type: event_type.clone(),
|
||||||
aggregate_type: aggregate_type.into(),
|
aggregate_type: aggregate_type.into(),
|
||||||
aggregate_id: aggregate_id.into(),
|
aggregate_id: aggregate_id.into(),
|
||||||
event_version: 1,
|
event_version: 1,
|
||||||
status: "committed".into(),
|
status: "committed".into(),
|
||||||
actor_type: context.actor.actor_type.clone(),
|
actor_type: context.actor.actor_type.clone(),
|
||||||
payload: tree_artifact_payload(
|
payload: tree_artifact_payload(
|
||||||
context,
|
context,
|
||||||
command,
|
command,
|
||||||
&event_type,
|
&event_type,
|
||||||
aggregate_type,
|
aggregate_type,
|
||||||
aggregate_id,
|
aggregate_id,
|
||||||
&domain_event_plan,
|
&domain_event_plan,
|
||||||
),
|
),
|
||||||
created_at: now.into(),
|
created_at: now.into(),
|
||||||
}
|
},
|
||||||
})
|
)
|
||||||
.collect();
|
.collect();
|
||||||
let domain_event = domain_events.first().cloned();
|
let domain_event = domain_events.first().cloned();
|
||||||
|
|
||||||
@@ -2507,7 +2517,12 @@ pub fn build_runtime_command_artifact_plan(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usize, total: usize) -> String {
|
fn tree_domain_event_artifact_id(
|
||||||
|
command_id: &str,
|
||||||
|
event_type: &str,
|
||||||
|
index: usize,
|
||||||
|
total: usize,
|
||||||
|
) -> String {
|
||||||
if total <= 1 || index == 0 {
|
if total <= 1 || index == 0 {
|
||||||
return format!("evt_{command_id}");
|
return format!("evt_{command_id}");
|
||||||
}
|
}
|
||||||
@@ -2524,7 +2539,10 @@ fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usiz
|
|||||||
format!("evt_{command_id}_{:02}_{suffix}", index + 1)
|
format!("evt_{command_id}_{:02}_{suffix}", index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_preflight_field<'a>(command: &'a RuntimeCommandEnvelopeWire, field: &str) -> Option<&'a Value> {
|
fn read_preflight_field<'a>(
|
||||||
|
command: &'a RuntimeCommandEnvelopeWire,
|
||||||
|
field: &str,
|
||||||
|
) -> Option<&'a Value> {
|
||||||
command
|
command
|
||||||
.preflight_data
|
.preflight_data
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -5168,7 +5186,9 @@ fn build_page_aggregate_projection_result(
|
|||||||
source: PageAggregateSource,
|
source: PageAggregateSource,
|
||||||
) -> Result<PageAggregateProjection, BridgeError> {
|
) -> Result<PageAggregateProjection, BridgeError> {
|
||||||
if data.is_null() {
|
if data.is_null() {
|
||||||
return Err(BridgeError::not_found("page.aggregate.get 未返回页面聚合数据"));
|
return Err(BridgeError::not_found(
|
||||||
|
"page.aggregate.get 未返回页面聚合数据",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let meta = data.get("meta").unwrap_or(data);
|
let meta = data.get("meta").unwrap_or(data);
|
||||||
@@ -5248,9 +5268,8 @@ fn build_page_aggregate_projection_result(
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or(Value::Null),
|
.unwrap_or(Value::Null),
|
||||||
};
|
};
|
||||||
let layout_options = serde_json::to_value(&page_options).map_err(|error| {
|
let layout_options = serde_json::to_value(&page_options)
|
||||||
BridgeError::transport(format!("PageOptions 序列化失败: {error}"))
|
.map_err(|error| BridgeError::transport(format!("PageOptions 序列化失败: {error}")))?;
|
||||||
})?;
|
|
||||||
let revision_ref = revision
|
let revision_ref = revision
|
||||||
.as_u64()
|
.as_u64()
|
||||||
.map(|value| format!("{resolved_document_id}:{value}"));
|
.map(|value| format!("{resolved_document_id}:{value}"));
|
||||||
@@ -5275,7 +5294,9 @@ fn build_page_aggregate_projection_result(
|
|||||||
title,
|
title,
|
||||||
updated_at: updated_at.map(Value::String).unwrap_or(Value::Null),
|
updated_at: updated_at.map(Value::String).unwrap_or(Value::Null),
|
||||||
permissions: PagePermissions {
|
permissions: PagePermissions {
|
||||||
read_only: bool_field(meta, "can_edit").map(|can_edit| !can_edit).unwrap_or(false),
|
read_only: bool_field(meta, "can_edit")
|
||||||
|
.map(|can_edit| !can_edit)
|
||||||
|
.unwrap_or(false),
|
||||||
disable_download: bool_field(meta, "disable_download")
|
disable_download: bool_field(meta, "disable_download")
|
||||||
.or_else(|| bool_field(meta, "disableDownload"))
|
.or_else(|| bool_field(meta, "disableDownload"))
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
@@ -5353,11 +5374,7 @@ fn build_mindmap_projection_result(
|
|||||||
.filter(|value| value.get("data").is_some() || value.get("children").is_some())
|
.filter(|value| value.get("data").is_some() || value.get("children").is_some())
|
||||||
.unwrap_or(data);
|
.unwrap_or(data);
|
||||||
let tree = normalize_mindmap_from_value(tree_input)?;
|
let tree = normalize_mindmap_from_value(tree_input)?;
|
||||||
let root_node = tree
|
let root_node = tree.data.uid.clone().unwrap_or_else(|| "root".into());
|
||||||
.data
|
|
||||||
.uid
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "root".into());
|
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges);
|
collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges);
|
||||||
@@ -5414,10 +5431,12 @@ fn collect_mindmap_projection_rows(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value {
|
fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value {
|
||||||
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| json!({
|
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| {
|
||||||
"enqueueAssetIds": [],
|
json!({
|
||||||
"results": []
|
"enqueueAssetIds": [],
|
||||||
}));
|
"results": []
|
||||||
|
})
|
||||||
|
});
|
||||||
if let Some(map) = payload.as_object_mut() {
|
if let Some(map) = payload.as_object_mut() {
|
||||||
map.insert("projectionOwner".into(), json!("rust-kernel"));
|
map.insert("projectionOwner".into(), json!("rust-kernel"));
|
||||||
if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) {
|
if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) {
|
||||||
@@ -7483,9 +7502,7 @@ fn execute_query_result(
|
|||||||
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
|
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
|
||||||
let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?;
|
let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?;
|
||||||
serde_json::to_value(result).map_err(|error| {
|
serde_json::to_value(result).map_err(|error| {
|
||||||
BridgeError::transport(format!(
|
BridgeError::transport(format!("mindmap.projection.get result 序列化失败: {error}"))
|
||||||
"mindmap.projection.get result 序列化失败: {error}"
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"search.documents" | "search.documents.query" => {
|
"search.documents" | "search.documents.query" => {
|
||||||
@@ -8167,14 +8184,10 @@ fn execute_command(
|
|||||||
"documentId": payload.document_id,
|
"documentId": payload.document_id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let page_body_event_payload = document_save_page_body_domain_event_payload(
|
let page_body_event_payload =
|
||||||
&payload,
|
document_save_page_body_domain_event_payload(&payload, &editor_document);
|
||||||
&editor_document,
|
let snapshot_event_payload =
|
||||||
);
|
document_save_snapshot_domain_event_payload(&payload, &canonical_content)?;
|
||||||
let snapshot_event_payload = document_save_snapshot_domain_event_payload(
|
|
||||||
&payload,
|
|
||||||
&canonical_content,
|
|
||||||
)?;
|
|
||||||
let page_body_event_plan = tree_domain_event_plan_with_payload(
|
let page_body_event_plan = tree_domain_event_plan_with_payload(
|
||||||
"page.body.saved",
|
"page.body.saved",
|
||||||
page_body_event_payload,
|
page_body_event_payload,
|
||||||
@@ -8207,7 +8220,8 @@ fn execute_command(
|
|||||||
} else {
|
} else {
|
||||||
"documents.embed"
|
"documents.embed"
|
||||||
};
|
};
|
||||||
let page_aggregate_embed_plan = build_page_aggregate_embed_plan(&command_wire, &payload)?;
|
let page_aggregate_embed_plan =
|
||||||
|
build_page_aggregate_embed_plan(&command_wire, &payload)?;
|
||||||
let embed_content = page_aggregate_embed_plan
|
let embed_content = page_aggregate_embed_plan
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|plan| plan.get("content"))
|
.and_then(|plan| plan.get("content"))
|
||||||
@@ -9575,47 +9589,47 @@ mod tests {
|
|||||||
"id": "block_1",
|
"id": "block_1",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
},
|
},
|
||||||
"streamDeltaHint": {
|
"streamDeltaHint": {
|
||||||
"family": "tree",
|
"family": "tree",
|
||||||
"kind": "resync_required",
|
"kind": "resync_required",
|
||||||
"args": {
|
"args": {
|
||||||
"reason": "blocks.patch",
|
"reason": "blocks.patch",
|
||||||
"documentId": "doc_1",
|
"documentId": "doc_1",
|
||||||
"blockId": "block_1"
|
"blockId": "block_1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domainEventHint": {
|
"domainEventHint": {
|
||||||
"family": "tree",
|
"family": "tree",
|
||||||
"eventType": "block.patched"
|
"eventType": "block.patched"
|
||||||
},
|
},
|
||||||
"domainEventPlan": {
|
"domainEventPlan": {
|
||||||
"family": "tree",
|
"family": "tree",
|
||||||
"schema": "mnote.tree.domain_event",
|
"schema": "mnote.tree.domain_event",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"eventType": "block.patched",
|
"eventType": "block.patched",
|
||||||
"payload": {
|
"payload": {
|
||||||
"document": {
|
"document": {
|
||||||
"id": "doc_1",
|
"id": "doc_1",
|
||||||
"workspaceId": "ws_1"
|
"workspaceId": "ws_1"
|
||||||
},
|
},
|
||||||
"block": {
|
"block": {
|
||||||
"id": "block_1"
|
"id": "block_1"
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"summary": "replace_block",
|
"summary": "replace_block",
|
||||||
"nextType": "paragraph"
|
"nextType": "paragraph"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"streamDeltaHint": {
|
"streamDeltaHint": {
|
||||||
"family": "tree",
|
"family": "tree",
|
||||||
"kind": "resync_required",
|
"kind": "resync_required",
|
||||||
"args": {
|
"args": {
|
||||||
"reason": "blocks.patch",
|
"reason": "blocks.patch",
|
||||||
"documentId": "doc_1",
|
"documentId": "doc_1",
|
||||||
"blockId": "block_1"
|
"blockId": "block_1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9775,7 +9789,10 @@ mod tests {
|
|||||||
.expect("canonical search query result should build");
|
.expect("canonical search query result should build");
|
||||||
|
|
||||||
assert_eq!(result["projectionOwner"], json!("rust-kernel"));
|
assert_eq!(result["projectionOwner"], json!("rust-kernel"));
|
||||||
assert_eq!(result["results"][0]["projectionOwner"], json!("rust-kernel"));
|
assert_eq!(
|
||||||
|
result["results"][0]["projectionOwner"],
|
||||||
|
json!("rust-kernel")
|
||||||
|
);
|
||||||
assert_eq!(result["results"][0]["id"], json!("page_1"));
|
assert_eq!(result["results"][0]["id"], json!("page_1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9848,7 +9865,10 @@ mod tests {
|
|||||||
RuntimeExecutionPlan::Command(plan) => {
|
RuntimeExecutionPlan::Command(plan) => {
|
||||||
assert_eq!(plan.command_name, "mindmap.command.apply");
|
assert_eq!(plan.command_name, "mindmap.command.apply");
|
||||||
assert_eq!(plan.function_name, "mindmaps:applyCommand");
|
assert_eq!(plan.function_name, "mindmaps:applyCommand");
|
||||||
assert_eq!(plan.args_json["canonicalCommand"], json!("mindmap.command.apply"));
|
assert_eq!(
|
||||||
|
plan.args_json["canonicalCommand"],
|
||||||
|
json!("mindmap.command.apply")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => {
|
RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => {
|
||||||
panic!("expected command plan")
|
panic!("expected command plan")
|
||||||
@@ -11514,11 +11534,11 @@ mod tests {
|
|||||||
"content": [{ "id": "block_1", "type": "pageReference" }],
|
"content": [{ "id": "block_1", "type": "pageReference" }],
|
||||||
"expectedRevision": 5,
|
"expectedRevision": 5,
|
||||||
"conflictDetectionKey": "conflict_5",
|
"conflictDetectionKey": "conflict_5",
|
||||||
"sourceDocumentId": "doc_1",
|
"sourceDocumentId": "doc_1",
|
||||||
"targetDocumentId": "doc_2",
|
"targetDocumentId": "doc_2",
|
||||||
"anchorBlockId": "anchor_1",
|
"anchorBlockId": "anchor_1",
|
||||||
"pageAggregateEmbedPlan": null,
|
"pageAggregateEmbedPlan": null,
|
||||||
"streamDeltaHint": {
|
"streamDeltaHint": {
|
||||||
"family": "tree",
|
"family": "tree",
|
||||||
"kind": "noop",
|
"kind": "noop",
|
||||||
"args": {}
|
"args": {}
|
||||||
@@ -12009,6 +12029,31 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_move_order_plan_accepts_convex_float_sort_order_snapshot() {
|
||||||
|
let payload = DocumentMoveCommandPayload {
|
||||||
|
document_id: "doc_b".into(),
|
||||||
|
parent_id: None,
|
||||||
|
sort_order: 0,
|
||||||
|
};
|
||||||
|
let snapshot = serde_json::from_value::<DocumentMoveSnapshotPayload>(json!({
|
||||||
|
"documents": [
|
||||||
|
{ "id": "doc_a", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0.0, "created_at": "2026-04-25T00:00:01Z" },
|
||||||
|
{ "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 1.0, "created_at": "2026-04-25T00:00:02Z" }
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
.expect("snapshot with float sort_order");
|
||||||
|
|
||||||
|
let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot)
|
||||||
|
.expect("move order plan");
|
||||||
|
|
||||||
|
assert_eq!(plan.normalized_sort_order, 0);
|
||||||
|
assert!(plan
|
||||||
|
.patches
|
||||||
|
.iter()
|
||||||
|
.any(|patch| patch.document_id == "doc_b" && patch.sort_order == 0 && patch.moved));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn document_move_order_plan_normalizes_cross_parent_and_clamps_index() {
|
fn document_move_order_plan_normalizes_cross_parent_and_clamps_index() {
|
||||||
let payload = DocumentMoveCommandPayload {
|
let payload = DocumentMoveCommandPayload {
|
||||||
@@ -12598,33 +12643,33 @@ mod tests {
|
|||||||
page_id: Some("doc_2".into()),
|
page_id: Some("doc_2".into()),
|
||||||
block_id: None,
|
block_id: None,
|
||||||
}),
|
}),
|
||||||
payload: json!({
|
payload: json!({
|
||||||
"documentId": "doc_2",
|
"documentId": "doc_2",
|
||||||
"workspaceId": "ws_1",
|
"workspaceId": "ws_1",
|
||||||
"revision": 5,
|
"revision": 5,
|
||||||
"conflictDetectionKey": "conflict_5",
|
"conflictDetectionKey": "conflict_5",
|
||||||
"sourceDocumentId": "doc_1",
|
"sourceDocumentId": "doc_1",
|
||||||
"targetDocumentId": "doc_2",
|
"targetDocumentId": "doc_2",
|
||||||
"anchorBlockId": "anchor_1",
|
"anchorBlockId": "anchor_1",
|
||||||
}),
|
}),
|
||||||
preflight_data: Some(json!({
|
preflight_data: Some(json!({
|
||||||
"pageAggregateEmbed": {
|
"pageAggregateEmbed": {
|
||||||
"sourceDocumentId": "doc_1",
|
"sourceDocumentId": "doc_1",
|
||||||
"sourceTitle": "来源页面",
|
"sourceTitle": "来源页面",
|
||||||
"targetDocumentId": "doc_2",
|
"targetDocumentId": "doc_2",
|
||||||
"targetContent": {
|
"targetContent": {
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{ "id": "anchor_1", "type": "paragraph" }
|
{ "id": "anchor_1", "type": "paragraph" }
|
||||||
],
|
],
|
||||||
"format": "editor"
|
"format": "editor"
|
||||||
},
|
},
|
||||||
"anchorBlockId": "anchor_1",
|
"anchorBlockId": "anchor_1",
|
||||||
"blockId": "page_ref_doc_1"
|
"blockId": "page_ref_doc_1"
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
reason: Some("树命令嵌入页面".into()),
|
reason: Some("树命令嵌入页面".into()),
|
||||||
refs: vec![],
|
refs: vec![],
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
validate_only: false,
|
validate_only: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -12636,59 +12681,59 @@ mod tests {
|
|||||||
assert_eq!(plan.command_name, "tree.node.embed");
|
assert_eq!(plan.command_name, "tree.node.embed");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plan.args_json,
|
plan.args_json,
|
||||||
json!({
|
json!({
|
||||||
"id": "doc_2",
|
"id": "doc_2",
|
||||||
"content": {
|
"content": {
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{ "id": "anchor_1", "type": "paragraph" },
|
{ "id": "anchor_1", "type": "paragraph" },
|
||||||
{
|
{
|
||||||
"id": "page_ref_doc_1",
|
"id": "page_ref_doc_1",
|
||||||
"type": "pageReference",
|
"type": "pageReference",
|
||||||
"props": {
|
"props": {
|
||||||
"pageId": "doc_1",
|
"pageId": "doc_1",
|
||||||
"title": "来源页面"
|
"title": "来源页面"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "editor"
|
"format": "editor"
|
||||||
},
|
},
|
||||||
"expectedRevision": 5,
|
"expectedRevision": 5,
|
||||||
"conflictDetectionKey": "conflict_5",
|
"conflictDetectionKey": "conflict_5",
|
||||||
"sourceDocumentId": "doc_1",
|
"sourceDocumentId": "doc_1",
|
||||||
"targetDocumentId": "doc_2",
|
"targetDocumentId": "doc_2",
|
||||||
"anchorBlockId": "anchor_1",
|
"anchorBlockId": "anchor_1",
|
||||||
"pageAggregateEmbedPlan": {
|
"pageAggregateEmbedPlan": {
|
||||||
"schema": "mnote.page_aggregate.embed_plan",
|
"schema": "mnote.page_aggregate.embed_plan",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"sourceDocumentId": "doc_1",
|
"sourceDocumentId": "doc_1",
|
||||||
"targetDocumentId": "doc_2",
|
"targetDocumentId": "doc_2",
|
||||||
"anchorBlockId": "anchor_1",
|
"anchorBlockId": "anchor_1",
|
||||||
"insertIndex": 1,
|
"insertIndex": 1,
|
||||||
"block": {
|
"block": {
|
||||||
"id": "page_ref_doc_1",
|
"id": "page_ref_doc_1",
|
||||||
"type": "pageReference",
|
"type": "pageReference",
|
||||||
"props": {
|
"props": {
|
||||||
"pageId": "doc_1",
|
"pageId": "doc_1",
|
||||||
"title": "来源页面"
|
"title": "来源页面"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{ "id": "anchor_1", "type": "paragraph" },
|
{ "id": "anchor_1", "type": "paragraph" },
|
||||||
{
|
{
|
||||||
"id": "page_ref_doc_1",
|
"id": "page_ref_doc_1",
|
||||||
"type": "pageReference",
|
"type": "pageReference",
|
||||||
"props": {
|
"props": {
|
||||||
"pageId": "doc_1",
|
"pageId": "doc_1",
|
||||||
"title": "来源页面"
|
"title": "来源页面"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "editor"
|
"format": "editor"
|
||||||
},
|
},
|
||||||
"blockCount": 2
|
"blockCount": 2
|
||||||
},
|
},
|
||||||
"streamDeltaHint": {
|
"streamDeltaHint": {
|
||||||
"family": "tree",
|
"family": "tree",
|
||||||
"kind": "noop",
|
"kind": "noop",
|
||||||
"args": {}
|
"args": {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
pub mod ai;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod ai;
|
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod governance;
|
pub mod governance;
|
||||||
pub mod kernel;
|
pub mod kernel;
|
||||||
|
|||||||
@@ -331,9 +331,7 @@ impl PageAggregateBuilder {
|
|||||||
disable_copy: self.disable_copy,
|
disable_copy: self.disable_copy,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
layout: PageLayout {
|
layout: PageLayout { page_options },
|
||||||
page_options,
|
|
||||||
},
|
|
||||||
body: PageBody {
|
body: PageBody {
|
||||||
content: self.content,
|
content: self.content,
|
||||||
revision: self.revision,
|
revision: self.revision,
|
||||||
|
|||||||
@@ -567,8 +567,7 @@ pub async fn title(
|
|||||||
let title = body.title.trim();
|
let title = body.title.trim();
|
||||||
if title.is_empty() {
|
if title.is_empty() {
|
||||||
return Err(
|
return Err(
|
||||||
WebError::bad_request_code("title_required", "缺少有效页面标题")
|
WebError::bad_request_code("title_required", "缺少有效页面标题").with_context(&context),
|
||||||
.with_context(&context),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let effective_workspace_id =
|
let effective_workspace_id =
|
||||||
@@ -974,6 +973,9 @@ mod tests {
|
|||||||
.expect("body");
|
.expect("body");
|
||||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["meta"]["commandName"], "page.layout.updateOptions");
|
assert_eq!(payload["meta"]["commandName"], "page.layout.updateOptions");
|
||||||
assert_eq!(payload["meta"]["canonicalCommand"], "page.layout.updateOptions");
|
assert_eq!(
|
||||||
|
payload["meta"]["canonicalCommand"],
|
||||||
|
"page.layout.updateOptions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,10 @@ mod tests {
|
|||||||
assert_eq!(payload["contract"]["toolEventOwner"], "rust-web-hermes");
|
assert_eq!(payload["contract"]["toolEventOwner"], "rust-web-hermes");
|
||||||
assert_eq!(payload["contract"]["clientActionOwner"], "rust-web-hermes");
|
assert_eq!(payload["contract"]["clientActionOwner"], "rust-web-hermes");
|
||||||
assert_eq!(payload["canonicalRoute"], "/api/hermes/bridge");
|
assert_eq!(payload["canonicalRoute"], "/api/hermes/bridge");
|
||||||
assert!(payload["eventStreamEndpoint"].as_str().unwrap_or_default().contains("/api/hermes/events/"));
|
assert!(payload["eventStreamEndpoint"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("/api/hermes/events/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -284,6 +287,9 @@ mod tests {
|
|||||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||||
assert_eq!(payload["bridge"], "hermes_session");
|
assert_eq!(payload["bridge"], "hermes_session");
|
||||||
assert_eq!(payload["canonicalRoute"], "/api/hermes/bridge");
|
assert_eq!(payload["canonicalRoute"], "/api/hermes/bridge");
|
||||||
assert_eq!(payload["contract"]["structuredWriteOwner"], "rust-web-hermes");
|
assert_eq!(
|
||||||
|
payload["contract"]["structuredWriteOwner"],
|
||||||
|
"rust-web-hermes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,14 @@ async fn load_search_results_with_filters(
|
|||||||
"customRangeTo": filters.custom_range.as_ref().and_then(|range| range.to.clone()),
|
"customRangeTo": filters.custom_range.as_ref().and_then(|range| range.to.clone()),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
match execute_runtime_query_via_convex(config, context, Some(workspace_id), runtime_query.clone()).await {
|
match execute_runtime_query_via_convex(
|
||||||
|
config,
|
||||||
|
context,
|
||||||
|
Some(workspace_id),
|
||||||
|
runtime_query.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
Err(_) => execute_runtime_query_against_data(
|
Err(_) => execute_runtime_query_against_data(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -161,7 +161,12 @@ fn stream_event(event_name: &str, payload: &Value) -> Event {
|
|||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.or_else(|| value.as_u64().map(|number| number.to_string()))
|
.or_else(|| value.as_u64().map(|number| number.to_string()))
|
||||||
})
|
})
|
||||||
.or_else(|| payload.get("cursor").and_then(Value::as_str).map(ToOwned::to_owned))
|
.or_else(|| {
|
||||||
|
payload
|
||||||
|
.get("cursor")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
})
|
||||||
.unwrap_or_else(|| "0".into());
|
.unwrap_or_else(|| "0".into());
|
||||||
Event::default()
|
Event::default()
|
||||||
.event(event_name)
|
.event(event_name)
|
||||||
|
|||||||
@@ -4511,6 +4511,37 @@ fn create_command_wire(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_tree_move_preflight_data(
|
||||||
|
state: &AppState,
|
||||||
|
context: &RequestContext,
|
||||||
|
workspace_id: &str,
|
||||||
|
) -> Result<Value, WebError> {
|
||||||
|
let spec = ProjectionSnapshotSpec {
|
||||||
|
workspace_id,
|
||||||
|
root_node_id: None,
|
||||||
|
depth: Some(99),
|
||||||
|
projection: KernelProjectionKind::SidebarTree,
|
||||||
|
query: None,
|
||||||
|
max_results: None,
|
||||||
|
};
|
||||||
|
let snapshot = load_projection_snapshot(state.config(), context, &spec)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
WebError::bad_gateway_code(
|
||||||
|
"tree_move_preflight_snapshot_failed",
|
||||||
|
format!("移动前排序快照加载失败: {}", error.message()),
|
||||||
|
)
|
||||||
|
.with_context(context)
|
||||||
|
.with_header("x-error-phase", "tree_move_preflight_snapshot")
|
||||||
|
})?;
|
||||||
|
let documents = snapshot
|
||||||
|
.dataset
|
||||||
|
.get("documents")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!([]));
|
||||||
|
Ok(json!({ "documents": documents }))
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve_tree_create_workspace_id(
|
async fn resolve_tree_create_workspace_id(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
context: &RequestContext,
|
context: &RequestContext,
|
||||||
@@ -4671,7 +4702,12 @@ pub async fn tree_command(
|
|||||||
_ => resolve_effective_workspace_id(&context, requested_workspace_id, true)?
|
_ => resolve_effective_workspace_id(&context, requested_workspace_id, true)?
|
||||||
.expect("workspace_required 已确保存在"),
|
.expect("workspace_required 已确保存在"),
|
||||||
};
|
};
|
||||||
let command_wire = create_command_wire(&context, &effective_workspace_id, request)?;
|
let needs_move_preflight = matches!(&request, TreeCommandRequest::Move { .. });
|
||||||
|
let mut command_wire = create_command_wire(&context, &effective_workspace_id, request)?;
|
||||||
|
if needs_move_preflight {
|
||||||
|
command_wire.preflight_data =
|
||||||
|
Some(load_tree_move_preflight_data(&state, &context, &effective_workspace_id).await?);
|
||||||
|
}
|
||||||
let execution = execute_runtime_command_via_convex_with_artifacts(
|
let execution = execute_runtime_command_via_convex_with_artifacts(
|
||||||
state.config(),
|
state.config(),
|
||||||
&context,
|
&context,
|
||||||
|
|||||||
@@ -195,12 +195,12 @@ fn render_document_title_controller_script() -> &'static str {
|
|||||||
}
|
}
|
||||||
if (!documentId) return;
|
if (!documentId) return;
|
||||||
const escapedId = cssEscape(documentId);
|
const escapedId = cssEscape(documentId);
|
||||||
setText(`[data-node-id="${escapedId}"] .tree-link-title`, title);
|
setText(`.tree-row[data-node-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
|
||||||
setText(`[data-document-id="${escapedId}"] .tree-link-title`, title);
|
setText(`.tree-row[data-document-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
|
||||||
setText(`[data-doc-id="${escapedId}"] .tree-link-title`, title);
|
setText(`.tree-row[data-doc-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
|
||||||
setText(`[data-node-id="${escapedId}"] .wolai-row-title`, title);
|
setText(`.wolai-page-row[data-node-id="${escapedId}"] > .wolai-row-title`, title);
|
||||||
setText(`a[href="/documents/${escapedId}"] .wolai-row-title`, title);
|
setText(`a[href="/documents/${escapedId}"] > .wolai-row-title`, title);
|
||||||
setText(`a[href^="/documents/${escapedId}?"] .wolai-row-title`, title);
|
setText(`a[href^="/documents/${escapedId}?"] > .wolai-row-title`, title);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveTitle = async () => {
|
const saveTitle = async () => {
|
||||||
@@ -627,8 +627,9 @@ async fn build_page_aggregate_snapshot(
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
serde_json::from_value::<PageAggregate>(projection)
|
serde_json::from_value::<PageAggregate>(projection).map_err(|error| {
|
||||||
.map_err(|error| WebError::internal(format!("Page Aggregate projection 反序列化失败: {error}")))
|
WebError::internal(format!("Page Aggregate projection 反序列化失败: {error}"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stamp_recent_page_cookie(headers: &mut HeaderMap, document_id: &str) {
|
fn stamp_recent_page_cookie(headers: &mut HeaderMap, document_id: &str) {
|
||||||
@@ -941,6 +942,9 @@ mod tests {
|
|||||||
assert!(html.contains("data-page-title-input=\"true\""));
|
assert!(html.contains("data-page-title-input=\"true\""));
|
||||||
assert!(html.contains("data-title-endpoint=\"/api/documents/title\""));
|
assert!(html.contains("data-title-endpoint=\"/api/documents/title\""));
|
||||||
assert!(html.contains("mnote.document_title_controller.v1"));
|
assert!(html.contains("mnote.document_title_controller.v1"));
|
||||||
|
assert!(html
|
||||||
|
.contains(".tree-row[data-node-id=\"${escapedId}\"] > .tree-link > .tree-link-title"));
|
||||||
|
assert!(!html.contains("[data-node-id=\"${escapedId}\"] .tree-link-title"));
|
||||||
assert!(html.contains("\"titleEndpoint\":\"/api/documents/title\""));
|
assert!(html.contains("\"titleEndpoint\":\"/api/documents/title\""));
|
||||||
assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__"));
|
assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__"));
|
||||||
assert!(html.contains("mnote.tree_live_bootstrap.v1"));
|
assert!(html.contains("mnote.tree_live_bootstrap.v1"));
|
||||||
|
|||||||
@@ -48,13 +48,9 @@ pub fn DocumentPage(
|
|||||||
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={title.clone()}>
|
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={title.clone()}>
|
||||||
<main class="document-shell" data-editor-host="leptos_tiptap_island" data-document-id={document_id.clone()} data-workspace-id={workspace_id.clone()}>
|
<main class="document-shell" data-editor-host="leptos_tiptap_island" data-document-id={document_id.clone()} data-workspace-id={workspace_id.clone()}>
|
||||||
<header class="document-shell-header">
|
<header class="document-shell-header">
|
||||||
<div class="document-page-icon" aria-hidden="true">
|
<div class="document-page-icon" aria-hidden="true">
|
||||||
<svg class="mnote-symbol mnote-symbol--document" data-icon="home" viewBox="0 0 24 24" focusable="false">
|
<span class="material-symbols-outlined material-symbols-filled mnote-material-page-icon" data-icon="home"></span>
|
||||||
<path d="M4 10.5 12 3l8 7.5"></path>
|
</div>
|
||||||
<path d="M6.5 9.5V21h11V9.5"></path>
|
|
||||||
<path d="M9.5 21v-6h5v6"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1 class="document-title-heading">
|
<h1 class="document-title-heading">
|
||||||
<textarea
|
<textarea
|
||||||
id="mnote-page-title-input"
|
id="mnote-page-title-input"
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
|
|
||||||
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
|
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
|
||||||
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
|
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
|
||||||
|
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
|
||||||
var mnoteNavigationInFlight = '';
|
var mnoteNavigationInFlight = '';
|
||||||
var draggingPageNodeId = '';
|
var draggingPageNodeId = '';
|
||||||
var activePageDropRow = null;
|
var activePageDropRow = null;
|
||||||
var draggingFileTreeRowIds = [];
|
var draggingFileTreeRowIds = [];
|
||||||
var activeFileTreeDropRow = null;
|
var activeFileTreeDropRow = null;
|
||||||
var projectionRefreshTimer = 0;
|
var projectionRefreshTimer = 0;
|
||||||
|
var activeTreeContextMenu = null;
|
||||||
|
|
||||||
function closestAction(target, selector) {
|
function closestAction(target, selector) {
|
||||||
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
||||||
@@ -39,6 +41,40 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
return match ? decodeURIComponent(match[1]) : '';
|
return match ? decodeURIComponent(match[1]) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSidebarTreeMode(value) {
|
||||||
|
var mode = String(value || '').trim();
|
||||||
|
return mode === 'filetree' ? 'filetree' : 'page';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredSidebarTreeMode() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree'));
|
||||||
|
if (params.has('treeView') || params.has('sidebarTree')) return fromUrl;
|
||||||
|
try {
|
||||||
|
var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : '';
|
||||||
|
return normalizeSidebarTreeMode(stored);
|
||||||
|
} catch (_) {
|
||||||
|
return 'page';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistSidebarTreeMode(mode) {
|
||||||
|
var normalized = normalizeSidebarTreeMode(mode);
|
||||||
|
document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized);
|
||||||
|
try {
|
||||||
|
if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized);
|
||||||
|
} catch (_) {}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeSidebarTreeMode() {
|
||||||
|
var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
|
||||||
|
if (active instanceof HTMLElement) {
|
||||||
|
return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab'));
|
||||||
|
}
|
||||||
|
return readStoredSidebarTreeMode();
|
||||||
|
}
|
||||||
|
|
||||||
function resolveWorkspaceId(trigger) {
|
function resolveWorkspaceId(trigger) {
|
||||||
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
|
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
@@ -82,9 +118,29 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToDocument(nodeId, workspaceId) {
|
function navigateToDocument(nodeId, workspaceId, options) {
|
||||||
if (!nodeId) return;
|
if (!nodeId) return;
|
||||||
var url = '/documents/' + encodeURIComponent(nodeId) + (workspaceId ? '?workspaceId=' + encodeURIComponent(workspaceId) : '');
|
var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode());
|
||||||
|
persistSidebarTreeMode(treeView);
|
||||||
|
if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) {
|
||||||
|
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) {
|
||||||
|
if (row instanceof HTMLElement) {
|
||||||
|
row.setAttribute('data-active', 'false');
|
||||||
|
row.setAttribute('data-selected', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
|
||||||
|
if (row instanceof HTMLElement) {
|
||||||
|
if (row.getAttribute('data-shell-mode') === 'filetree') row.setAttribute('data-selected', 'true');
|
||||||
|
else row.setAttribute('data-active', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin);
|
||||||
|
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
|
||||||
|
if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree');
|
||||||
|
var url = targetUrl.pathname + targetUrl.search;
|
||||||
if (mnoteNavigationInFlight === url) return;
|
if (mnoteNavigationInFlight === url) return;
|
||||||
mnoteNavigationInFlight = url;
|
mnoteNavigationInFlight = url;
|
||||||
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
||||||
@@ -106,12 +162,12 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
title: '新页面'
|
title: '新页面'
|
||||||
});
|
});
|
||||||
var nextWorkspaceId = result.workspaceId || workspaceId;
|
var nextWorkspaceId = result.workspaceId || workspaceId;
|
||||||
navigateToDocument(result.documentId, nextWorkspaceId);
|
navigateToDocument(result.documentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchSidebarTreeTab(trigger) {
|
function applySidebarTreeTab(mode, shell) {
|
||||||
var mode = (trigger.getAttribute('data-mnote-sidebar-tree-tab') || 'page').trim();
|
mode = persistSidebarTreeMode(mode);
|
||||||
var shell = trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]');
|
shell = shell || document.querySelector('[data-testid="wolai-sidebar-page-tree-shell"]');
|
||||||
if (!shell) return;
|
if (!shell) return;
|
||||||
var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]');
|
var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]');
|
||||||
for (var i = 0; i < tabs.length; i++) {
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
@@ -127,13 +183,25 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchSidebarTreeTab(trigger) {
|
||||||
|
var mode = trigger ? trigger.getAttribute('data-mnote-sidebar-tree-tab') : 'page';
|
||||||
|
applySidebarTreeTab(mode, trigger ? trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]') : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSidebarTreeTab() {
|
||||||
|
applySidebarTreeTab(readStoredSidebarTreeMode(), null);
|
||||||
|
}
|
||||||
|
|
||||||
function updateTitleEverywhere(documentId, title) {
|
function updateTitleEverywhere(documentId, title) {
|
||||||
if (!documentId) return;
|
if (!documentId) return;
|
||||||
|
var escaped = cssEscape(documentId);
|
||||||
var selectors = [
|
var selectors = [
|
||||||
'[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
'.tree-row[data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
||||||
'[data-document-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
'.tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
||||||
'[data-doc-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
'.tree-row[data-doc-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
||||||
'[data-node-id="' + cssEscape(documentId) + '"] .wolai-row-title'
|
'.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
|
||||||
|
'a[href="/documents/' + escaped + '"] > .wolai-row-title',
|
||||||
|
'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
|
||||||
];
|
];
|
||||||
selectors.forEach(function(selector) {
|
selectors.forEach(function(selector) {
|
||||||
document.querySelectorAll(selector).forEach(function(node) {
|
document.querySelectorAll(selector).forEach(function(node) {
|
||||||
@@ -313,7 +381,267 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowTitle(row) {
|
||||||
|
var title = row ? row.querySelector(':scope > .tree-link > .tree-link-title') : null;
|
||||||
|
return title && title.textContent ? title.textContent.trim() : '无标题';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowCenter(row) {
|
||||||
|
var rect = row.getBoundingClientRect();
|
||||||
|
return { x: rect.left + Math.min(rect.width - 12, 180), y: rect.top + Math.min(rect.height, 22) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTreeContextMenu() {
|
||||||
|
if (activeTreeContextMenu && activeTreeContextMenu.parentElement) {
|
||||||
|
activeTreeContextMenu.parentElement.removeChild(activeTreeContextMenu);
|
||||||
|
}
|
||||||
|
activeTreeContextMenu = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyTreeContextValue(value, actionName) {
|
||||||
|
var text = String(value || '');
|
||||||
|
var done = function() {
|
||||||
|
document.documentElement.setAttribute('data-mnote-tree-context-last-copy', actionName || 'copy');
|
||||||
|
};
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
return navigator.clipboard.writeText(text).then(done).catch(function(){});
|
||||||
|
}
|
||||||
|
var textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute('readonly', 'readonly');
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
try { document.execCommand('copy'); } catch (_) {}
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
done();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentHref(documentId, workspaceId) {
|
||||||
|
var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
|
||||||
|
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTreePicker(mode, detail) {
|
||||||
|
var url = new URL('/tree', window.location.origin);
|
||||||
|
url.searchParams.set('mode', 'picker');
|
||||||
|
url.searchParams.set('allowRootPick', '1');
|
||||||
|
url.searchParams.set('intent', mode);
|
||||||
|
if (detail.workspaceId) url.searchParams.set('workspaceId', detail.workspaceId);
|
||||||
|
if (detail.documentId) {
|
||||||
|
url.searchParams.set('sourceDocumentId', detail.documentId);
|
||||||
|
url.searchParams.set('excludeIds', detail.documentId);
|
||||||
|
}
|
||||||
|
window.location.assign(url.pathname + url.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToPreviousSiblingChild(trigger, detail) {
|
||||||
|
var documentId = detail.documentId || '';
|
||||||
|
var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]');
|
||||||
|
if (!(row instanceof HTMLElement)) return;
|
||||||
|
var parentId = row.getAttribute('data-parent-id') || '';
|
||||||
|
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(candidate) {
|
||||||
|
return (candidate.getAttribute('data-parent-id') || '') === parentId;
|
||||||
|
});
|
||||||
|
var index = siblings.indexOf(row);
|
||||||
|
if (index <= 0) {
|
||||||
|
window.alert('当前页面前面没有同级页面。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var previous = siblings[index - 1];
|
||||||
|
var previousId = previous.getAttribute('data-node-id') || '';
|
||||||
|
var children = previous.parentElement ? previous.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
|
||||||
|
void dispatchTreeCommand(trigger || row, {
|
||||||
|
action: 'move',
|
||||||
|
workspaceId: detail.workspaceId || resolveWorkspaceId(row),
|
||||||
|
documentId: documentId,
|
||||||
|
parentId: previousId,
|
||||||
|
sortOrder: children.length
|
||||||
|
}).then(function(){ scheduleProjectionRefresh(detail.workspaceId || resolveWorkspaceId(row)); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTreeContextMenuAction(action, detail, trigger) {
|
||||||
|
closeTreeContextMenu();
|
||||||
|
var documentId = detail.documentId || '';
|
||||||
|
var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body);
|
||||||
|
var title = detail.title || '无标题';
|
||||||
|
if (action === 'open-right') {
|
||||||
|
dispatchSidebarEvent('tree.page.open-right', detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'share') {
|
||||||
|
dispatchSidebarEvent('tree.page.share', detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'move') {
|
||||||
|
openTreePicker('move', detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'embed') {
|
||||||
|
openTreePicker('embed', detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'copy-link') {
|
||||||
|
void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'copy-link-title') {
|
||||||
|
void copyTreeContextValue(title + ' ' + documentHref(documentId, workspaceId), 'copy-link-title');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'copy-reference-inline') {
|
||||||
|
void copyTreeContextValue('((' + title + ' ' + documentId + '))', 'copy-reference-inline');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'copy-reference-embed') {
|
||||||
|
void copyTreeContextValue('{{' + title + ' ' + documentId + '}}', 'copy-reference-embed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'copy-id') {
|
||||||
|
void copyTreeContextValue(documentId || detail.assetId || detail.rowId || '', 'copy-id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'duplicate') {
|
||||||
|
dispatchSidebarEvent('tree.page.duplicate', detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'rename') {
|
||||||
|
var nextTitle = window.prompt('重命名页面', title);
|
||||||
|
if (nextTitle && nextTitle.trim() && documentId) {
|
||||||
|
void dispatchTreeCommand(trigger || document.body, {
|
||||||
|
action: 'rename',
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
documentId: documentId,
|
||||||
|
title: nextTitle.trim()
|
||||||
|
}).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'create-child') {
|
||||||
|
void createPage(trigger || document.body, documentId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'convert-child') {
|
||||||
|
convertToPreviousSiblingChild(trigger, detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'delete-trash' && documentId) {
|
||||||
|
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
|
||||||
|
void dispatchTreeCommand(trigger || document.body, {
|
||||||
|
action: 'purge',
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
documentId: documentId
|
||||||
|
}).then(function(){ scheduleProjectionRefresh(workspaceId); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendTreeContextMenuButton(menu, item, detail, trigger) {
|
||||||
|
if (item.separator) {
|
||||||
|
var sep = document.createElement('div');
|
||||||
|
sep.className = 'mnote-tree-context-menu__separator';
|
||||||
|
sep.setAttribute('role', 'separator');
|
||||||
|
menu.appendChild(sep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
|
||||||
|
button.setAttribute('role', 'menuitem');
|
||||||
|
button.setAttribute('data-action', item.action);
|
||||||
|
button.disabled = item.disabled === true;
|
||||||
|
var icon = document.createElement('span');
|
||||||
|
icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
|
||||||
|
icon.setAttribute('aria-hidden', 'true');
|
||||||
|
icon.setAttribute('data-icon', item.icon || 'radio_button_unchecked');
|
||||||
|
var label = document.createElement('span');
|
||||||
|
label.className = 'mnote-tree-context-menu__label';
|
||||||
|
label.textContent = item.label;
|
||||||
|
button.appendChild(icon);
|
||||||
|
button.appendChild(label);
|
||||||
|
if (item.shortcut) {
|
||||||
|
var shortcut = document.createElement('span');
|
||||||
|
shortcut.className = 'mnote-tree-context-menu__shortcut';
|
||||||
|
shortcut.textContent = item.shortcut;
|
||||||
|
button.appendChild(shortcut);
|
||||||
|
}
|
||||||
|
button.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleTreeContextMenuAction(item.action, detail, trigger);
|
||||||
|
});
|
||||||
|
menu.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTreeContextMenu(kind, detail, x, y, trigger) {
|
||||||
|
closeTreeContextMenu();
|
||||||
|
var menu = document.createElement('div');
|
||||||
|
menu.className = 'mnote-tree-context-menu';
|
||||||
|
menu.setAttribute('role', 'menu');
|
||||||
|
menu.setAttribute('data-testid', 'mnote-tree-context-menu');
|
||||||
|
menu.setAttribute('data-kind', kind);
|
||||||
|
var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
|
||||||
|
var items = isAsset ? [
|
||||||
|
{ action: 'open-right', icon: 'open_in_new', label: '在右侧边栏打开', shortcut: 'Alt + O' },
|
||||||
|
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||||
|
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' }
|
||||||
|
] : [
|
||||||
|
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt + O' },
|
||||||
|
{ action: 'share', icon: 'share', label: '共享...' },
|
||||||
|
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||||
|
{ action: 'embed', icon: 'account_tree', label: '嵌入到...' },
|
||||||
|
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
|
||||||
|
{ action: 'copy-link-title', icon: 'link', label: '复制访问链接(带标题)' },
|
||||||
|
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
|
||||||
|
{ action: 'copy-id', icon: 'tag', label: '复制页面 ID' },
|
||||||
|
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
|
||||||
|
{ separator: true },
|
||||||
|
{ action: 'rename', icon: 'edit', label: '重命名' },
|
||||||
|
{ action: 'create-child', icon: 'add', label: '新建子页面' },
|
||||||
|
{ action: 'convert-child', icon: 'subdirectory_arrow_right', label: '转为上一个子页面' },
|
||||||
|
{ action: 'delete-trash', icon: 'delete', label: '删除到垃圾桶', danger: true }
|
||||||
|
];
|
||||||
|
items.forEach(function(item) { appendTreeContextMenuButton(menu, item, detail, trigger); });
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
var rect = menu.getBoundingClientRect();
|
||||||
|
var left = Math.min(Math.max(8, x || 8), Math.max(8, window.innerWidth - rect.width - 8));
|
||||||
|
var top = Math.min(Math.max(8, y || 8), Math.max(8, window.innerHeight - rect.height - 8));
|
||||||
|
menu.style.left = left + 'px';
|
||||||
|
menu.style.top = top + 'px';
|
||||||
|
activeTreeContextMenu = menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPageTreeContextMenu(row, x, y, trigger) {
|
||||||
|
if (!(row instanceof HTMLElement)) return;
|
||||||
|
var documentId = row.getAttribute('data-node-id') || '';
|
||||||
|
openTreeContextMenu('page', {
|
||||||
|
documentId: documentId,
|
||||||
|
rowId: documentId,
|
||||||
|
rowKind: 'document',
|
||||||
|
title: rowTitle(row),
|
||||||
|
workspaceId: resolveWorkspaceId(row)
|
||||||
|
}, x, y, trigger || row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFileTreeContextMenu(row, x, y, trigger) {
|
||||||
|
if (!(row instanceof HTMLElement)) return;
|
||||||
|
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
|
||||||
|
openTreeContextMenu('filetree', {
|
||||||
|
documentId: documentId,
|
||||||
|
rowId: row.getAttribute('data-row-id') || '',
|
||||||
|
rowKind: row.getAttribute('data-row-kind') || '',
|
||||||
|
assetId: row.getAttribute('data-asset-id') || '',
|
||||||
|
title: rowTitle(row),
|
||||||
|
workspaceId: resolveWorkspaceId(row)
|
||||||
|
}, x, y, trigger || row);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
|
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
||||||
|
if (activeTreeContextMenu) closeTreeContextMenu();
|
||||||
|
|
||||||
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
||||||
if (tabTrigger) {
|
if (tabTrigger) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -350,7 +678,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
if (fileAction === 'menu') {
|
if (fileAction === 'menu') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
var point = rowCenter(fileBtn || fileRow);
|
||||||
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
||||||
|
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -360,7 +690,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
fileRow.setAttribute('data-selected', 'true');
|
fileRow.setAttribute('data-selected', 'true');
|
||||||
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
||||||
if ((rowKind === 'document' || rowKind === 'index') && documentId) {
|
if ((rowKind === 'document' || rowKind === 'index') && documentId) {
|
||||||
navigateToDocument(documentId, resolveWorkspaceId(fileRow));
|
navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree' });
|
||||||
|
} else if (assetId) {
|
||||||
|
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -379,7 +711,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (action === 'open') {
|
} else if (action === 'open') {
|
||||||
var workspaceId = resolveWorkspaceId(btn);
|
var workspaceId = resolveWorkspaceId(btn);
|
||||||
navigateToDocument(nodeId, workspaceId);
|
navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (action === 'create') {
|
} else if (action === 'create') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -400,10 +732,30 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
} else if (action === 'menu') {
|
} else if (action === 'menu') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
var menuPoint = rowCenter(btn);
|
||||||
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
|
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
|
||||||
|
openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('contextmenu', function(event) {
|
||||||
|
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
||||||
|
if (fileRow) {
|
||||||
|
event.preventDefault();
|
||||||
|
openFileTreeContextMenu(fileRow, event.clientX, event.clientY, fileRow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
|
||||||
|
if (pageRow) {
|
||||||
|
event.preventDefault();
|
||||||
|
openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Escape') closeTreeContextMenu();
|
||||||
|
});
|
||||||
|
|
||||||
function readPageDragNodeId(event) {
|
function readPageDragNodeId(event) {
|
||||||
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
|
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
|
||||||
return (fromTransfer || draggingPageNodeId || '').trim();
|
return (fromTransfer || draggingPageNodeId || '').trim();
|
||||||
@@ -557,7 +909,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
window.addEventListener('tree:title-updated', function(event) {
|
window.addEventListener('tree:title-updated', function(event) {
|
||||||
var detail = event.detail || {};
|
var detail = event.detail || {};
|
||||||
updateTitleEverywhere(detail.documentId, detail.title);
|
updateTitleEverywhere(detail.documentId, detail.title);
|
||||||
scheduleProjectionRefresh(detail.workspaceId);
|
document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('tree:snapshot', function(event) {
|
window.addEventListener('tree:snapshot', function(event) {
|
||||||
@@ -593,6 +945,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
|
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
|
||||||
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
|
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
|
||||||
}
|
}
|
||||||
|
restoreSidebarTreeTab();
|
||||||
})();
|
})();
|
||||||
"##;
|
"##;
|
||||||
|
|
||||||
@@ -793,12 +1146,12 @@ pub fn PageLayout(
|
|||||||
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
||||||
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><svg class="mnote-symbol nav-icon" data-icon="search" viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="5.5"></circle><path d="m15 15 5 5"></path></svg></a>
|
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
|
||||||
<a href="/graph" title="关系图" aria-label="关系图"><svg class="mnote-symbol nav-icon" data-icon="account_tree" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6h5v5H6z"></path><path d="M15 3h5v5h-5z"></path><path d="M15 16h5v5h-5z"></path><path d="M11 8.5h2.5a4 4 0 0 0 4-4"></path><path d="M11 8.5h2.5a4 4 0 0 1 4 4V16"></path></svg></a>
|
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
|
||||||
<a href="/actions" title="快捷动作" aria-label="快捷动作"><svg class="mnote-symbol nav-icon" data-icon="bolt" viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 5 14h6l-1 8 9-13h-6z"></path></svg></a>
|
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
|
||||||
<a href="/help" title="帮助" aria-label="帮助"><svg class="mnote-symbol nav-icon" data-icon="help" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.8 9a2.4 2.4 0 0 1 4.5 1.2c0 1.7-2.3 2-2.3 3.8"></path><path d="M12 17.5h.01"></path></svg></a>
|
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
|
||||||
<a href="/files" title="文件" aria-label="文件"><svg class="mnote-symbol nav-icon" data-icon="inventory_2" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v4H4z"></path><path d="M6 10v9h12v-9"></path><path d="M9 14h6"></path></svg></a>
|
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
|
||||||
<a href="/more" title="更多" aria-label="更多"><svg class="mnote-symbol nav-icon" data-icon="more_horiz" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h.01"></path><path d="M12 12h.01"></path><path d="M19 12h.01"></path></svg></a>
|
<a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
||||||
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
||||||
@@ -808,28 +1161,28 @@ pub fn PageLayout(
|
|||||||
<div class="mnote-main">
|
<div class="mnote-main">
|
||||||
<header class="wolai-topbar" data-testid="wolai-topbar">
|
<header class="wolai-topbar" data-testid="wolai-topbar">
|
||||||
<div class="wolai-topbar-left">
|
<div class="wolai-topbar-left">
|
||||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏">"☰"</button>
|
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
||||||
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
||||||
<span class="wolai-breadcrumb-root">"The Digital Atelier"</span>
|
<span class="wolai-breadcrumb-root">"The Digital Atelier"</span>
|
||||||
<span class="wolai-breadcrumb-separator" aria-hidden="true">"/"</span>
|
<span class="wolai-breadcrumb-separator" aria-hidden="true">"/"</span>
|
||||||
<span class="wolai-breadcrumb-current"><svg class="mnote-symbol wolai-home-icon" data-icon="home" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 10.5 12 3l8 7.5"></path><path d="M6.5 9.5V21h11V9.5"></path><path d="M9.5 21v-6h5v6"></path></svg><span data-page-title-current="true">{topbar_title}</span></span>
|
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="wolai-topbar-actions" aria-label="页面操作">
|
<div class="wolai-topbar-actions" aria-label="页面操作">
|
||||||
<span class="wolai-public-pill"><span aria-hidden="true">"●"</span>"Public"</span>
|
<span class="wolai-public-pill"><span aria-hidden="true">"●"</span>"Public"</span>
|
||||||
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏">"☆"</button>
|
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
||||||
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史">"◷"</button>
|
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
|
||||||
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI">"✦"</button>
|
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||||
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索">"⌕"</button>
|
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
|
||||||
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多">"…"</button>
|
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<article class="mnote-content">
|
<article class="mnote-content">
|
||||||
{children()}
|
{children()}
|
||||||
</article>
|
</article>
|
||||||
<div class="wolai-floating-actions" aria-label="浮动操作">
|
<div class="wolai-floating-actions" aria-label="浮动操作">
|
||||||
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助">"?"</button>
|
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
|
||||||
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手">"✦"</button>
|
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -844,6 +1197,9 @@ mod tests {
|
|||||||
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
|
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
|
||||||
assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
|
assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
|
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("treeView"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
|
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
|
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("drop"));
|
assert!(SIDEBAR_TREE_JS.contains("drop"));
|
||||||
@@ -851,10 +1207,24 @@ mod tests {
|
|||||||
assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
|
assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
|
assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("tree.asset.open"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("mnote-tree-context-menu"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("复制访问链接(带标题)"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("删除到垃圾桶"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains(
|
||||||
|
".tree-row[data-node-id=\"' + escaped + '\"] > .tree-link > .tree-link-title"
|
||||||
|
));
|
||||||
|
assert!(!SIDEBAR_TREE_JS
|
||||||
|
.contains("[data-node-id=\"' + cssEscape(documentId) + '\"] .tree-link-title"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
||||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
||||||
|
|||||||
@@ -93,6 +93,100 @@ a:hover {
|
|||||||
stroke-width: 1.35;
|
stroke-width: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-family: var(--wolai-font-sans);
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-feature-settings: "liga";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
background: currentColor;
|
||||||
|
-webkit-mask: var(--mnote-icon-mask) center / contain no-repeat;
|
||||||
|
mask: var(--mnote-icon-mask) center / contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined[data-icon="search"]::before { content: "⌕"; }
|
||||||
|
.material-symbols-outlined[data-icon="account_tree"]::before { content: "⌘"; }
|
||||||
|
.material-symbols-outlined[data-icon="bolt"]::before { content: "ϟ"; }
|
||||||
|
.material-symbols-outlined[data-icon="help"]::before { content: "?"; }
|
||||||
|
.material-symbols-outlined[data-icon="inventory_2"]::before { content: "▣"; }
|
||||||
|
.material-symbols-outlined[data-icon="more_horiz"]::before { content: "…"; }
|
||||||
|
.material-symbols-outlined[data-icon="menu"]::before { content: "☰"; }
|
||||||
|
.material-symbols-outlined[data-icon="home"]::before { content: "⌂"; }
|
||||||
|
.material-symbols-outlined[data-icon="star"]::before { content: "☆"; }
|
||||||
|
.material-symbols-outlined[data-icon="history"]::before { content: "◷"; }
|
||||||
|
.material-symbols-outlined[data-icon="auto_awesome"]::before { content: "✦"; }
|
||||||
|
.material-symbols-outlined[data-icon="folder_open"]::before,
|
||||||
|
.material-symbols-outlined[data-icon="drive_file_move"]::before { content: "▱"; }
|
||||||
|
.material-symbols-outlined[data-icon="delete"]::before { content: "⌫"; }
|
||||||
|
.material-symbols-outlined[data-icon="right_panel_open"]::before,
|
||||||
|
.material-symbols-outlined[data-icon="open_in_new"]::before { content: "↗"; }
|
||||||
|
.material-symbols-outlined[data-icon="share"]::before { content: "⇪"; }
|
||||||
|
.material-symbols-outlined[data-icon="link"]::before { content: "∞"; }
|
||||||
|
.material-symbols-outlined[data-icon="content_copy"]::before,
|
||||||
|
.material-symbols-outlined[data-icon="file_copy"]::before { content: "⧉"; }
|
||||||
|
.material-symbols-outlined[data-icon="tag"]::before { content: "#"; }
|
||||||
|
.material-symbols-outlined[data-icon="edit"]::before { content: "✎"; }
|
||||||
|
.material-symbols-outlined[data-icon="add"]::before { content: "+"; }
|
||||||
|
.material-symbols-outlined[data-icon="subdirectory_arrow_right"]::before { content: "↳"; }
|
||||||
|
|
||||||
|
.material-symbols-filled {
|
||||||
|
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 本地 SVG mask 图标,避免 Google Material Symbols 字体未加载时露出英文图标名。 */
|
||||||
|
.material-symbols-outlined[data-icon]::before { content: ""; }
|
||||||
|
.material-symbols-outlined[data-icon="search"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.8 18a7.2 7.2 0 1 1 0-14.4 7.2 7.2 0 0 1 0 14.4Z' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='m16 16 4.2 4.2' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="account_tree"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 6h4v4H6zM14 4h4v4h-4zM14 16h4v4h-4z' fill='none' stroke='black' stroke-width='1.8'/%3E%3Cpath d='M10 8h2a2 2 0 0 0 2-2M10 8h2a2 2 0 0 1 2 2v8' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="bolt"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13 2 4.5 13h6L9 22l10.5-13h-6z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="help"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M9.8 9a2.4 2.4 0 0 1 4.6 1.1c0 1.7-1.7 2-2.2 3.1' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3Ccircle cx='12' cy='17' r='1.1' fill='black'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="inventory_2"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 5.5h16v4H4zM6 9.5h12V19H6z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='M9.5 13.5h5' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="more_horiz"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='5' cy='12' r='2' fill='black'/%3E%3Ccircle cx='12' cy='12' r='2' fill='black'/%3E%3Ccircle cx='19' cy='12' r='2' fill='black'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="menu"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 7h16M4 12h16M4 17h16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="home"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 11 12 4l8 7v9H5v-9Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='M9.5 20v-6h5v6' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="star"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 3 2.7 5.4 6 .9-4.4 4.3 1 6-5.3-2.8-5.3 2.8 1-6-4.4-4.3 6-.9z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="history"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 12a8 8 0 1 0 2.3-5.7L4 8.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M4 4v4.5h4.5M12 8v5l3.5 2' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="auto_awesome"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 3 1.6 5.2L19 10l-5.4 1.8L12 17l-1.6-5.2L5 10l5.4-1.8zM5.5 15.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8zM18.5 2.5l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7z' fill='black'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="folder_open"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 7.5h6l2 2H21v8.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-9a1.5 1.5 0 0 1 1.5-1.5Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='M4 18 7 11h14l-3 7' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="drive_file_move"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6h6l2 2h8v10H4z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='m13 12 3 3-3 3M8 15h8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="delete"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 6h14M9 6V4h6v2M8 6l1 14h6l1-14M10.5 10v6M13.5 10v6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="right_panel_open"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 5h16v14H4zM14 5v14M8 12h6M11 9l3 3-3 3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="open_in_new"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 6H5v13h13v-3M12 5h7v7M10 14 19 5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="share"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='18' cy='5' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='6' cy='12' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='18' cy='19' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m8.7 10.7 6.6-4.4M8.7 13.3l6.6 4.4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="link"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.5 14.5 14.5 9.5M10.8 6.2l1.1-1.1a4 4 0 0 1 5.7 5.7l-1.6 1.6M13.2 17.8l-1.1 1.1a4 4 0 0 1-5.7-5.7L8 11.6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="content_copy"],
|
||||||
|
.material-symbols-outlined[data-icon="file_copy"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 8h10v12H8zM6 16H4V4h10v2' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="tag"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 4 5 20M19 4l-4 16M4 9h16M3 15h16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="edit"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m4 17-.5 3.5L7 20l11-11-3-3zM13 8l3 3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="add"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 5v14M5 12h14' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="subdirectory_arrow_right"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4v7a3 3 0 0 0 3 3h8M14 10l4 4-4 4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||||
|
.material-symbols-outlined[data-icon="radio_button_unchecked"]::before {
|
||||||
|
width: .78em;
|
||||||
|
height: .78em;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 999px;
|
||||||
|
-webkit-mask: none;
|
||||||
|
mask: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 滚动条(悬停时显示) ===== */
|
/* ===== 滚动条(悬停时显示) ===== */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -1190,10 +1284,14 @@ body {
|
|||||||
flex: 0 0 20px;
|
flex: 0 0 20px;
|
||||||
color: #111111;
|
color: #111111;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree .tree-kind-badge::before {
|
.sidebar-tree .tree-kind-badge::before {
|
||||||
content: "□";
|
content: "□";
|
||||||
|
font-family: var(--wolai-font-sans);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree .tree-kind-badge[data-kind="page"]::before {
|
.sidebar-tree .tree-kind-badge[data-kind="page"]::before {
|
||||||
@@ -1210,11 +1308,28 @@ body {
|
|||||||
color: var(--atelier-secondary);
|
color: var(--atelier-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree .tree-kind-badge[data-kind="folder"]::before {
|
.sidebar-tree .tree-kind-badge[data-kind="folder"]::before,
|
||||||
|
.sidebar-tree .tree-kind-badge[data-kind="asset_folder"]::before {
|
||||||
content: "▱";
|
content: "▱";
|
||||||
color: #D08A1F;
|
color: #D08A1F;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-tree .tree-kind-badge[data-kind="image"]::before {
|
||||||
|
content: "▧";
|
||||||
|
color: #5B7CFA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tree .tree-kind-badge[data-kind="mindmap"]::before {
|
||||||
|
content: "⌘";
|
||||||
|
color: #31AA4D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tree .tree-kind-badge[data-kind="table"]::before,
|
||||||
|
.sidebar-tree .tree-kind-badge[data-kind="luckysheet"]::before {
|
||||||
|
content: "▦";
|
||||||
|
color: #2367F6;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-tree .tree-link {
|
.sidebar-tree .tree-link {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -1245,6 +1360,89 @@ body {
|
|||||||
color: var(--atelier-text);
|
color: var(--atelier-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 236px;
|
||||||
|
max-width: min(320px, calc(100vw - 16px));
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 16px 32px rgba(27, 28, 28, 0.12);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
color: var(--atelier-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__item {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__item:hover {
|
||||||
|
background: #F4F3F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__item--danger {
|
||||||
|
color: #D64545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__item--danger:hover {
|
||||||
|
background: rgba(214, 69, 69, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__icon {
|
||||||
|
width: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
color: #6D6A65;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__icon::before {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__item--danger .mnote-tree-context-menu__icon {
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__label {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__shortcut {
|
||||||
|
color: #A19D97;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-tree-context-menu__separator {
|
||||||
|
height: 1px;
|
||||||
|
margin: 5px 4px;
|
||||||
|
background: rgba(27, 28, 28, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-tree .tree-node .tree-children .tree-row {
|
.sidebar-tree .tree-node .tree-children .tree-row {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
@@ -1406,6 +1604,16 @@ body {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mnote-material-page-icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
font-size: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-material-page-icon::before {
|
||||||
|
font-size: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
.document-title-heading,
|
.document-title-heading,
|
||||||
.document-shell-header h1 {
|
.document-shell-header h1 {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -1497,7 +1705,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mnote-leptos-tiptap-island-editor-root .block-handle-shell {
|
#mnote-leptos-tiptap-island-editor-root .block-handle-shell {
|
||||||
left: -52px !important;
|
left: -88px !important;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -1607,9 +1815,14 @@ body {
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-page-icon .mnote-symbol--document {
|
.mnote-material-page-icon {
|
||||||
width: 44px;
|
width: 48px;
|
||||||
height: 44px;
|
height: 48px;
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-material-page-icon::before {
|
||||||
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-shell-header h1 {
|
.document-shell-header h1 {
|
||||||
@@ -1617,7 +1830,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mnote-leptos-tiptap-island-editor-root .block-handle-shell {
|
#mnote-leptos-tiptap-island-editor-root .block-handle-shell {
|
||||||
left: -36px !important;
|
left: -64px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1700,17 +1913,17 @@ mod tests {
|
|||||||
fn mnote_css_keeps_tiptap_handles_in_margin_and_title_editable() {
|
fn mnote_css_keeps_tiptap_handles_in_margin_and_title_editable() {
|
||||||
assert!(MNOTE_CSS.contains(".document-title-input"));
|
assert!(MNOTE_CSS.contains(".document-title-input"));
|
||||||
assert!(MNOTE_CSS.contains(".block-handle-shell"));
|
assert!(MNOTE_CSS.contains(".block-handle-shell"));
|
||||||
assert!(MNOTE_CSS.contains("left: -52px !important"));
|
assert!(MNOTE_CSS.contains("left: -88px !important"));
|
||||||
assert!(MNOTE_CSS.contains("pointer-events: none"));
|
assert!(MNOTE_CSS.contains("pointer-events: none"));
|
||||||
assert!(MNOTE_CSS.contains(".mnote-symbol"));
|
assert!(MNOTE_CSS.contains(".material-symbols-outlined"));
|
||||||
assert!(MNOTE_CSS.contains("data-icon=\"home\""));
|
assert!(MNOTE_CSS.contains(".mnote-tree-context-menu"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mnote_css_is_reasonably_sized() {
|
fn mnote_css_is_reasonably_sized() {
|
||||||
// 至少 2000 字符才能包含完整样式
|
// 至少 2000 字符才能包含完整样式
|
||||||
assert!(MNOTE_CSS.len() > 2000);
|
assert!(MNOTE_CSS.len() > 2000);
|
||||||
// 最多 20000 字符避免过于臃肿
|
// 菜单与本地 SVG mask 图标会增加体积,仍保持在单文件可审阅范围内。
|
||||||
assert!(MNOTE_CSS.len() < 32000);
|
assert!(MNOTE_CSS.len() < 46000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ mod tests {
|
|||||||
assert!(html.contains("/documents/doc_root?workspaceId=ws_demo"));
|
assert!(html.contains("/documents/doc_root?workspaceId=ws_demo"));
|
||||||
assert!(html.contains("data-testid=\"wolai-sidebar-create-page\""));
|
assert!(html.contains("data-testid=\"wolai-sidebar-create-page\""));
|
||||||
assert!(html.contains("data-mnote-action=\"create-page\""));
|
assert!(html.contains("data-mnote-action=\"create-page\""));
|
||||||
assert!(html.contains("class=\"mnote-symbol"));
|
assert!(html.contains("class=\"material-symbols-outlined"));
|
||||||
assert!(html.contains("data-icon=\"home\""));
|
assert!(html.contains("data-icon=\"home\""));
|
||||||
assert!(html.contains("data-icon=\"star\""));
|
assert!(html.contains("data-icon=\"star\""));
|
||||||
assert!(html.contains("data-icon=\"delete\""));
|
assert!(html.contains("data-icon=\"delete\""));
|
||||||
@@ -485,28 +485,16 @@ fn item_icon_name(icon: Option<&str>) -> &'static str {
|
|||||||
|
|
||||||
fn render_symbol(icon: &str, class_name: &str) -> String {
|
fn render_symbol(icon: &str, class_name: &str) -> String {
|
||||||
let icon = item_icon_name(Some(icon));
|
let icon = item_icon_name(Some(icon));
|
||||||
let body = match icon {
|
let filled_class = if icon == "star" || icon == "home" {
|
||||||
"delete" => {
|
" material-symbols-filled"
|
||||||
r#"<path d="M5 6h14"></path><path d="M9 6V4h6v2"></path><path d="M8 6l1 14h6l1-14"></path><path d="M10.5 10v6"></path><path d="M13.5 10v6"></path>"#
|
} else {
|
||||||
}
|
""
|
||||||
"folder_open" => {
|
|
||||||
r#"<path d="M3.5 6.5h6l2 2h9"></path><path d="M4 8.5v9.5h14.5l2-7.5H7l-3 7.5"></path>"#
|
|
||||||
}
|
|
||||||
"inventory_2" => {
|
|
||||||
r#"<path d="M4 6h16v4H4z"></path><path d="M6 10v9h12v-9"></path><path d="M9 14h6"></path>"#
|
|
||||||
}
|
|
||||||
"star" => {
|
|
||||||
r#"<path d="m12 3 2.5 5.2 5.7.8-4.1 4 1 5.7-5.1-2.7-5.1 2.7 1-5.7-4.1-4 5.7-.8z"></path>"#
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
r#"<path d="M4 10.5 12 3l8 7.5"></path><path d="M6.5 9.5V21h11V9.5"></path><path d="M9.5 21v-6h5v6"></path>"#
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
r#"<svg class="mnote-symbol {}" data-icon="{}" viewBox="0 0 24 24" aria-hidden="true" focusable="false">{}</svg>"#,
|
r#"<span class="material-symbols-outlined{} {}" data-icon="{}" aria-hidden="true"></span>"#,
|
||||||
|
filled_class,
|
||||||
escape_html(class_name),
|
escape_html(class_name),
|
||||||
escape_html(icon),
|
escape_html(icon),
|
||||||
body
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"rustc_fingerprint":9228011546279038255,"outputs":{"11652014622397750202":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"unknown\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-unknown-unknown`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-unknown-unknown`\n\nwarning: 2 warnings emitted\n\n"},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.89.0 (29483883e 2025-08-04)\nbinary: rustc\ncommit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2\ncommit-date: 2025-08-04\nhost: x86_64-unknown-linux-gnu\nrelease: 1.89.0\nLLVM version: 20.1.7\n","stderr":""}},"successes":{}}
|
{"rustc_fingerprint":9228011546279038255,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.89.0 (29483883e 2025-08-04)\nbinary: rustc\ncommit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2\ncommit-date: 2025-08-04\nhost: x86_64-unknown-linux-gnu\nrelease: 1.89.0\nLLVM version: 20.1.7\n","stderr":""},"11652014622397750202":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"unknown\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-unknown-unknown`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-unknown-unknown`\n\nwarning: 2 warnings emitted\n\n"}},"successes":{}}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
readRustTreeDomainEventPlan,
|
readRustTreeDomainEventPlan,
|
||||||
readRustTreeDomainEventPlans,
|
readRustTreeDomainEventPlans,
|
||||||
readRustTreeDomainEventType,
|
readRustTreeDomainEventType,
|
||||||
|
resolveRustRuntimeProcessEnv,
|
||||||
type RustBridgeCommandPlan,
|
type RustBridgeCommandPlan,
|
||||||
} from "./rust-runtime";
|
} from "./rust-runtime";
|
||||||
|
|
||||||
@@ -82,6 +83,26 @@ describe("shouldUseBuiltBridgeRuntimeBinary", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveRustRuntimeProcessEnv", () => {
|
||||||
|
it("未设置默认 toolchain 时应注入仓库声明的 Rust toolchain", () => {
|
||||||
|
expect(resolveRustRuntimeProcessEnv({} as NodeJS.ProcessEnv)).toMatchObject({
|
||||||
|
CARGO_TERM_COLOR: "never",
|
||||||
|
RUSTUP_TOOLCHAIN: "1.89.0",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("应保留调用方显式设置的 Rust toolchain", () => {
|
||||||
|
expect(
|
||||||
|
resolveRustRuntimeProcessEnv({
|
||||||
|
RUSTUP_TOOLCHAIN: "nightly",
|
||||||
|
} as NodeJS.ProcessEnv),
|
||||||
|
).toMatchObject({
|
||||||
|
CARGO_TERM_COLOR: "never",
|
||||||
|
RUSTUP_TOOLCHAIN: "nightly",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("executeRustBridgeMutationTransport", () => {
|
describe("executeRustBridgeMutationTransport", () => {
|
||||||
it("documents.move 应把 Rust treeWriteOperation 透传给 Convex 写执行器", async () => {
|
it("documents.move 应把 Rust treeWriteOperation 透传给 Convex 写执行器", async () => {
|
||||||
const movePlan = {
|
const movePlan = {
|
||||||
|
|||||||
@@ -217,6 +217,17 @@ type RuntimeInvocation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RUST_RUNTIME_TIMEOUT_MS = 30_000;
|
const RUST_RUNTIME_TIMEOUT_MS = 30_000;
|
||||||
|
const RUST_RUNTIME_FALLBACK_TOOLCHAIN = "1.89.0";
|
||||||
|
|
||||||
|
export function resolveRustRuntimeProcessEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||||
|
const explicitToolchain = env.RUSTUP_TOOLCHAIN?.trim() || env.MNOTE_RUST_BRIDGE_TOOLCHAIN?.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...env,
|
||||||
|
CARGO_TERM_COLOR: "never",
|
||||||
|
RUSTUP_TOOLCHAIN: explicitToolchain || RUST_RUNTIME_FALLBACK_TOOLCHAIN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function pathExists(targetPath: string) {
|
async function pathExists(targetPath: string) {
|
||||||
try {
|
try {
|
||||||
@@ -348,10 +359,7 @@ async function runRustRuntime(input: Record<string, unknown>): Promise<RustRunti
|
|||||||
const result = await new Promise<RuntimeProcessResult>((resolve, reject) => {
|
const result = await new Promise<RuntimeProcessResult>((resolve, reject) => {
|
||||||
const child = spawn(invocation.command, invocation.args, {
|
const child = spawn(invocation.command, invocation.args, {
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
env: {
|
env: resolveRustRuntimeProcessEnv(),
|
||||||
...process.env,
|
|
||||||
CARGO_TERM_COLOR: "never",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user