fix: relink rust web tree editor runtime
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::web_shell::{load_sidebar_tree_html, load_workspace_shell_projection};
|
||||
use crate::routes::web_shell::{
|
||||
load_file_tree_html, load_sidebar_tree_html, load_workspace_shell_projection,
|
||||
};
|
||||
use crate::transport::convex::execute_convex_mutation_by_name;
|
||||
use crate::workspace_shell::render_workspace_shell_sidebar_html;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{header, HeaderName, HeaderValue, Request, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
@@ -29,6 +33,7 @@ struct GatewayManifest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct RootEntryQuery {
|
||||
page_id: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn gateway_health(State(state): State<AppState>) -> Response {
|
||||
@@ -82,44 +87,61 @@ pub async fn root_entry(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<RootEntryQuery>,
|
||||
) -> Response {
|
||||
let workspace_id = context
|
||||
.workspace
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.unwrap_or("ws_demo");
|
||||
let requested_page_id = query
|
||||
.page_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
) -> Result<Response, WebError> {
|
||||
let workspace_id =
|
||||
resolve_root_workspace_id(&state, &context, query.workspace_id.as_deref()).await?;
|
||||
let requested_page_id = normalize_optional_id(query.page_id.as_deref());
|
||||
let recent_page_id = extract_cookie_value(&context, COOKIE_RECENT_PAGE_ID);
|
||||
let active_page_id = requested_page_id.or(recent_page_id.as_deref());
|
||||
let recent_page_id = normalize_optional_id(recent_page_id.as_deref());
|
||||
let requested_or_recent_page_id =
|
||||
choose_root_entry_active_page_id(requested_page_id, recent_page_id, None, None);
|
||||
let default_workspace_name = format!("{} 的空间", state.config().dev_user_name);
|
||||
let workspace_projection = load_workspace_shell_projection(
|
||||
state.config(),
|
||||
&context,
|
||||
workspace_id,
|
||||
active_page_id,
|
||||
&workspace_id,
|
||||
requested_or_recent_page_id.as_deref(),
|
||||
&default_workspace_name,
|
||||
)
|
||||
.await;
|
||||
let sidebar_tree_html =
|
||||
load_sidebar_tree_html(
|
||||
state.config(),
|
||||
&context,
|
||||
workspace_id,
|
||||
workspace_projection.active_page_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let selected_active_page_id = choose_root_entry_active_page_id(
|
||||
requested_page_id,
|
||||
recent_page_id,
|
||||
workspace_projection.active_page_id.as_deref(),
|
||||
workspace_projection
|
||||
.my_page_items
|
||||
.first()
|
||||
.map(|item| item.id.as_str()),
|
||||
);
|
||||
let sidebar_tree_html = load_sidebar_tree_html(
|
||||
state.config(),
|
||||
&context,
|
||||
&workspace_id,
|
||||
selected_active_page_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let file_tree_html = load_file_tree_html(
|
||||
state.config(),
|
||||
&context,
|
||||
&workspace_id,
|
||||
selected_active_page_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let workspace_sidebar_html = render_workspace_shell_sidebar_html(
|
||||
&workspace_projection,
|
||||
Some(sidebar_tree_html.as_str()),
|
||||
Some(file_tree_html.as_str()),
|
||||
);
|
||||
let workspace_name = workspace_projection.workspace_name.clone();
|
||||
let active_page_id = selected_active_page_id.unwrap_or_default();
|
||||
let active_page_title = workspace_projection
|
||||
.active_page_title
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let content = crate::ssr::render_view(leptos::view! {
|
||||
<crate::ssr::pages::home::HomePage sidebar_tree_html={sidebar_tree_html} workspace_name={workspace_name} workspace_sidebar_html={workspace_sidebar_html} />
|
||||
<crate::ssr::pages::home::HomePage sidebar_tree_html={sidebar_tree_html} workspace_name={workspace_name} workspace_id={workspace_id.clone()} workspace_sidebar_html={workspace_sidebar_html} active_page_id={active_page_id} active_page_title={active_page_title} />
|
||||
});
|
||||
let mut response = Html(format!(
|
||||
r#"<!doctype html>
|
||||
@@ -138,7 +160,7 @@ pub async fn root_entry(
|
||||
))
|
||||
.into_response();
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
response
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn legacy_next_proxy(
|
||||
@@ -241,21 +263,82 @@ pub async fn legacy_next_proxy(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn resolve_root_workspace_id(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
requested_workspace_id: Option<&str>,
|
||||
) -> Result<String, WebError> {
|
||||
if let Some(workspace_id) = normalize_optional_id(requested_workspace_id)
|
||||
.or_else(|| normalize_optional_id(context.workspace.workspace_id.as_deref()))
|
||||
{
|
||||
return Ok(workspace_id.to_string());
|
||||
}
|
||||
|
||||
let bootstrap = execute_convex_mutation_by_name(
|
||||
state.config(),
|
||||
context,
|
||||
"workspaces:ensureDefaultWorkspace",
|
||||
json!({
|
||||
"fallbackName": state.config().dev_user_name,
|
||||
"workspaceIdIfCreate": format!("ws_{}", context.trace.request_id),
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
"root_workspace_bootstrap",
|
||||
)
|
||||
.await?;
|
||||
|
||||
bootstrap
|
||||
.get("activeWorkspaceId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| normalize_optional_id(Some(value)))
|
||||
.map(ToOwned::to_owned)
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_gateway_code(
|
||||
"root_workspace_bootstrap_bad_response",
|
||||
"workspaces.ensureDefaultWorkspace 未返回 activeWorkspaceId",
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "root_workspace_bootstrap")
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_optional_id(value: Option<&str>) -> Option<&str> {
|
||||
value.map(str::trim).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn choose_root_entry_active_page_id(
|
||||
requested_page_id: Option<&str>,
|
||||
recent_page_id: Option<&str>,
|
||||
projection_active_page_id: Option<&str>,
|
||||
first_page_id: Option<&str>,
|
||||
) -> Option<String> {
|
||||
normalize_optional_id(requested_page_id)
|
||||
.or_else(|| normalize_optional_id(recent_page_id))
|
||||
.or_else(|| normalize_optional_id(projection_active_page_id))
|
||||
.or_else(|| normalize_optional_id(first_page_id))
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn extract_cookie_value(context: &RequestContext, name: &str) -> Option<String> {
|
||||
context.auth.cookie_header.as_deref()?.split(';').find_map(|part| {
|
||||
let (cookie_name, cookie_value) = part.trim().split_once('=')?;
|
||||
if cookie_name.trim() == name {
|
||||
let value = cookie_value.trim();
|
||||
if value.is_empty() {
|
||||
None
|
||||
context
|
||||
.auth
|
||||
.cookie_header
|
||||
.as_deref()?
|
||||
.split(';')
|
||||
.find_map(|part| {
|
||||
let (cookie_name, cookie_value) = part.trim().split_once('=')?;
|
||||
if cookie_name.trim() == name {
|
||||
let value = cookie_value.trim();
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn stamp_gateway_headers(headers: &mut axum::http::HeaderMap, legacy: bool) {
|
||||
@@ -340,7 +423,7 @@ mod tests {
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
mutation_fixtures_json: Some(r#"{"workspaces:ensureDefaultWorkspace":{"workspaces":[{"id":"ws_demo","name":"我的空间","type":"personal","iconUrl":null,"memberCount":1,"isDefault":true}],"activeWorkspaceId":"ws_demo"}}"#.into()),
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
@@ -475,7 +558,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains(r#"<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">"#));
|
||||
assert!(html
|
||||
.contains(r#"<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">"#));
|
||||
assert!(html.contains(r#"data-testid="wolai-sidebar""#));
|
||||
assert!(html.contains(r#"data-testid="wolai-topbar""#));
|
||||
assert!(html.contains(r#"data-testid="wolai-floating-ai""#));
|
||||
@@ -487,6 +571,43 @@ mod tests {
|
||||
assert!(!html.contains(r#"<a href="/documents">文档</a>"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_entry_active_selection_prefers_page_id_over_recent_projection_and_first_page() {
|
||||
let selected = super::choose_root_entry_active_page_id(
|
||||
Some("page_query"),
|
||||
Some("page_recent"),
|
||||
Some("page_projection"),
|
||||
Some("page_first"),
|
||||
);
|
||||
assert_eq!(selected.as_deref(), Some("page_query"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_entry_active_selection_falls_back_to_recent_projection_first_then_empty() {
|
||||
let from_recent = super::choose_root_entry_active_page_id(
|
||||
None,
|
||||
Some("page_recent"),
|
||||
Some("page_projection"),
|
||||
Some("page_first"),
|
||||
);
|
||||
assert_eq!(from_recent.as_deref(), Some("page_recent"));
|
||||
|
||||
let from_projection = super::choose_root_entry_active_page_id(
|
||||
Some(" "),
|
||||
None,
|
||||
Some("page_projection"),
|
||||
Some("page_first"),
|
||||
);
|
||||
assert_eq!(from_projection.as_deref(), Some("page_projection"));
|
||||
|
||||
let from_first =
|
||||
super::choose_root_entry_active_page_id(None, None, None, Some("page_first"));
|
||||
assert_eq!(from_first.as_deref(), Some("page_first"));
|
||||
|
||||
let empty = super::choose_root_entry_active_page_id(None, None, None, None);
|
||||
assert_eq!(empty, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_uses_recent_page_cookie_as_active_page() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
@@ -494,6 +615,7 @@ mod tests {
|
||||
Request::builder()
|
||||
.uri("/")
|
||||
.header("cookie", "mnote_recent_page_id=page_child")
|
||||
.header("x-mnote-workspace-id", "ws_demo")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
@@ -506,7 +628,9 @@ mod tests {
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains(r#"data-node-id="page_child""#));
|
||||
assert!(html.contains(r#"class="wolai-page-row wolai-active-row" href="/documents/page_child?workspaceId=ws_demo" data-node-id="page_child""#));
|
||||
assert!(html.contains(r#"href="/documents/page_child?workspaceId=ws_demo""#));
|
||||
assert!(html.contains(r#"data-active="true""#));
|
||||
assert!(html.contains(r#"data-root-active-page-id="page_child""#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user