feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -6,13 +6,13 @@ use crate::routes::gateway::current_actor_id;
|
||||
use crate::routes::local_folder_source::{
|
||||
local_root_has_workspace_manifest, local_workspace_id_from_root_uri,
|
||||
};
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use control_plane::{UpsertUserInput, UpsertUserUiPreferenceInput, UserUiPreferenceRecord};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Map};
|
||||
use serde_json::{Map, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) const SOURCE_FAMILY_MY_SPACE: &str = "my_space";
|
||||
@@ -58,9 +58,33 @@ struct PagePreferenceScope {
|
||||
struct EffectivePagePreferences {
|
||||
scope: PagePreferenceScope,
|
||||
page_options: PageOptions,
|
||||
page_width_preferences: BTreeMap<String, EffectivePageWidthPreference>,
|
||||
ai_preferences: BTreeMap<String, Value>,
|
||||
sources: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct EffectivePageWidthPreference {
|
||||
mode: String,
|
||||
custom: Option<String>,
|
||||
resolved_mode: String,
|
||||
resolved_custom: Option<String>,
|
||||
css_max_width: String,
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PageWidthPreferenceValue {
|
||||
mode: String,
|
||||
custom: Option<String>,
|
||||
}
|
||||
|
||||
const PAGE_WIDTH_CONTENT_TYPES: [&str; 7] = [
|
||||
"default", "markdown", "word", "pdf", "excel", "ppt", "mindmap",
|
||||
];
|
||||
|
||||
pub(crate) async fn effective_preferences(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
@@ -220,30 +244,57 @@ fn resolve_effective_page_preferences(
|
||||
page_options.hide_title_header = true;
|
||||
}
|
||||
let mut sources = BTreeMap::new();
|
||||
apply_preference_records(&mut page_options, &mut sources, &scope, &preferences)?;
|
||||
let mut page_width_preferences = default_page_width_preferences();
|
||||
let mut ai_preferences = BTreeMap::new();
|
||||
apply_preference_records(
|
||||
&mut page_options,
|
||||
&mut page_width_preferences,
|
||||
&mut ai_preferences,
|
||||
&mut sources,
|
||||
&scope,
|
||||
&preferences,
|
||||
)?;
|
||||
resolve_page_width_preferences(&mut page_width_preferences);
|
||||
Ok(EffectivePagePreferences {
|
||||
scope,
|
||||
page_options,
|
||||
page_width_preferences,
|
||||
ai_preferences,
|
||||
sources,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_preference_records(
|
||||
page_options: &mut PageOptions,
|
||||
page_width_preferences: &mut BTreeMap<String, EffectivePageWidthPreference>,
|
||||
ai_preferences: &mut BTreeMap<String, Value>,
|
||||
sources: &mut BTreeMap<String, String>,
|
||||
scope: &PagePreferenceScope,
|
||||
preferences: &[UserUiPreferenceRecord],
|
||||
) -> Result<(), WebError> {
|
||||
for scope_kind in ["global", "source_family", "workspace", "document"] {
|
||||
let mut scope_kinds = vec![
|
||||
"global".to_string(),
|
||||
"source_family".to_string(),
|
||||
"workspace".to_string(),
|
||||
"document".to_string(),
|
||||
];
|
||||
for preference in preferences {
|
||||
if preference.scope_kind.starts_with("ai.") && !scope_kinds.contains(&preference.scope_kind)
|
||||
{
|
||||
scope_kinds.push(preference.scope_kind.clone());
|
||||
}
|
||||
}
|
||||
for scope_kind in scope_kinds {
|
||||
for preference in preferences
|
||||
.iter()
|
||||
.filter(|preference| preference.scope_kind.trim() == scope_kind)
|
||||
.filter(|preference| preference.scope_kind.trim() == scope_kind.as_str())
|
||||
{
|
||||
let scope_matches = match scope_kind {
|
||||
let scope_matches = match scope_kind.as_str() {
|
||||
"global" => preference.scope_id.trim() == "default",
|
||||
"source_family" => preference.scope_id.trim() == scope.source_family,
|
||||
"workspace" => preference.scope_id.trim() == scope.workspace_id,
|
||||
"document" => preference.scope_id.trim() == scope.document_id,
|
||||
kind if kind.starts_with("ai.") => preference.scope_id.trim() == scope.workspace_id,
|
||||
_ => false,
|
||||
};
|
||||
if !scope_matches {
|
||||
@@ -255,6 +306,22 @@ fn apply_preference_records(
|
||||
preference.key
|
||||
))
|
||||
})?;
|
||||
if preference.key.starts_with("ai.common.") || preference.key.starts_with("ai.agent.") {
|
||||
ai_preferences.insert(preference.key.clone(), value);
|
||||
sources.insert(preference.key.clone(), scope_kind.to_string());
|
||||
continue;
|
||||
}
|
||||
if let Some(content_type) = page_width_content_type_for_key(&preference.key) {
|
||||
if let Some(normalized) = normalize_page_width_preference(content_type, &value) {
|
||||
if let Some(preference_value) = page_width_preferences.get_mut(content_type) {
|
||||
preference_value.mode = normalized.mode;
|
||||
preference_value.custom = normalized.custom;
|
||||
preference_value.source = scope_kind.to_string();
|
||||
sources.insert(preference.key.clone(), scope_kind.to_string());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if apply_page_option_value(page_options, &preference.key, &value) {
|
||||
sources.insert(preference.key.clone(), scope_kind.to_string());
|
||||
}
|
||||
@@ -291,6 +358,19 @@ fn page_preference_scope(
|
||||
}
|
||||
|
||||
fn preference_scope_for_key(key: &str, scope: &PagePreferenceScope) -> Option<(String, String)> {
|
||||
let trimmed = key.trim();
|
||||
if trimmed.starts_with("ai.common.") {
|
||||
return Some(("ai.common".to_string(), scope.workspace_id.clone()));
|
||||
}
|
||||
if let Some(rest) = trimmed.strip_prefix("ai.agent.") {
|
||||
let agent = rest.split('.').next().unwrap_or_default().trim();
|
||||
if !agent.is_empty() {
|
||||
return Some((format!("ai.agent.{agent}"), scope.workspace_id.clone()));
|
||||
}
|
||||
}
|
||||
if page_width_content_type_for_key(key).is_some() {
|
||||
return Some(("global".to_string(), "default".to_string()));
|
||||
}
|
||||
match key.trim() {
|
||||
"hideTitleHeader" | "hide_title_header" => {
|
||||
Some(("source_family".to_string(), scope.source_family.clone()))
|
||||
@@ -321,7 +401,7 @@ fn preference_scope_for_key(key: &str, scope: &PagePreferenceScope) -> Option<(S
|
||||
}
|
||||
|
||||
fn preference_workspace_for_scope(workspace_id: &str, scope_kind: &str) -> Option<String> {
|
||||
if scope_kind == "workspace" || scope_kind == "document" {
|
||||
if scope_kind == "workspace" || scope_kind == "document" || scope_kind.starts_with("ai.") {
|
||||
Some(workspace_id.to_string())
|
||||
} else {
|
||||
None
|
||||
@@ -329,7 +409,7 @@ fn preference_workspace_for_scope(workspace_id: &str, scope_kind: &str) -> Optio
|
||||
}
|
||||
|
||||
fn preference_source_kind_for_scope(source_kind: &str, scope_kind: &str) -> Option<String> {
|
||||
if scope_kind == "workspace" || scope_kind == "document" {
|
||||
if scope_kind == "workspace" || scope_kind == "document" || scope_kind.starts_with("ai.") {
|
||||
Some(source_kind.to_string())
|
||||
} else {
|
||||
None
|
||||
@@ -338,6 +418,10 @@ fn preference_source_kind_for_scope(source_kind: &str, scope_kind: &str) -> Opti
|
||||
|
||||
fn effective_preferences_payload(effective: EffectivePagePreferences) -> Value {
|
||||
let page_options = serde_json::to_value(&effective.page_options).unwrap_or_else(|_| json!({}));
|
||||
let page_width_preferences =
|
||||
serde_json::to_value(&effective.page_width_preferences).unwrap_or_else(|_| json!({}));
|
||||
let ai_preferences =
|
||||
serde_json::to_value(&effective.ai_preferences).unwrap_or_else(|_| json!({}));
|
||||
let sources = effective
|
||||
.sources
|
||||
.into_iter()
|
||||
@@ -354,6 +438,8 @@ fn effective_preferences_payload(effective: EffectivePagePreferences) -> Value {
|
||||
"documentId": effective.scope.document_id,
|
||||
},
|
||||
"pageOptions": page_options,
|
||||
"pageWidthPreferences": page_width_preferences,
|
||||
"aiPreferences": ai_preferences,
|
||||
"sources": Value::Object(sources),
|
||||
}
|
||||
})
|
||||
@@ -474,6 +560,127 @@ fn apply_page_option_value(options: &mut PageOptions, key: &str, value: &Value)
|
||||
false
|
||||
}
|
||||
|
||||
fn page_width_content_type_for_key(key: &str) -> Option<&'static str> {
|
||||
let content_type = key.trim().strip_prefix("pageWidth.")?;
|
||||
PAGE_WIDTH_CONTENT_TYPES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|candidate| *candidate == content_type)
|
||||
}
|
||||
|
||||
fn default_page_width_preferences() -> BTreeMap<String, EffectivePageWidthPreference> {
|
||||
PAGE_WIDTH_CONTENT_TYPES
|
||||
.into_iter()
|
||||
.map(|content_type| {
|
||||
let mode = match content_type {
|
||||
"default" => "comfortable",
|
||||
"markdown" => "readable",
|
||||
"word" | "pdf" | "ppt" => "wide",
|
||||
"excel" | "mindmap" => "full",
|
||||
_ => "comfortable",
|
||||
};
|
||||
(
|
||||
content_type.to_string(),
|
||||
EffectivePageWidthPreference {
|
||||
mode: mode.to_string(),
|
||||
custom: None,
|
||||
resolved_mode: mode.to_string(),
|
||||
resolved_custom: None,
|
||||
css_max_width: page_width_css_max_width(mode).to_string(),
|
||||
source: "system".to_string(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_page_width_preference(
|
||||
content_type: &str,
|
||||
value: &Value,
|
||||
) -> Option<PageWidthPreferenceValue> {
|
||||
let mode = value
|
||||
.as_object()
|
||||
.and_then(|object| object.get("mode"))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| value.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|mode| !mode.is_empty())?;
|
||||
let mode = if page_width_mode_is_supported(mode) {
|
||||
mode
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let mode = if content_type == "default" && mode == "inherit" {
|
||||
"comfortable"
|
||||
} else {
|
||||
mode
|
||||
};
|
||||
let custom = value
|
||||
.as_object()
|
||||
.and_then(|object| object.get("custom"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|custom| !custom.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
Some(PageWidthPreferenceValue {
|
||||
mode: mode.to_string(),
|
||||
custom,
|
||||
})
|
||||
}
|
||||
|
||||
fn page_width_mode_is_supported(mode: &str) -> bool {
|
||||
matches!(
|
||||
mode,
|
||||
"inherit" | "readable" | "comfortable" | "wide" | "full" | "custom"
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_page_width_preferences(
|
||||
preferences: &mut BTreeMap<String, EffectivePageWidthPreference>,
|
||||
) {
|
||||
let default_mode = preferences
|
||||
.get("default")
|
||||
.map(|preference| preference.mode.as_str())
|
||||
.filter(|mode| *mode != "inherit")
|
||||
.unwrap_or("comfortable")
|
||||
.to_string();
|
||||
let default_custom = preferences
|
||||
.get("default")
|
||||
.and_then(|preference| preference.custom.clone());
|
||||
|
||||
for content_type in PAGE_WIDTH_CONTENT_TYPES {
|
||||
let Some(preference) = preferences.get_mut(content_type) else {
|
||||
continue;
|
||||
};
|
||||
let resolved_mode = if content_type != "default" && preference.mode == "inherit" {
|
||||
default_mode.clone()
|
||||
} else if preference.mode == "inherit" {
|
||||
"comfortable".to_string()
|
||||
} else {
|
||||
preference.mode.clone()
|
||||
};
|
||||
let resolved_custom = if content_type != "default" && preference.mode == "inherit" {
|
||||
default_custom.clone()
|
||||
} else {
|
||||
preference.custom.clone()
|
||||
};
|
||||
preference.resolved_mode = resolved_mode.clone();
|
||||
preference.resolved_custom = resolved_custom;
|
||||
preference.css_max_width = page_width_css_max_width(&resolved_mode).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn page_width_css_max_width(mode: &str) -> &'static str {
|
||||
match mode {
|
||||
"readable" => "760px",
|
||||
"comfortable" => "980px",
|
||||
"wide" => "1180px",
|
||||
"full" => "none",
|
||||
"custom" => "980px",
|
||||
_ => "980px",
|
||||
}
|
||||
}
|
||||
|
||||
trait EmptyStringExt {
|
||||
fn if_empty_else(self, fallback: impl FnOnce() -> String) -> String;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user