Skip to main content

yggdrasil/pages/admin/note_editor/
save.rs

1//! One serialized save pipeline for autosave, explicit actions and revision restoration.
2//! Explicit actions report through toasts; plain saves only update the header status.
3//!
4//! 异步续写全部走 `try_*`:离开笔记(编辑器卸载)后迟到的响应不会写入任何状态,也不弹提示。
5use 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
13/// 编辑器已卸载:流水线中途放弃,结果由 [`finish`] 丢弃。
14fn 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            // 编辑器已卸载时 `try_set` 失败,什么也不会发生。
53            if state.save.try_get_untracked().is_some() {
54                state.fail(failure_message(failure, &error));
55            }
56            false
57        }
58    }
59}
60
61/// False also means busy/uploading: callers must not continue to preview or restore.
62pub(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        // The user can keep typing while history loads. Never replace those edits.
111        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
128/// 工作稿停止输入后自动保存。每次状态变化都会让之前排队的保存失效(世代号),
129/// 已经发出的请求不受影响。
130pub(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        // 逐项求值,不让 `||` 短路漏掉追踪;`draft` 本身也要追踪:每次输入都重新计时。
137        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}