@@ -10,6 +10,7 @@ use crate::{
1010 NoteUpsertInput , NoteWorkReportOptionDto ,
1111 } ,
1212} ;
13+ use serde_json:: Value ;
1314use tauri:: State ;
1415
1516#[ tauri:: command]
@@ -59,8 +60,17 @@ pub async fn update_note_content(
5960) -> Result < NoteDetailDto , String > {
6061 let title = normalize_title ( & input. title ) ?;
6162 let content = normalize_content ( & input. content ) ?;
63+ let expected_updated_at = normalize_expected_updated_at ( input. expected_updated_at ) ;
6264 let links = normalize_links ( input. links ) ;
63- update_note_content_and_links ( & state. pool , id, & title, & content, & links) . await
65+ update_note_content_and_links (
66+ & state. pool ,
67+ id,
68+ & title,
69+ & content,
70+ expected_updated_at. as_deref ( ) ,
71+ & links,
72+ )
73+ . await
6474}
6575
6676#[ tauri:: command]
@@ -113,9 +123,82 @@ fn normalize_content(content: &str) -> Result<String, String> {
113123 if trimmed. is_empty ( ) {
114124 return Err ( String :: from ( "note content cannot be empty" ) ) ;
115125 }
126+ if is_effectively_empty_note_document ( trimmed) ? {
127+ return Err ( String :: from (
128+ "note content is empty and was blocked to prevent accidental overwrite" ,
129+ ) ) ;
130+ }
116131 Ok ( trimmed. to_string ( ) )
117132}
118133
134+ fn normalize_expected_updated_at ( value : Option < String > ) -> Option < String > {
135+ value. and_then ( |raw| {
136+ let trimmed = raw. trim ( ) ;
137+ if trimmed. is_empty ( ) {
138+ None
139+ } else {
140+ Some ( trimmed. to_string ( ) )
141+ }
142+ } )
143+ }
144+
145+ fn is_effectively_empty_note_document ( content : & str ) -> Result < bool , String > {
146+ let value: Value =
147+ serde_json:: from_str ( content) . map_err ( |_| String :: from ( "note content is invalid JSON" ) ) ?;
148+ let Some ( root) = value. as_object ( ) else {
149+ return Ok ( false ) ;
150+ } ;
151+ let root_type = root. get ( "type" ) . and_then ( Value :: as_str) . unwrap_or ( "" ) ;
152+ if root_type != "doc" {
153+ return Ok ( false ) ;
154+ }
155+ Ok ( !node_has_meaningful_content ( & value) )
156+ }
157+
158+ fn node_has_meaningful_content ( node : & Value ) -> bool {
159+ let Some ( map) = node. as_object ( ) else {
160+ return false ;
161+ } ;
162+ let node_type = map. get ( "type" ) . and_then ( Value :: as_str) . unwrap_or ( "" ) ;
163+ if node_type == "text" {
164+ return map
165+ . get ( "text" )
166+ . and_then ( Value :: as_str)
167+ . map ( |text| !text. trim ( ) . is_empty ( ) )
168+ . unwrap_or ( false ) ;
169+ }
170+ if node_type == "noteReference" {
171+ return true ;
172+ }
173+ if node_type == "image" {
174+ return map
175+ . get ( "attrs" )
176+ . and_then ( Value :: as_object)
177+ . and_then ( |attrs| attrs. get ( "src" ) )
178+ . and_then ( Value :: as_str)
179+ . map ( |src| !src. trim ( ) . is_empty ( ) )
180+ . unwrap_or ( false ) ;
181+ }
182+
183+ if let Some ( attrs) = map. get ( "attrs" ) . and_then ( Value :: as_object) {
184+ let latex = attrs
185+ . get ( "latex" )
186+ . or_else ( || attrs. get ( "value" ) )
187+ . or_else ( || attrs. get ( "text" ) )
188+ . and_then ( Value :: as_str)
189+ . map ( |s| !s. trim ( ) . is_empty ( ) )
190+ . unwrap_or ( false ) ;
191+ if latex {
192+ return true ;
193+ }
194+ }
195+
196+ if let Some ( children) = map. get ( "content" ) . and_then ( Value :: as_array) {
197+ return children. iter ( ) . any ( node_has_meaningful_content) ;
198+ }
199+ false
200+ }
201+
119202fn normalize_links ( links : Vec < NoteLinkRefInput > ) -> Vec < NoteLinkRefInput > {
120203 let mut out = Vec :: new ( ) ;
121204 for item in links {
0 commit comments