Implement Rust web sidebar title and tree interactions
This commit is contained in:
@@ -2,7 +2,8 @@ use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::query_support::{
|
||||
execute_runtime_query_against_data, resolve_effective_workspace_id,
|
||||
execute_runtime_query_against_data, execute_runtime_query_via_convex,
|
||||
resolve_effective_workspace_id,
|
||||
};
|
||||
use crate::routes::web_shell::load_sidebar_tree_html;
|
||||
use crate::ssr::pages::search::SearchPage;
|
||||
@@ -68,18 +69,30 @@ pub async fn shell(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let search_query = query.q.as_deref().map(str::trim).unwrap_or("");
|
||||
let initial_results = load_search_results(
|
||||
state.config(),
|
||||
&context,
|
||||
workspace_id,
|
||||
search_query,
|
||||
None,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| empty_search_projection());
|
||||
let contract = json!({
|
||||
"schema": "mnote.search_shell.v1",
|
||||
"owner": "mnote-web",
|
||||
"projectionOwner": initial_results.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"shell": "search",
|
||||
"workspaceId": workspace_id,
|
||||
"query": search_query,
|
||||
"initialResults": {
|
||||
"queryName": "search.documents",
|
||||
"results": []
|
||||
"queryName": "search.documents.query",
|
||||
"projectionOwner": initial_results.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"results": initial_results.get("results").cloned().unwrap_or_else(|| Value::Array(vec![]))
|
||||
},
|
||||
"island": {
|
||||
"kind": "react_search_palette",
|
||||
"kind": "search_interaction_island",
|
||||
"mountId": "mnote-search-island",
|
||||
"runtime": "SearchPaletteHost"
|
||||
},
|
||||
@@ -92,6 +105,7 @@ pub async fn shell(
|
||||
workspace_id={workspace_id.to_string()}
|
||||
search_query={search_query.to_string()}
|
||||
sidebar_tree_html={sidebar_tree_html}
|
||||
initial_results_html={render_initial_results_html(initial_results.get("results").and_then(Value::as_array))}
|
||||
/>
|
||||
});
|
||||
let html = format!(
|
||||
@@ -122,7 +136,7 @@ pub async fn shell(
|
||||
}
|
||||
|
||||
pub async fn documents(
|
||||
State(_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(body): Json<SearchDocumentsRequest>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
@@ -137,49 +151,16 @@ pub async fn documents(
|
||||
None
|
||||
};
|
||||
|
||||
let result = if normalized_query.is_empty() {
|
||||
json!({
|
||||
"enqueueAssetIds": [],
|
||||
"results": [],
|
||||
})
|
||||
} else {
|
||||
execute_runtime_query_against_data(
|
||||
&context,
|
||||
Some(&effective_workspace_id),
|
||||
RuntimeQueryEnvelopeWire {
|
||||
name: "search.documents".into(),
|
||||
payload: json!({
|
||||
"query": normalized_query,
|
||||
"workspaceId": effective_workspace_id,
|
||||
"pageId": page_id,
|
||||
"limit": body.limit.unwrap_or(30),
|
||||
"titleOnly": filters.title_only.unwrap_or(false),
|
||||
"exact": filters.exact.unwrap_or(false),
|
||||
"includeOcr": filters.include_ocr.unwrap_or(false),
|
||||
"timeRange": filters.time_range.unwrap_or_else(|| "any".into()),
|
||||
"timeField": filters.time_field.unwrap_or_else(|| "updated".into()),
|
||||
"customRangeFrom": filters.custom_range.as_ref().and_then(|range| range.from.clone()),
|
||||
"customRangeTo": filters.custom_range.as_ref().and_then(|range| range.to.clone()),
|
||||
}),
|
||||
},
|
||||
json!({
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_1",
|
||||
"workspaceId": effective_workspace_id,
|
||||
"title": "Rust Web 搜索结果",
|
||||
"rawText": "mnote-web search documents transport",
|
||||
"createdAt": "2026-04-28T00:00:00Z",
|
||||
"updatedAt": "2026-04-28T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"mindmaps": [],
|
||||
"tables": [],
|
||||
"tableRows": [],
|
||||
"assets": []
|
||||
}),
|
||||
)?
|
||||
};
|
||||
let result = load_search_results_with_filters(
|
||||
state.config(),
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&normalized_query,
|
||||
page_id,
|
||||
body.limit.unwrap_or(30),
|
||||
filters,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
@@ -188,10 +169,12 @@ pub async fn documents(
|
||||
headers,
|
||||
Json(json!({
|
||||
"results": result.get("results").cloned().unwrap_or(Value::Array(vec![])),
|
||||
"projectionOwner": result.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"recent": [],
|
||||
"meta": {
|
||||
"owner": "mnote-web",
|
||||
"queryName": "search.documents",
|
||||
"projectionOwner": result.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"queryName": "search.documents.query",
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
},
|
||||
@@ -199,10 +182,138 @@ pub async fn documents(
|
||||
))
|
||||
}
|
||||
|
||||
async fn load_search_results(
|
||||
config: &crate::app::AppConfig,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
query: &str,
|
||||
page_id: Option<String>,
|
||||
limit: u32,
|
||||
) -> Result<Value, WebError> {
|
||||
load_search_results_with_filters(
|
||||
config,
|
||||
context,
|
||||
workspace_id,
|
||||
query,
|
||||
page_id,
|
||||
limit,
|
||||
SearchDocumentsFilters::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_search_results_with_filters(
|
||||
config: &crate::app::AppConfig,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
query: &str,
|
||||
page_id: Option<String>,
|
||||
limit: u32,
|
||||
filters: SearchDocumentsFilters,
|
||||
) -> Result<Value, WebError> {
|
||||
if query.trim().is_empty() {
|
||||
return Ok(empty_search_projection());
|
||||
}
|
||||
let runtime_query = RuntimeQueryEnvelopeWire {
|
||||
name: "search.documents.query".into(),
|
||||
payload: json!({
|
||||
"query": query,
|
||||
"workspaceId": workspace_id,
|
||||
"pageId": page_id,
|
||||
"limit": limit,
|
||||
"titleOnly": filters.title_only.unwrap_or(false),
|
||||
"exact": filters.exact.unwrap_or(false),
|
||||
"includeOcr": filters.include_ocr.unwrap_or(false),
|
||||
"timeRange": filters.time_range.unwrap_or_else(|| "any".into()),
|
||||
"timeField": filters.time_field.unwrap_or_else(|| "updated".into()),
|
||||
"customRangeFrom": filters.custom_range.as_ref().and_then(|range| range.from.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 {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => execute_runtime_query_against_data(
|
||||
context,
|
||||
Some(workspace_id),
|
||||
runtime_query,
|
||||
fallback_search_dataset(workspace_id),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_search_projection() -> Value {
|
||||
json!({
|
||||
"enqueueAssetIds": [],
|
||||
"projectionOwner": "rust-kernel",
|
||||
"results": [],
|
||||
})
|
||||
}
|
||||
|
||||
fn fallback_search_dataset(workspace_id: &str) -> Value {
|
||||
json!({
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_1",
|
||||
"workspaceId": workspace_id,
|
||||
"title": "Rust Web 搜索结果",
|
||||
"rawText": "mnote-web search documents transport",
|
||||
"createdAt": "2026-04-28T00:00:00Z",
|
||||
"updatedAt": "2026-04-28T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"mindmaps": [],
|
||||
"tables": [],
|
||||
"tableRows": [],
|
||||
"assets": []
|
||||
})
|
||||
}
|
||||
|
||||
fn render_initial_results_html(results: Option<&Vec<Value>>) -> String {
|
||||
let Some(results) = results else {
|
||||
return r#"<div class="search-empty" data-search-empty="true">暂无结果</div>"#.into();
|
||||
};
|
||||
if results.is_empty() {
|
||||
return r#"<div class="search-empty" data-search-empty="true">暂无结果</div>"#.into();
|
||||
}
|
||||
let items = results
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let title = item
|
||||
.get("title")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("无标题");
|
||||
let snippet = item
|
||||
.get("snippet")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let href = item
|
||||
.get("publicPath")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("#");
|
||||
format!(
|
||||
r#"<a class="search-result" data-search-result-owner="rust-kernel" href="{}"><strong>{}</strong><span>{}</span></a>"#,
|
||||
escape_html(href),
|
||||
escape_html(title),
|
||||
escape_html(snippet)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
format!(r#"<div class="search-result-list">{items}</div>"#)
|
||||
}
|
||||
|
||||
fn escape_script_json(value: &str) -> String {
|
||||
value.replace("</script", "<\\/script")
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
fn stamp_search_headers(headers: &mut HeaderMap) {
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("mnote-web"));
|
||||
@@ -275,8 +386,10 @@ mod tests {
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains("data-mnote-shell=\"search\""));
|
||||
assert!(html.contains("mnote.search_shell.v1"));
|
||||
assert!(html.contains("react_search_palette"));
|
||||
assert!(html.contains("search.documents"));
|
||||
assert!(html.contains("search_interaction_island"));
|
||||
assert!(html.contains("search.documents.query"));
|
||||
assert!(html.contains("search-result"));
|
||||
assert!(html.contains("Rust Web 搜索结果"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -327,6 +440,8 @@ mod tests {
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["meta"]["owner"], "mnote-web");
|
||||
assert_eq!(payload["meta"]["queryName"], "search.documents");
|
||||
assert_eq!(payload["meta"]["queryName"], "search.documents.query");
|
||||
assert_eq!(payload["meta"]["projectionOwner"], "rust-kernel");
|
||||
assert_eq!(payload["projectionOwner"], "rust-kernel");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user