undo support for bulk tag add/remove
This commit is contained in:
parent
49a1970399
commit
bec77fd420
@ -6,6 +6,10 @@ undo-undo-action = Undo { $val }
|
|||||||
undo-action-undone = { $action } undone
|
undo-action-undone = { $action } undone
|
||||||
undo-redo-action = Redo { $action }
|
undo-redo-action = Redo { $action }
|
||||||
undo-action-redone = { $action } redone
|
undo-action-redone = { $action } redone
|
||||||
|
|
||||||
|
## Action that can be undone
|
||||||
|
|
||||||
undo-answer-card = Answer Card
|
undo-answer-card = Answer Card
|
||||||
undo-unbury-unsuspend = Unbury/Unsuspend
|
undo-unbury-unsuspend = Unbury/Unsuspend
|
||||||
undo-add-note = Add Note
|
undo-add-note = Add Note
|
||||||
|
undo-update-tag = Update Tag
|
||||||
|
@ -88,6 +88,9 @@ class TagManager:
|
|||||||
nids=nids, tags=tags, replacement=replacement, regex=regex
|
nids=nids, tags=tags, replacement=replacement, regex=regex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def bulk_remove(self, nids: Sequence[int], tags: str) -> int:
|
||||||
|
return self.bulk_update(nids, tags, "", False)
|
||||||
|
|
||||||
def rename(self, old: str, new: str) -> int:
|
def rename(self, old: str, new: str) -> int:
|
||||||
"Rename provided tag, returning number of changed notes."
|
"Rename provided tag, returning number of changed notes."
|
||||||
nids = self.col.find_notes(anki.collection.SearchNode(tag=old))
|
nids = self.col.find_notes(anki.collection.SearchNode(tag=old))
|
||||||
|
@ -104,13 +104,13 @@ def test_addDelTags():
|
|||||||
note2["Front"] = "2"
|
note2["Front"] = "2"
|
||||||
col.addNote(note2)
|
col.addNote(note2)
|
||||||
# adding for a given id
|
# adding for a given id
|
||||||
col.tags.bulkAdd([note.id], "foo")
|
col.tags.bulk_add([note.id], "foo")
|
||||||
note.load()
|
note.load()
|
||||||
note2.load()
|
note2.load()
|
||||||
assert "foo" in note.tags
|
assert "foo" in note.tags
|
||||||
assert "foo" not in note2.tags
|
assert "foo" not in note2.tags
|
||||||
# should be canonified
|
# should be canonified
|
||||||
col.tags.bulkAdd([note.id], "foo aaa")
|
col.tags.bulk_add([note.id], "foo aaa")
|
||||||
note.load()
|
note.load()
|
||||||
assert note.tags[0] == "aaa"
|
assert note.tags[0] == "aaa"
|
||||||
assert len(note.tags) == 2
|
assert len(note.tags) == 2
|
||||||
|
@ -61,7 +61,7 @@ def test_findCards():
|
|||||||
assert len(col.findCards("tag:monkey")) == 1
|
assert len(col.findCards("tag:monkey")) == 1
|
||||||
assert len(col.findCards("tag:sheep -tag:monkey")) == 1
|
assert len(col.findCards("tag:sheep -tag:monkey")) == 1
|
||||||
assert len(col.findCards("-tag:sheep")) == 4
|
assert len(col.findCards("-tag:sheep")) == 4
|
||||||
col.tags.bulkAdd(col.db.list("select id from notes"), "foo bar")
|
col.tags.bulk_add(col.db.list("select id from notes"), "foo bar")
|
||||||
assert len(col.findCards("tag:foo")) == len(col.findCards("tag:bar")) == 5
|
assert len(col.findCards("tag:foo")) == len(col.findCards("tag:bar")) == 5
|
||||||
col.tags.bulkRem(col.db.list("select id from notes"), "foo")
|
col.tags.bulkRem(col.db.list("select id from notes"), "foo")
|
||||||
assert len(col.findCards("tag:foo")) == 0
|
assert len(col.findCards("tag:foo")) == 0
|
||||||
|
@ -490,8 +490,11 @@ class Browser(QMainWindow):
|
|||||||
f.actionCreateFilteredDeck.setShortcuts(["Ctrl+G", "Ctrl+Alt+G"])
|
f.actionCreateFilteredDeck.setShortcuts(["Ctrl+G", "Ctrl+Alt+G"])
|
||||||
# notes
|
# notes
|
||||||
qconnect(f.actionAdd.triggered, self.mw.onAddCard)
|
qconnect(f.actionAdd.triggered, self.mw.onAddCard)
|
||||||
qconnect(f.actionAdd_Tags.triggered, lambda: self.addTags())
|
qconnect(f.actionAdd_Tags.triggered, lambda: self.add_tags_to_selected_notes())
|
||||||
qconnect(f.actionRemove_Tags.triggered, lambda: self.deleteTags())
|
qconnect(
|
||||||
|
f.actionRemove_Tags.triggered,
|
||||||
|
lambda: self.remove_tags_from_selected_notes(),
|
||||||
|
)
|
||||||
qconnect(f.actionClear_Unused_Tags.triggered, self.clearUnusedTags)
|
qconnect(f.actionClear_Unused_Tags.triggered, self.clearUnusedTags)
|
||||||
qconnect(f.actionToggle_Mark.triggered, lambda: self.onMark())
|
qconnect(f.actionToggle_Mark.triggered, lambda: self.onMark())
|
||||||
qconnect(f.actionChangeModel.triggered, self.onChangeModel)
|
qconnect(f.actionChangeModel.triggered, self.onChangeModel)
|
||||||
@ -1193,53 +1196,46 @@ where id in %s"""
|
|||||||
# Tags
|
# Tags
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def addTags(
|
def add_tags_to_selected_notes(
|
||||||
self,
|
self,
|
||||||
tags: Optional[str] = None,
|
tags: Optional[str] = None,
|
||||||
label: Optional[str] = None,
|
|
||||||
prompt: Optional[str] = None,
|
|
||||||
func: Optional[Callable] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.editor.saveNow(lambda: self._addTags(tags, label, prompt, func))
|
"Shows prompt if tags not provided."
|
||||||
|
self.editor.saveNow(
|
||||||
|
lambda: self._update_tags_of_selected_notes(
|
||||||
|
func=self.col.tags.bulk_add,
|
||||||
|
tags=tags,
|
||||||
|
prompt=tr(TR.BROWSING_ENTER_TAGS_TO_ADD),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _addTags(
|
def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None:
|
||||||
|
"Shows prompt if tags not provided."
|
||||||
|
self.editor.saveNow(
|
||||||
|
lambda: self._update_tags_of_selected_notes(
|
||||||
|
func=self.col.tags.bulk_remove,
|
||||||
|
tags=tags,
|
||||||
|
prompt=tr(TR.BROWSING_ENTER_TAGS_TO_DELETE),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_tags_of_selected_notes(
|
||||||
self,
|
self,
|
||||||
|
func: Callable[[List[int], str], int],
|
||||||
tags: Optional[str],
|
tags: Optional[str],
|
||||||
label: Optional[str],
|
|
||||||
prompt: Optional[str],
|
prompt: Optional[str],
|
||||||
func: Optional[Callable],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if prompt is None:
|
"If tags provided, prompt skipped. If tags not provided, prompt must be."
|
||||||
prompt = tr(TR.BROWSING_ENTER_TAGS_TO_ADD)
|
|
||||||
if tags is None:
|
if tags is None:
|
||||||
(tags, r) = getTag(self, self.col, prompt)
|
(tags, ok) = getTag(self, self.col, prompt)
|
||||||
else:
|
if not ok:
|
||||||
r = True
|
return
|
||||||
if not r:
|
|
||||||
return
|
|
||||||
if func is None:
|
|
||||||
func = self.col.tags.bulkAdd
|
|
||||||
if label is None:
|
|
||||||
label = tr(TR.BROWSING_ADD_TAGS)
|
|
||||||
if label:
|
|
||||||
self.mw.checkpoint(label)
|
|
||||||
self.model.beginReset()
|
self.model.beginReset()
|
||||||
func(self.selectedNotes(), tags)
|
func(self.selectedNotes(), tags)
|
||||||
self.model.endReset()
|
self.model.endReset()
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
||||||
|
|
||||||
def deleteTags(
|
|
||||||
self, tags: Optional[str] = None, label: Optional[str] = None
|
|
||||||
) -> None:
|
|
||||||
if label is None:
|
|
||||||
label = tr(TR.BROWSING_DELETE_TAGS)
|
|
||||||
self.addTags(
|
|
||||||
tags,
|
|
||||||
label,
|
|
||||||
tr(TR.BROWSING_ENTER_TAGS_TO_DELETE),
|
|
||||||
func=self.col.tags.bulkRem,
|
|
||||||
)
|
|
||||||
|
|
||||||
def clearUnusedTags(self) -> None:
|
def clearUnusedTags(self) -> None:
|
||||||
self.editor.saveNow(self._clearUnusedTags)
|
self.editor.saveNow(self._clearUnusedTags)
|
||||||
|
|
||||||
@ -1250,6 +1246,9 @@ where id in %s"""
|
|||||||
|
|
||||||
self.mw.taskman.run_in_background(self.col.tags.registerNotes, on_done)
|
self.mw.taskman.run_in_background(self.col.tags.registerNotes, on_done)
|
||||||
|
|
||||||
|
addTags = add_tags_to_selected_notes
|
||||||
|
deleteTags = remove_tags_from_selected_notes
|
||||||
|
|
||||||
# Suspending
|
# Suspending
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@ -1313,9 +1312,9 @@ where id in %s"""
|
|||||||
if mark is None:
|
if mark is None:
|
||||||
mark = not self.isMarked()
|
mark = not self.isMarked()
|
||||||
if mark:
|
if mark:
|
||||||
self.addTags(tags="marked")
|
self.add_tags_to_selected_notes(tags="marked")
|
||||||
else:
|
else:
|
||||||
self.deleteTags(tags="marked")
|
self.remove_tags_from_selected_notes(tags="marked")
|
||||||
|
|
||||||
def isMarked(self) -> bool:
|
def isMarked(self) -> bool:
|
||||||
return bool(self.card and self.card.note().hasTag("Marked"))
|
return bool(self.card and self.card.note().hasTag("Marked"))
|
||||||
@ -1643,7 +1642,7 @@ where id in %s"""
|
|||||||
nids = set()
|
nids = set()
|
||||||
for _, nidlist in res:
|
for _, nidlist in res:
|
||||||
nids.update(nidlist)
|
nids.update(nidlist)
|
||||||
self.col.tags.bulkAdd(list(nids), tr(TR.BROWSING_DUPLICATE))
|
self.col.tags.bulk_add(list(nids), tr(TR.BROWSING_DUPLICATE))
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
self.model.endReset()
|
self.model.endReset()
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self)
|
||||||
|
@ -1087,7 +1087,7 @@ impl BackendService for Backend {
|
|||||||
|
|
||||||
fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> BackendResult<pb::UInt32> {
|
fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> BackendResult<pb::UInt32> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.add_tags_for_notes(&to_nids(input.nids), &input.tags)
|
col.add_tags_to_notes(&to_nids(input.nids), &input.tags)
|
||||||
.map(|n| n as u32)
|
.map(|n| n as u32)
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
|
@ -6,6 +6,7 @@ use crate::{
|
|||||||
collection::Collection,
|
collection::Collection,
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, Result},
|
||||||
notes::{NoteID, TransformNoteOutput},
|
notes::{NoteID, TransformNoteOutput},
|
||||||
|
prelude::*,
|
||||||
text::{normalize_to_nfc, to_re},
|
text::{normalize_to_nfc, to_re},
|
||||||
types::Usn,
|
types::Usn,
|
||||||
undo::Undo,
|
undo::Undo,
|
||||||
@ -320,7 +321,7 @@ impl Collection {
|
|||||||
tags: &[Regex],
|
tags: &[Regex],
|
||||||
mut repl: R,
|
mut repl: R,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
self.transact(None, |col| {
|
self.transact(Some(UndoableOp::UpdateTag), |col| {
|
||||||
col.transform_notes(nids, |note, _nt| {
|
col.transform_notes(nids, |note, _nt| {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for re in tags {
|
for re in tags {
|
||||||
@ -362,7 +363,7 @@ impl Collection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_tags_for_notes(&mut self, nids: &[NoteID], tags: &str) -> Result<usize> {
|
pub fn add_tags_to_notes(&mut self, nids: &[NoteID], tags: &str) -> Result<usize> {
|
||||||
let tags: Vec<_> = split_tags(tags).collect();
|
let tags: Vec<_> = split_tags(tags).collect();
|
||||||
let matcher = regex::RegexSet::new(
|
let matcher = regex::RegexSet::new(
|
||||||
tags.iter()
|
tags.iter()
|
||||||
@ -371,7 +372,7 @@ impl Collection {
|
|||||||
)
|
)
|
||||||
.map_err(|_| AnkiError::invalid_input("invalid regex"))?;
|
.map_err(|_| AnkiError::invalid_input("invalid regex"))?;
|
||||||
|
|
||||||
self.transact(None, |col| {
|
self.transact(Some(UndoableOp::UpdateTag), |col| {
|
||||||
col.transform_notes(nids, |note, _nt| {
|
col.transform_notes(nids, |note, _nt| {
|
||||||
let mut need_to_add = true;
|
let mut need_to_add = true;
|
||||||
let mut match_count = 0;
|
let mut match_count = 0;
|
||||||
@ -575,13 +576,13 @@ mod test {
|
|||||||
let note = col.storage.get_note(note.id)?.unwrap();
|
let note = col.storage.get_note(note.id)?.unwrap();
|
||||||
assert_eq!(note.tags[0], "baz");
|
assert_eq!(note.tags[0], "baz");
|
||||||
|
|
||||||
let cnt = col.add_tags_for_notes(&[note.id], "cee aye")?;
|
let cnt = col.add_tags_to_notes(&[note.id], "cee aye")?;
|
||||||
assert_eq!(cnt, 1);
|
assert_eq!(cnt, 1);
|
||||||
let note = col.storage.get_note(note.id)?.unwrap();
|
let note = col.storage.get_note(note.id)?.unwrap();
|
||||||
assert_eq!(¬e.tags, &["aye", "baz", "cee"]);
|
assert_eq!(¬e.tags, &["aye", "baz", "cee"]);
|
||||||
|
|
||||||
// if all tags already on note, it doesn't get updated
|
// if all tags already on note, it doesn't get updated
|
||||||
let cnt = col.add_tags_for_notes(&[note.id], "cee aye")?;
|
let cnt = col.add_tags_to_notes(&[note.id], "cee aye")?;
|
||||||
assert_eq!(cnt, 0);
|
assert_eq!(cnt, 0);
|
||||||
|
|
||||||
// empty replacement deletes tag
|
// empty replacement deletes tag
|
||||||
|
@ -15,6 +15,7 @@ pub enum UndoableOp {
|
|||||||
UnburyUnsuspend,
|
UnburyUnsuspend,
|
||||||
AddNote,
|
AddNote,
|
||||||
RemoveNote,
|
RemoveNote,
|
||||||
|
UpdateTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UndoableOp {
|
impl UndoableOp {
|
||||||
@ -33,6 +34,7 @@ impl Collection {
|
|||||||
UndoableOp::UnburyUnsuspend => TR::UndoUnburyUnsuspend,
|
UndoableOp::UnburyUnsuspend => TR::UndoUnburyUnsuspend,
|
||||||
UndoableOp::AddNote => TR::UndoAddNote,
|
UndoableOp::AddNote => TR::UndoAddNote,
|
||||||
UndoableOp::RemoveNote => TR::StudyingDeleteNote,
|
UndoableOp::RemoveNote => TR::StudyingDeleteNote,
|
||||||
|
UndoableOp::UpdateTag => TR::UndoUpdateTag,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.i18n.tr(key).to_string()
|
self.i18n.tr(key).to_string()
|
||||||
|
Loading…
Reference in New Issue
Block a user