start on making deck config and schema/mod changes undoable

+ move timestamps into a struct in a separate file for convenience
This commit is contained in:
Damien Elmes 2021-04-18 17:30:02 +10:00
parent 3abe6168aa
commit 262b50445c
21 changed files with 363 additions and 176 deletions

View file

@ -1,10 +1,11 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::storage::SqliteStorage;
use crate::{collection::Collection, error::Result};
use rusqlite::types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef};
use rusqlite::OptionalExtension;
use crate::{prelude::*, storage::SqliteStorage};
use rusqlite::{
types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef},
OptionalExtension,
};
use serde_derive::{Deserialize, Serialize};
#[derive(Deserialize)]
@ -88,7 +89,7 @@ pub(super) fn db_command_bytes(col: &mut Collection, input: &[u8]) -> Result<Vec
}
DbRequest::Commit => {
if col.state.modified_by_dbproxy {
col.storage.set_modified()?;
col.storage.set_modified_time(TimestampMillis::now())?;
col.state.modified_by_dbproxy = false;
}
col.storage.commit_trx()?;

View file

@ -1,17 +1,20 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub(crate) mod timestamps;
mod transact;
pub(crate) mod undo;
use crate::i18n::I18n;
use crate::types::Usn;
use crate::{
browser_table,
decks::{Deck, DeckId},
notetype::{Notetype, NotetypeId},
prelude::*,
storage::SqliteStorage,
undo::UndoManager,
};
use crate::{error::Result, scheduler::queue::CardQueues};
use crate::{i18n::I18n, ops::StateChanges};
use crate::{log::Logger, scheduler::SchedulerInfo};
use std::{collections::HashMap, path::PathBuf, sync::Arc};
@ -86,68 +89,6 @@ pub struct Collection {
}
impl Collection {
fn transact_inner<F, R>(&mut self, op: Option<Op>, func: F) -> Result<OpOutput<R>>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.storage.begin_rust_trx()?;
self.begin_undoable_operation(op);
let mut res = func(self);
if res.is_ok() {
if let Err(e) = self.storage.set_modified() {
res = Err(e);
} else if let Err(e) = self.storage.commit_rust_trx() {
res = Err(e);
}
}
match res {
Ok(output) => {
let changes = if op.is_some() {
let changes = self.op_changes();
self.maybe_clear_study_queues_after_op(changes);
self.maybe_coalesce_note_undo_entry(changes);
changes
} else {
self.clear_study_queues();
// dummy value, not used by transact_no_undo(). only required
// until we can migrate all the code to undoable ops
OpChanges {
op: Op::SetFlag,
changes: StateChanges::default(),
}
};
self.end_undoable_operation();
Ok(OpOutput { output, changes })
}
Err(err) => {
self.discard_undo_and_study_queues();
self.storage.rollback_rust_trx()?;
Err(err)
}
}
}
/// Execute the provided closure in a transaction, rolling back if
/// an error is returned. Records undo state, and returns changes.
pub(crate) fn transact<F, R>(&mut self, op: Op, func: F) -> Result<OpOutput<R>>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.transact_inner(Some(op), func)
}
/// Execute the provided closure in a transaction, rolling back if
/// an error is returned.
pub(crate) fn transact_no_undo<F, R>(&mut self, func: F) -> Result<R>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.transact_inner(None, func).map(|out| out.output)
}
pub(crate) fn close(self, downgrade: bool) -> Result<()> {
self.storage.close(downgrade)
}
@ -169,8 +110,9 @@ impl Collection {
col.storage.clear_deck_usns()?;
col.storage.clear_notetype_usns()?;
col.storage.increment_usn()?;
col.storage.set_schema_modified()?;
col.storage.set_last_sync(col.storage.get_schema_mtime()?)
col.set_schema_modified()?;
col.storage
.set_last_sync(col.storage.get_collection_timestamps()?.schema_change)
})?;
self.storage.optimize()
}

View file

@ -0,0 +1,32 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::prelude::*;
pub(crate) struct CollectionTimestamps {
pub collection_change: TimestampMillis,
pub schema_change: TimestampMillis,
pub last_sync: TimestampMillis,
}
impl CollectionTimestamps {
pub fn collection_changed_since_sync(&self) -> bool {
self.collection_change > self.last_sync
}
pub fn schema_changed_since_sync(&self) -> bool {
self.schema_change > self.last_sync
}
}
impl Collection {
pub(crate) fn set_modified(&mut self) -> Result<()> {
let stamps = self.storage.get_collection_timestamps()?;
self.set_modified_time_undoable(TimestampMillis::now(), stamps.collection_change)
}
pub(crate) fn set_schema_modified(&mut self) -> Result<()> {
let stamps = self.storage.get_collection_timestamps()?;
self.set_schema_modified_time_undoable(TimestampMillis::now(), stamps.schema_change)
}
}

View file

@ -0,0 +1,68 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{ops::StateChanges, prelude::*};
impl Collection {
fn transact_inner<F, R>(&mut self, op: Option<Op>, func: F) -> Result<OpOutput<R>>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.storage.begin_rust_trx()?;
self.begin_undoable_operation(op);
let mut res = func(self);
if res.is_ok() {
if let Err(e) = self.set_modified() {
res = Err(e);
} else if let Err(e) = self.storage.commit_rust_trx() {
res = Err(e);
}
}
match res {
Ok(output) => {
let changes = if op.is_some() {
let changes = self.op_changes();
self.maybe_clear_study_queues_after_op(changes);
self.maybe_coalesce_note_undo_entry(changes);
changes
} else {
self.clear_study_queues();
// dummy value, not used by transact_no_undo(). only required
// until we can migrate all the code to undoable ops
OpChanges {
op: Op::SetFlag,
changes: StateChanges::default(),
}
};
self.end_undoable_operation();
Ok(OpOutput { output, changes })
}
Err(err) => {
self.discard_undo_and_study_queues();
self.storage.rollback_rust_trx()?;
Err(err)
}
}
}
/// Execute the provided closure in a transaction, rolling back if
/// an error is returned. Records undo state, and returns changes.
pub(crate) fn transact<F, R>(&mut self, op: Op, func: F) -> Result<OpOutput<R>>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.transact_inner(Some(op), func)
}
/// Execute the provided closure in a transaction, rolling back if
/// an error is returned.
pub(crate) fn transact_no_undo<F, R>(&mut self, func: F) -> Result<R>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.transact_inner(None, func).map(|out| out.output)
}
}

View file

@ -0,0 +1,46 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::prelude::*;
#[derive(Debug)]
pub(crate) enum UndoableCollectionChange {
Schema(TimestampMillis),
Modified(TimestampMillis),
}
impl Collection {
pub(crate) fn undo_collection_change(
&mut self,
change: UndoableCollectionChange,
) -> Result<()> {
match change {
UndoableCollectionChange::Schema(schema) => {
let current = self.storage.get_collection_timestamps()?.schema_change;
self.set_schema_modified_time_undoable(schema, current)
}
UndoableCollectionChange::Modified(modified) => {
let current = self.storage.get_collection_timestamps()?.collection_change;
self.set_schema_modified_time_undoable(modified, current)
}
}
}
pub(super) fn set_modified_time_undoable(
&mut self,
modified: TimestampMillis,
original: TimestampMillis,
) -> Result<()> {
self.save_undo(UndoableCollectionChange::Modified(original));
self.storage.set_modified_time(modified)
}
pub(super) fn set_schema_modified_time_undoable(
&mut self,
schema: TimestampMillis,
original: TimestampMillis,
) -> Result<()> {
self.save_undo(UndoableCollectionChange::Schema(original));
self.storage.set_schema_modified_time(schema)
}
}

View file

@ -153,7 +153,7 @@ impl Collection {
fn check_orphaned_cards(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
let cnt = self.storage.delete_orphaned_cards()?;
if cnt > 0 {
self.storage.set_schema_modified()?;
self.set_schema_modified()?;
out.cards_missing_note = cnt;
}
Ok(())
@ -186,7 +186,7 @@ impl Collection {
}
if wrong > 0 {
self.storage.set_schema_modified()?;
self.set_schema_modified()?;
out.card_properties_invalid += wrong;
}
@ -275,7 +275,7 @@ impl Collection {
|| out.templates_missing > 0
|| out.notetypes_recovered > 0
{
self.storage.set_schema_modified()?;
self.set_schema_modified()?;
}
Ok(())
@ -365,10 +365,10 @@ impl Collection {
Ok(Arc::new(basic))
}
fn check_revlog(&self, out: &mut CheckDatabaseOutput) -> Result<()> {
fn check_revlog(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
let cnt = self.storage.fix_revlog_properties()?;
if cnt > 0 {
self.storage.set_schema_modified()?;
self.set_schema_modified()?;
out.revlog_properties_invalid = cnt;
}

View file

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod schema11;
pub(crate) mod undo;
mod update;
pub use {
@ -114,11 +115,15 @@ impl Collection {
}
/// Remove a deck configuration. This will force a full sync.
pub(crate) fn remove_deck_config(&self, dcid: DeckConfId) -> Result<()> {
pub(crate) fn remove_deck_config(&mut self, dcid: DeckConfId) -> Result<()> {
if dcid.0 == 1 {
return Err(AnkiError::invalid_input("can't delete default conf"));
}
self.storage.set_schema_modified()?;
self.storage.remove_deck_conf(dcid)
let conf = self
.storage
.get_deck_config(dcid)?
.ok_or(AnkiError::NotFound)?;
self.set_schema_modified()?;
self.remove_deck_config_undoable(conf)
}
}

View file

@ -0,0 +1,76 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::prelude::*;
#[derive(Debug)]
pub(crate) enum UndoableDeckConfigChange {
Added(Box<DeckConf>),
Updated(Box<DeckConf>),
Removed(Box<DeckConf>),
}
// fixme: schema mod
impl Collection {
pub(crate) fn undo_deck_config_change(
&mut self,
change: UndoableDeckConfigChange,
) -> Result<()> {
match change {
UndoableDeckConfigChange::Added(config) => self.remove_deck_config_undoable(*config),
UndoableDeckConfigChange::Updated(mut config) => {
let current = self
.storage
.get_deck_config(config.id)?
.ok_or_else(|| AnkiError::invalid_input("deck config disappeared"))?;
self.update_single_deck_config_undoable(&mut *config, current)
}
UndoableDeckConfigChange::Removed(config) => self.restore_deleted_deck_config(*config),
}
}
pub(crate) fn remove_deck_config_undoable(&mut self, config: DeckConf) -> Result<()> {
self.storage.remove_deck_conf(config.id)?;
self.save_undo(UndoableDeckConfigChange::Removed(Box::new(config)));
Ok(())
}
#[allow(dead_code)]
pub(super) fn add_deck_config_undoable(
&mut self,
config: &mut DeckConf,
) -> Result<(), AnkiError> {
self.storage.add_deck_conf(config)?;
self.save_undo(UndoableDeckConfigChange::Added(Box::new(config.clone())));
Ok(())
}
pub(super) fn update_single_deck_config_undoable(
&mut self,
config: &mut DeckConf,
original: DeckConf,
) -> Result<()> {
self.save_undo(UndoableDeckConfigChange::Updated(Box::new(original)));
self.storage.update_deck_conf(config)
}
#[allow(dead_code)]
fn add_or_update_deck_config_with_existing_id_undoable(
&mut self,
config: &mut DeckConf,
) -> Result<(), AnkiError> {
self.storage
.add_or_update_deck_config_with_existing_id(config)?;
self.save_undo(UndoableDeckConfigChange::Added(Box::new(config.clone())));
Ok(())
}
fn restore_deleted_deck_config(&mut self, config: DeckConf) -> Result<()> {
self.storage
.add_or_update_deck_config_with_existing_id(&config)?;
self.save_undo(UndoableDeckConfigChange::Added(Box::new(config)));
Ok(())
}
}

View file

@ -11,7 +11,7 @@ pub struct UpdateDeckConfigsIn {
pub target_deck_id: DeckId,
/// Deck will be set to last provided deck config.
pub configs: Vec<DeckConf>,
pub removed_config_ids: Vec<DeckId>,
pub removed_config_ids: Vec<DeckConfId>,
pub apply_to_children: bool,
}
@ -25,7 +25,10 @@ impl Collection {
all_config: self.get_deck_config_with_extra_for_update()?,
current_deck: Some(self.get_current_deck_for_update(deck)?),
defaults: Some(DeckConf::default().into()),
schema_modified: self.storage.schema_modified()?,
schema_modified: self
.storage
.get_collection_timestamps()?
.schema_changed_since_sync(),
})
}
@ -98,6 +101,10 @@ impl Collection {
return Err(AnkiError::invalid_input("config not provided"));
}
for dcid in &input.removed_config_ids {
self.remove_deck_config(*dcid)?;
}
todo!();
}
}

View file

@ -494,7 +494,7 @@ impl Collection {
// fixme: currently the storage layer is taking care of removing the notes and cards,
// but we need to do it in this layer in the future for undo handling
self.transact_no_undo(|col| {
col.storage.set_schema_modified()?;
col.set_schema_modified()?;
col.state.notetype_cache.remove(&ntid);
col.clear_aux_config_for_notetype(ntid)?;
col.storage.remove_notetype(ntid)?;

View file

@ -74,7 +74,7 @@ impl Collection {
}
}
self.storage.set_schema_modified()?;
self.set_schema_modified()?;
let nids = self.search_notes(&format!("mid:{}", nt.id))?;
let usn = self.usn()?;
@ -115,7 +115,7 @@ impl Collection {
return Ok(());
}
self.storage.set_schema_modified()?;
self.set_schema_modified()?;
let changes = TemplateOrdChanges::new(ords, previous_template_count as u32);
if !changes.removed.is_empty() {

View file

@ -85,6 +85,9 @@ pub struct StateChanges {
pub tag: bool,
pub notetype: bool,
pub config: bool,
pub deck_config: bool,
/// covers schema change (and last_sync in the future), but not mtime
pub collection: bool,
}
#[derive(Debug, Clone, Copy)]

View file

@ -127,7 +127,7 @@ impl Collection {
}
// force full sync
self.storage.set_schema_modified()
self.set_schema_modified()
}
fn upgrade_cards_to_v2(&mut self) -> Result<()> {

View file

@ -0,0 +1,58 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::SqliteStorage;
use crate::{collection::timestamps::CollectionTimestamps, prelude::*};
use rusqlite::{params, NO_PARAMS};
impl SqliteStorage {
pub(crate) fn get_collection_timestamps(&self) -> Result<CollectionTimestamps> {
self.db
.prepare_cached("select mod, scm, ls from col")?
.query_row(NO_PARAMS, |row| {
Ok(CollectionTimestamps {
collection_change: row.get(0)?,
schema_change: row.get(1)?,
last_sync: row.get(2)?,
})
})
.map_err(Into::into)
}
pub(crate) fn set_schema_modified_time(&self, stamp: TimestampMillis) -> Result<()> {
self.db
.prepare_cached("update col set scm = ?")?
.execute(&[stamp])?;
Ok(())
}
pub(crate) fn set_last_sync(&self, stamp: TimestampMillis) -> Result<()> {
self.db
.prepare("update col set ls = ?")?
.execute(&[stamp])?;
Ok(())
}
pub(crate) fn set_modified_time(&self, stamp: TimestampMillis) -> Result<()> {
self.db
.prepare_cached("update col set mod=?")?
.execute(params![stamp])?;
Ok(())
}
// Creation timestamp is used less frequently, and has separate accessor
pub(crate) fn creation_stamp(&self) -> Result<TimestampSecs> {
self.db
.prepare_cached("select crt from col")?
.query_row(NO_PARAMS, |row| row.get(0))
.map_err(Into::into)
}
pub(crate) fn set_creation_stamp(&self, stamp: TimestampSecs) -> Result<()> {
self.db
.prepare("update col set crt = ?")?
.execute(&[stamp])?;
Ok(())
}
}

View file

@ -4,8 +4,7 @@
use super::SqliteStorage;
use crate::{
deckconf::{DeckConf, DeckConfId, DeckConfSchema11, DeckConfigInner},
error::Result,
i18n::I18n,
prelude::*,
};
use prost::Message;
use rusqlite::{params, Row, NO_PARAMS};
@ -81,8 +80,12 @@ impl SqliteStorage {
Ok(())
}
/// Used for syncing.
pub(crate) fn add_or_update_deck_config(&self, conf: &DeckConf) -> Result<()> {
/// Used for syncing&undo; will keep provided ID. Shouldn't be used to add
/// new config normally, since it does not allocate an id.
pub(crate) fn add_or_update_deck_config_with_existing_id(&self, conf: &DeckConf) -> Result<()> {
if conf.id.0 == 0 {
return Err(AnkiError::invalid_input("deck with id 0"));
}
let mut conf_bytes = vec![];
conf.inner.encode(&mut conf_bytes)?;
self.db

View file

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub(crate) mod card;
mod collection_timestamps;
mod config;
mod deck;
mod deckconf;

View file

@ -4,7 +4,7 @@
use crate::config::schema11::schema11_config_as_string;
use crate::error::Result;
use crate::error::{AnkiError, DbErrorKind};
use crate::timestamp::{TimestampMillis, TimestampSecs};
use crate::timestamp::TimestampMillis;
use crate::{i18n::I18n, scheduler::timing::v1_creation_date, text::without_combining};
use regex::Regex;
use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
@ -256,81 +256,6 @@ impl SqliteStorage {
//////////////////////////////////////////
pub(crate) fn set_modified(&self) -> Result<()> {
self.set_modified_time(TimestampMillis::now())
}
pub(crate) fn set_modified_time(&self, stamp: TimestampMillis) -> Result<()> {
self.db
.prepare_cached("update col set mod=?")?
.execute(params![stamp])?;
Ok(())
}
pub(crate) fn get_modified_time(&self) -> Result<TimestampMillis> {
self.db
.prepare_cached("select mod from col")?
.query_and_then(NO_PARAMS, |r| r.get(0))?
.next()
.ok_or_else(|| AnkiError::invalid_input("missing col"))?
.map_err(Into::into)
}
pub(crate) fn creation_stamp(&self) -> Result<TimestampSecs> {
self.db
.prepare_cached("select crt from col")?
.query_row(NO_PARAMS, |row| row.get(0))
.map_err(Into::into)
}
pub(crate) fn set_creation_stamp(&self, stamp: TimestampSecs) -> Result<()> {
self.db
.prepare("update col set crt = ?")?
.execute(&[stamp])?;
Ok(())
}
pub(crate) fn set_schema_modified(&self) -> Result<()> {
self.db
.prepare_cached("update col set scm = ?")?
.execute(&[TimestampMillis::now()])?;
Ok(())
}
pub(crate) fn schema_modified(&self) -> Result<bool> {
self.db
.prepare_cached("select scm > ls from col")?
.query_row(NO_PARAMS, |r| r.get(0))
.map_err(Into::into)
}
pub(crate) fn get_schema_mtime(&self) -> Result<TimestampMillis> {
self.db
.prepare_cached("select scm from col")?
.query_and_then(NO_PARAMS, |r| r.get(0))?
.next()
.ok_or_else(|| AnkiError::invalid_input("missing col"))?
.map_err(Into::into)
}
pub(crate) fn get_last_sync(&self) -> Result<TimestampMillis> {
self.db
.prepare_cached("select ls from col")?
.query_and_then(NO_PARAMS, |r| r.get(0))?
.next()
.ok_or_else(|| AnkiError::invalid_input("missing col"))?
.map_err(Into::into)
}
pub(crate) fn set_last_sync(&self, stamp: TimestampMillis) -> Result<()> {
self.db
.prepare("update col set ls = ?")?
.execute(&[stamp])?;
Ok(())
}
//////////////////////////////////////////
/// true if corrupt/can't access
pub(crate) fn quick_check_corrupt(&self) -> bool {
match self.db.pragma_query_value(None, "quick_check", |row| {

View file

@ -610,12 +610,10 @@ pub(crate) async fn get_remote_sync_meta(auth: SyncAuth) -> Result<SyncMeta> {
impl Collection {
pub fn get_local_sync_status(&mut self) -> Result<sync_status_out::Required> {
let last_sync = self.storage.get_last_sync()?;
let schema_mod = self.storage.get_schema_mtime()?;
let normal_mod = self.storage.get_modified_time()?;
let required = if schema_mod > last_sync {
let stamps = self.storage.get_collection_timestamps()?;
let required = if stamps.schema_changed_since_sync() {
sync_status_out::Required::FullSync
} else if normal_mod > last_sync {
} else if stamps.collection_changed_since_sync() {
sync_status_out::Required::NormalSync
} else {
sync_status_out::Required::NoChanges
@ -687,9 +685,10 @@ impl Collection {
}
fn sync_meta(&self) -> Result<SyncMeta> {
let stamps = self.storage.get_collection_timestamps()?;
Ok(SyncMeta {
modified: self.storage.get_modified_time()?,
schema: self.storage.get_schema_mtime()?,
modified: stamps.collection_change,
schema: stamps.schema_change,
usn: self.storage.usn(true)?,
current_time: TimestampSecs::now(),
server_message: "".into(),
@ -894,7 +893,8 @@ impl Collection {
};
if proceed {
let conf = conf.into();
self.storage.add_or_update_deck_config(&conf)?;
self.storage
.add_or_update_deck_config_with_existing_id(&conf)?;
}
}
Ok(())

View file

@ -80,9 +80,10 @@ impl LocalServer {
#[async_trait(?Send)]
impl SyncServer for LocalServer {
async fn meta(&self) -> Result<SyncMeta> {
let stamps = self.col.storage.get_collection_timestamps()?;
Ok(SyncMeta {
modified: self.col.storage.get_modified_time()?,
schema: self.col.storage.get_schema_mtime()?,
modified: stamps.collection_change,
schema: stamps.schema_change,
usn: self.col.storage.usn(true)?,
current_time: TimestampSecs::now(),
server_message: String::new(),

View file

@ -2,7 +2,8 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
card::undo::UndoableCardChange, config::undo::UndoableConfigChange,
card::undo::UndoableCardChange, collection::undo::UndoableCollectionChange,
config::undo::UndoableConfigChange, deckconf::undo::UndoableDeckConfigChange,
decks::undo::UndoableDeckChange, notes::undo::UndoableNoteChange, prelude::*,
revlog::undo::UndoableRevlogChange, scheduler::queue::undo::UndoableQueueChange,
tags::undo::UndoableTagChange,
@ -13,10 +14,12 @@ pub(crate) enum UndoableChange {
Card(UndoableCardChange),
Note(UndoableNoteChange),
Deck(UndoableDeckChange),
DeckConfig(UndoableDeckConfigChange),
Tag(UndoableTagChange),
Revlog(UndoableRevlogChange),
Queue(UndoableQueueChange),
Config(UndoableConfigChange),
Collection(UndoableCollectionChange),
}
impl UndoableChange {
@ -29,6 +32,8 @@ impl UndoableChange {
UndoableChange::Revlog(c) => col.undo_revlog_change(c),
UndoableChange::Queue(c) => col.undo_queue_change(c),
UndoableChange::Config(c) => col.undo_config_change(c),
UndoableChange::DeckConfig(c) => col.undo_deck_config_change(c),
UndoableChange::Collection(c) => col.undo_collection_change(c),
}
}
}
@ -51,6 +56,12 @@ impl From<UndoableDeckChange> for UndoableChange {
}
}
impl From<UndoableDeckConfigChange> for UndoableChange {
fn from(c: UndoableDeckConfigChange) -> Self {
UndoableChange::DeckConfig(c)
}
}
impl From<UndoableTagChange> for UndoableChange {
fn from(c: UndoableTagChange) -> Self {
UndoableChange::Tag(c)
@ -74,3 +85,9 @@ impl From<UndoableConfigChange> for UndoableChange {
UndoableChange::Config(c)
}
}
impl From<UndoableCollectionChange> for UndoableChange {
fn from(c: UndoableCollectionChange) -> Self {
UndoableChange::Collection(c)
}
}

View file

@ -127,6 +127,8 @@ impl UndoManager {
UndoableChange::Revlog(_) => {}
UndoableChange::Queue(_) => {}
UndoableChange::Config(_) => changes.config = true,
UndoableChange::DeckConfig(_) => changes.deck_config = true,
UndoableChange::Collection(_) => changes.collection = true,
}
}