yggdrasil/pages/admin/note_editor/
save.rs1use super::state::{reconcile_save, EditorState, SaveState};
6use crate::api::error::ApiError;
7use crate::api::notes::{act_on_note, get_owned_note, save_note};
8use crate::models::note::{Note, NoteAction};
9use crate::utils::error::failure_message;
10use leptos::prelude::*;
11use leptos::task::spawn_local;
12
13fn closed() -> ApiError {
15 ApiError::Internal("编辑器已关闭".into())
16}
17
18async fn save_snapshot(state: EditorState) -> Result<Note, ApiError> {
19 let snapshot = state.draft_now().ok_or_else(closed)?;
20 let saved = state.saved_now().ok_or_else(closed)?;
21 let dirty = state.dirty_now().ok_or_else(closed)?;
22 let note = match saved {
23 Some(note) if !dirty => note,
24 _ => save_note(snapshot.clone()).await?,
25 };
26 state
27 .draft
28 .try_update(|current| reconcile_save(current, &snapshot, note.draft()))
29 .ok_or_else(closed)?;
30 state.saved.try_set(Some(note.clone()));
31 Ok(note)
32}
33
34fn finish(
35 state: EditorState,
36 result: Result<Note, ApiError>,
37 message: Option<&str>,
38 failure: &str,
39) -> bool {
40 match result {
41 Ok(note) => {
42 if state.saved.try_set(Some(note)).is_some() {
43 return false;
44 }
45 let _ = state.save.try_set(SaveState::Idle);
46 if let Some(message) = message {
47 state.toast.success(message);
48 }
49 true
50 }
51 Err(error) => {
52 if state.save.try_get_untracked().is_some() {
54 state.fail(failure_message(failure, &error));
55 }
56 false
57 }
58 }
59}
60
61pub(super) async fn persist(state: EditorState, action: Option<NoteAction>) -> bool {
63 if !state.begin() {
64 return false;
65 }
66 let result = async {
67 let note = save_snapshot(state).await?;
68 match action {
69 Some(action) => act_on_note(note.id, note.version, action).await,
70 None => Ok(note),
71 }
72 }
73 .await;
74 finish(
75 state,
76 result,
77 action.map(|action| match action {
78 NoteAction::Publish => "公开版本已更新",
79 NoteAction::IncludeKnowledge => "已收录当前版本",
80 NoteAction::Unpublish => "已撤回公开版本",
81 NoteAction::ExcludeKnowledge => "已移出知识库",
82 NoteAction::Trash => "已移入回收站",
83 NoteAction::Restore => "已恢复为草稿",
84 }),
85 match action {
86 None => "保存失败",
87 Some(NoteAction::Publish) => "发布失败",
88 Some(NoteAction::IncludeKnowledge) => "收录失败",
89 Some(NoteAction::Unpublish) => "撤回失败",
90 Some(NoteAction::ExcludeKnowledge) => "移出知识库失败",
91 Some(NoteAction::Trash) => "移入回收站失败",
92 Some(NoteAction::Restore) => "恢复失败",
93 },
94 )
95}
96
97pub(super) async fn restore_revision(state: EditorState, version: i32) {
98 if !state.begin() {
99 return;
100 }
101 let result = async {
102 let note = save_snapshot(state).await?;
103 if state.dirty_now().ok_or_else(closed)? {
104 return Err(ApiError::BadRequest(
105 "保存期间有新的编辑,已保留当前内容,请再次选择历史版本".into(),
106 ));
107 }
108 let before_fetch = state.draft_now().ok_or_else(closed)?;
109 let old = get_owned_note(note.id, Some(version)).await?;
110 if state.draft_now().as_ref() != Some(&before_fetch) {
112 return Err(ApiError::BadRequest(
113 "恢复期间有新的编辑,已保留当前内容,请再次选择历史版本".into(),
114 ));
115 }
116 state.restore_local(old.draft());
117 save_snapshot(state).await
118 }
119 .await;
120 finish(
121 state,
122 result,
123 Some("历史版本已恢复为新草稿"),
124 "恢复历史版本失败",
125 );
126}
127
128pub(super) fn use_autosave(
131 state: EditorState,
132 recovery: RwSignal<Option<crate::models::note::NoteDraft>>,
133) {
134 let generation = StoredValue::new(0_u64);
135 Effect::new(move |_| {
136 let busy = state.busy();
138 let uploading = state.uploading.get();
139 let failed = !state.error().is_empty();
140 let recovering = recovery.with(Option::is_some);
141 let dirty = state.dirty();
142 state.draft.track();
143 let blocked = busy || uploading || failed || recovering || !dirty;
144 let Some(token) = generation.try_update_value(|g| {
145 *g += 1;
146 *g
147 }) else {
148 return;
149 };
150 if blocked {
151 return;
152 }
153 spawn_local(async move {
154 crate::utils::time::sleep_ms(1200).await;
155 if generation.try_get_value() != Some(token) {
156 return;
157 }
158 let still_due = state.draft_now().is_some()
159 && !state.busy_now()
160 && !state.uploading.try_get_untracked().unwrap_or(true)
161 && !state.error_now()
162 && recovery
163 .try_with_untracked(Option::is_none)
164 .unwrap_or(false)
165 && state.dirty_now().unwrap_or(false);
166 if still_due {
167 persist(state, None).await;
168 }
169 });
170 });
171}