mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Allow creating deck via #deck:...
if non-existent when importing (#4154)
* add deck name field to metadata protobuf msg * fallback to creating new deck specified in `#deck:...` * update tests * create deck if it doesn't exist * plumbing * allow creating deck via `#deck:...` * apply suggestion for protobuf
This commit is contained in:
parent
037dfa1bc1
commit
80ff9a120c
8 changed files with 78 additions and 13 deletions
|
@ -48,6 +48,7 @@ importing-merge-notetypes-help =
|
|||
Warning: This will require a one-way sync, and may mark existing notes as modified.
|
||||
importing-mnemosyne-20-deck-db = Mnemosyne 2.0 Deck (*.db)
|
||||
importing-multicharacter-separators-are-not-supported-please = Multi-character separators are not supported. Please enter one character only.
|
||||
importing-new-deck-will-be-created = A new deck will be created: { $name }
|
||||
importing-notes-added-from-file = Notes added from file: { $val }
|
||||
importing-notes-found-in-file = Notes found in file: { $val }
|
||||
importing-notes-skipped-as-theyre-already-in = Notes skipped, as up-to-date copies are already in your collection: { $val }
|
||||
|
|
|
@ -176,9 +176,12 @@ message CsvMetadata {
|
|||
// to determine the number of columns.
|
||||
repeated string column_labels = 5;
|
||||
oneof deck {
|
||||
// id of an existing deck
|
||||
int64 deck_id = 6;
|
||||
// One-based. 0 means n/a.
|
||||
uint32 deck_column = 7;
|
||||
// name of new deck to be created
|
||||
string deck_name = 17;
|
||||
}
|
||||
oneof notetype {
|
||||
// One notetype for all rows with given column mapping.
|
||||
|
|
|
@ -61,6 +61,7 @@ impl CsvDeckExt for CsvDeck {
|
|||
match self {
|
||||
Self::DeckId(did) => NameOrId::Id(*did),
|
||||
Self::DeckColumn(_) => NameOrId::default(),
|
||||
Self::DeckName(name) => NameOrId::Name(name.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +69,7 @@ impl CsvDeckExt for CsvDeck {
|
|||
match self {
|
||||
Self::DeckId(_) => None,
|
||||
Self::DeckColumn(column) => Some(*column as usize),
|
||||
Self::DeckName(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,6 +163,8 @@ impl Collection {
|
|||
"deck" => {
|
||||
if let Ok(Some(did)) = self.deck_id_by_name_or_id(&NameOrId::parse(value)) {
|
||||
metadata.deck = Some(CsvDeck::DeckId(did.0));
|
||||
} else if !value.is_empty() {
|
||||
metadata.deck = Some(CsvDeck::DeckName(value.to_string()));
|
||||
}
|
||||
}
|
||||
"notetype column" => {
|
||||
|
@ -626,6 +628,7 @@ pub(in crate::import_export) mod test {
|
|||
pub trait CsvMetadataTestExt {
|
||||
fn defaults_for_testing() -> Self;
|
||||
fn unwrap_deck_id(&self) -> i64;
|
||||
fn unwrap_deck_name(&self) -> &str;
|
||||
fn unwrap_notetype_id(&self) -> i64;
|
||||
fn unwrap_notetype_map(&self) -> &[u32];
|
||||
}
|
||||
|
@ -660,6 +663,13 @@ pub(in crate::import_export) mod test {
|
|||
}
|
||||
}
|
||||
|
||||
fn unwrap_deck_name(&self) -> &str {
|
||||
match &self.deck {
|
||||
Some(CsvDeck::DeckName(name)) => name,
|
||||
_ => panic!("no deck name"),
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_notetype_id(&self) -> i64 {
|
||||
match self.notetype {
|
||||
Some(CsvNotetype::GlobalNotetype(ref nt)) => nt.id,
|
||||
|
@ -683,8 +693,11 @@ pub(in crate::import_export) mod test {
|
|||
metadata!(col, format!("#deck:{deck_id}\n")).unwrap_deck_id(),
|
||||
deck_id
|
||||
);
|
||||
// unknown deck
|
||||
assert_eq!(metadata!(col, "#deck:foo\n").unwrap_deck_name(), "foo");
|
||||
assert_eq!(metadata!(col, "#deck:1234\n").unwrap_deck_name(), "1234");
|
||||
// fallback
|
||||
assert_eq!(metadata!(col, "#deck:foo\n").unwrap_deck_id(), 1);
|
||||
assert_eq!(metadata!(col, "#deck:\n").unwrap_deck_id(), 1);
|
||||
assert_eq!(metadata!(col, "\n").unwrap_deck_id(), 1);
|
||||
}
|
||||
|
||||
|
@ -726,8 +739,8 @@ pub(in crate::import_export) mod test {
|
|||
numeric_deck_2_id
|
||||
);
|
||||
assert_eq!(
|
||||
metadata!(col, format!("#deck:1234\n")).unwrap_deck_id(),
|
||||
1 // default deck
|
||||
metadata!(col, format!("#deck:1234\n")).unwrap_deck_name(),
|
||||
"1234"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ impl Duplicate {
|
|||
}
|
||||
|
||||
impl DeckIdsByNameOrId {
|
||||
fn new(col: &mut Collection, default: &NameOrId) -> Result<Self> {
|
||||
fn new(col: &mut Collection, default: &NameOrId, usn: Usn) -> Result<Self> {
|
||||
let names: HashMap<UniCase<String>, DeckId> = col
|
||||
.get_all_normal_deck_names(false)?
|
||||
.into_iter()
|
||||
|
@ -160,6 +160,13 @@ impl DeckIdsByNameOrId {
|
|||
default: None,
|
||||
};
|
||||
new.default = new.get(default);
|
||||
if new.default.is_none() && *default != NameOrId::default() {
|
||||
let mut deck = Deck::new_normal();
|
||||
deck.name = NativeDeckName::from_human_name(default.to_string());
|
||||
col.add_deck_inner(&mut deck, usn)?;
|
||||
new.insert(deck.id, deck.human_name());
|
||||
new.default = Some(deck.id);
|
||||
}
|
||||
|
||||
Ok(new)
|
||||
}
|
||||
|
@ -193,7 +200,7 @@ impl<'a> Context<'a> {
|
|||
NameOrId::default(),
|
||||
col.notetype_by_name_or_id(&data.default_notetype)?,
|
||||
);
|
||||
let deck_ids = DeckIdsByNameOrId::new(col, &data.default_deck)?;
|
||||
let deck_ids = DeckIdsByNameOrId::new(col, &data.default_deck, usn)?;
|
||||
let existing_checksums = ExistingChecksums::new(col, data.match_scope)?;
|
||||
let existing_guids = col.storage.all_notes_by_guid()?;
|
||||
|
||||
|
|
|
@ -83,6 +83,15 @@ impl From<String> for NameOrId {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NameOrId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NameOrId::Id(did) => write!(f, "{did}"),
|
||||
NameOrId::Name(name) => write!(f, "{name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignNote {
|
||||
pub(crate) fn into_log_note(self) -> LogNote {
|
||||
LogNote {
|
||||
|
|
|
@ -17,12 +17,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import { dupeResolutionChoices, matchScopeChoices } from "./choices";
|
||||
import type { ImportCsvState } from "./lib";
|
||||
import Warning from "../deck-options/Warning.svelte";
|
||||
|
||||
export let state: ImportCsvState;
|
||||
|
||||
const metadata = state.metadata;
|
||||
const globalNotetype = state.globalNotetype;
|
||||
const deckId = state.deckId;
|
||||
const deckName = state.newDeckName;
|
||||
|
||||
const settings = {
|
||||
notetype: {
|
||||
|
@ -64,6 +66,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
modal.show();
|
||||
carousel.to(index);
|
||||
}
|
||||
|
||||
const choices = state.deckNameIds.map(({ id, name }) => {
|
||||
return { label: name, value: id };
|
||||
});
|
||||
|
||||
if (deckName) {
|
||||
choices.push({
|
||||
label: deckName,
|
||||
value: 0n,
|
||||
});
|
||||
}
|
||||
|
||||
$: newDeckCreationNotice =
|
||||
deckName && $deckId === 0n
|
||||
? tr.importingNewDeckWillBeCreated({ name: deckName })
|
||||
: "";
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.importingImportOptions()}>
|
||||
|
@ -95,13 +113,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</EnumSelectorRow>
|
||||
{/if}
|
||||
|
||||
{#if $deckId !== null}
|
||||
{#if deckName || $deckId}
|
||||
<EnumSelectorRow
|
||||
bind:value={$deckId}
|
||||
defaultValue={state.defaultDeckId}
|
||||
choices={state.deckNameIds.map(({ id, name }) => {
|
||||
return { label: name, value: id };
|
||||
})}
|
||||
{choices}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("deck"))}
|
||||
|
@ -111,6 +127,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</EnumSelectorRow>
|
||||
{/if}
|
||||
|
||||
<Warning warning={newDeckCreationNotice} className="alert-info" />
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={$metadata.dupeResolution}
|
||||
defaultValue={0}
|
||||
|
|
|
@ -22,8 +22,12 @@ export function getGlobalNotetype(meta: CsvMetadata): CsvMetadata_MappedNotetype
|
|||
return meta.notetype.case === "globalNotetype" ? meta.notetype.value : null;
|
||||
}
|
||||
|
||||
export function getDeckId(meta: CsvMetadata): bigint | null {
|
||||
return meta.deck.case === "deckId" ? meta.deck.value : null;
|
||||
export function getDeckId(meta: CsvMetadata): bigint {
|
||||
return meta.deck.case === "deckId" ? meta.deck.value : 0n;
|
||||
}
|
||||
|
||||
export function getDeckName(meta: CsvMetadata): string | null {
|
||||
return meta.deck.case === "deckName" ? meta.deck.value : null;
|
||||
}
|
||||
|
||||
export class ImportCsvState {
|
||||
|
@ -35,6 +39,7 @@ export class ImportCsvState {
|
|||
readonly defaultIsHtml: boolean;
|
||||
readonly defaultNotetypeId: bigint | null;
|
||||
readonly defaultDeckId: bigint | null;
|
||||
readonly newDeckName: string | null;
|
||||
|
||||
readonly metadata: Writable<CsvMetadata>;
|
||||
readonly globalNotetype: Writable<CsvMetadata_MappedNotetype | null>;
|
||||
|
@ -83,6 +88,7 @@ export class ImportCsvState {
|
|||
this.defaultIsHtml = metadata.isHtml;
|
||||
this.defaultNotetypeId = this.lastGlobalNotetype?.id || null;
|
||||
this.defaultDeckId = this.lastDeckId;
|
||||
this.newDeckName = getDeckName(metadata);
|
||||
}
|
||||
|
||||
doImport(): Promise<ImportResponse> {
|
||||
|
@ -104,7 +110,7 @@ export class ImportCsvState {
|
|||
path: this.path,
|
||||
delimiter: changed.delimiter,
|
||||
notetypeId: getGlobalNotetype(changed)?.id,
|
||||
deckId: getDeckId(changed) ?? undefined,
|
||||
deckId: getDeckId(changed) || undefined,
|
||||
isHtml: changed.isHtml,
|
||||
});
|
||||
// carry over tags
|
||||
|
@ -157,7 +163,13 @@ export class ImportCsvState {
|
|||
this.lastDeckId = deckId;
|
||||
if (deckId !== null) {
|
||||
this.metadata.update((metadata) => {
|
||||
metadata.deck.value = deckId;
|
||||
if (deckId !== 0n) {
|
||||
metadata.deck.case = "deckId";
|
||||
metadata.deck.value = deckId;
|
||||
} else {
|
||||
metadata.deck.case = "deckName";
|
||||
metadata.deck.value = this.newDeckName!;
|
||||
}
|
||||
return metadata;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue