mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12: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.
|
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-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-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-added-from-file = Notes added from file: { $val }
|
||||||
importing-notes-found-in-file = Notes found in 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 }
|
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.
|
// to determine the number of columns.
|
||||||
repeated string column_labels = 5;
|
repeated string column_labels = 5;
|
||||||
oneof deck {
|
oneof deck {
|
||||||
|
// id of an existing deck
|
||||||
int64 deck_id = 6;
|
int64 deck_id = 6;
|
||||||
// One-based. 0 means n/a.
|
// One-based. 0 means n/a.
|
||||||
uint32 deck_column = 7;
|
uint32 deck_column = 7;
|
||||||
|
// name of new deck to be created
|
||||||
|
string deck_name = 17;
|
||||||
}
|
}
|
||||||
oneof notetype {
|
oneof notetype {
|
||||||
// One notetype for all rows with given column mapping.
|
// One notetype for all rows with given column mapping.
|
||||||
|
|
|
@ -61,6 +61,7 @@ impl CsvDeckExt for CsvDeck {
|
||||||
match self {
|
match self {
|
||||||
Self::DeckId(did) => NameOrId::Id(*did),
|
Self::DeckId(did) => NameOrId::Id(*did),
|
||||||
Self::DeckColumn(_) => NameOrId::default(),
|
Self::DeckColumn(_) => NameOrId::default(),
|
||||||
|
Self::DeckName(name) => NameOrId::Name(name.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ impl CsvDeckExt for CsvDeck {
|
||||||
match self {
|
match self {
|
||||||
Self::DeckId(_) => None,
|
Self::DeckId(_) => None,
|
||||||
Self::DeckColumn(column) => Some(*column as usize),
|
Self::DeckColumn(column) => Some(*column as usize),
|
||||||
|
Self::DeckName(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,8 @@ impl Collection {
|
||||||
"deck" => {
|
"deck" => {
|
||||||
if let Ok(Some(did)) = self.deck_id_by_name_or_id(&NameOrId::parse(value)) {
|
if let Ok(Some(did)) = self.deck_id_by_name_or_id(&NameOrId::parse(value)) {
|
||||||
metadata.deck = Some(CsvDeck::DeckId(did.0));
|
metadata.deck = Some(CsvDeck::DeckId(did.0));
|
||||||
|
} else if !value.is_empty() {
|
||||||
|
metadata.deck = Some(CsvDeck::DeckName(value.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"notetype column" => {
|
"notetype column" => {
|
||||||
|
@ -626,6 +628,7 @@ pub(in crate::import_export) mod test {
|
||||||
pub trait CsvMetadataTestExt {
|
pub trait CsvMetadataTestExt {
|
||||||
fn defaults_for_testing() -> Self;
|
fn defaults_for_testing() -> Self;
|
||||||
fn unwrap_deck_id(&self) -> i64;
|
fn unwrap_deck_id(&self) -> i64;
|
||||||
|
fn unwrap_deck_name(&self) -> &str;
|
||||||
fn unwrap_notetype_id(&self) -> i64;
|
fn unwrap_notetype_id(&self) -> i64;
|
||||||
fn unwrap_notetype_map(&self) -> &[u32];
|
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 {
|
fn unwrap_notetype_id(&self) -> i64 {
|
||||||
match self.notetype {
|
match self.notetype {
|
||||||
Some(CsvNotetype::GlobalNotetype(ref nt)) => nt.id,
|
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(),
|
metadata!(col, format!("#deck:{deck_id}\n")).unwrap_deck_id(),
|
||||||
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
|
// 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);
|
assert_eq!(metadata!(col, "\n").unwrap_deck_id(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -726,8 +739,8 @@ pub(in crate::import_export) mod test {
|
||||||
numeric_deck_2_id
|
numeric_deck_2_id
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
metadata!(col, format!("#deck:1234\n")).unwrap_deck_id(),
|
metadata!(col, format!("#deck:1234\n")).unwrap_deck_name(),
|
||||||
1 // default deck
|
"1234"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -147,7 +147,7 @@ impl Duplicate {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeckIdsByNameOrId {
|
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
|
let names: HashMap<UniCase<String>, DeckId> = col
|
||||||
.get_all_normal_deck_names(false)?
|
.get_all_normal_deck_names(false)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -160,6 +160,13 @@ impl DeckIdsByNameOrId {
|
||||||
default: None,
|
default: None,
|
||||||
};
|
};
|
||||||
new.default = new.get(default);
|
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)
|
Ok(new)
|
||||||
}
|
}
|
||||||
|
@ -193,7 +200,7 @@ impl<'a> Context<'a> {
|
||||||
NameOrId::default(),
|
NameOrId::default(),
|
||||||
col.notetype_by_name_or_id(&data.default_notetype)?,
|
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_checksums = ExistingChecksums::new(col, data.match_scope)?;
|
||||||
let existing_guids = col.storage.all_notes_by_guid()?;
|
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 {
|
impl ForeignNote {
|
||||||
pub(crate) fn into_log_note(self) -> LogNote {
|
pub(crate) fn into_log_note(self) -> LogNote {
|
||||||
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 { dupeResolutionChoices, matchScopeChoices } from "./choices";
|
||||||
import type { ImportCsvState } from "./lib";
|
import type { ImportCsvState } from "./lib";
|
||||||
|
import Warning from "../deck-options/Warning.svelte";
|
||||||
|
|
||||||
export let state: ImportCsvState;
|
export let state: ImportCsvState;
|
||||||
|
|
||||||
const metadata = state.metadata;
|
const metadata = state.metadata;
|
||||||
const globalNotetype = state.globalNotetype;
|
const globalNotetype = state.globalNotetype;
|
||||||
const deckId = state.deckId;
|
const deckId = state.deckId;
|
||||||
|
const deckName = state.newDeckName;
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
notetype: {
|
notetype: {
|
||||||
|
@ -64,6 +66,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
modal.show();
|
modal.show();
|
||||||
carousel.to(index);
|
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>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.importingImportOptions()}>
|
<TitledContainer title={tr.importingImportOptions()}>
|
||||||
|
@ -95,13 +113,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</EnumSelectorRow>
|
</EnumSelectorRow>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $deckId !== null}
|
{#if deckName || $deckId}
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$deckId}
|
bind:value={$deckId}
|
||||||
defaultValue={state.defaultDeckId}
|
defaultValue={state.defaultDeckId}
|
||||||
choices={state.deckNameIds.map(({ id, name }) => {
|
{choices}
|
||||||
return { label: name, value: id };
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<SettingTitle
|
<SettingTitle
|
||||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("deck"))}
|
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>
|
</EnumSelectorRow>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Warning warning={newDeckCreationNotice} className="alert-info" />
|
||||||
|
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$metadata.dupeResolution}
|
bind:value={$metadata.dupeResolution}
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
|
|
|
@ -22,8 +22,12 @@ export function getGlobalNotetype(meta: CsvMetadata): CsvMetadata_MappedNotetype
|
||||||
return meta.notetype.case === "globalNotetype" ? meta.notetype.value : null;
|
return meta.notetype.case === "globalNotetype" ? meta.notetype.value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDeckId(meta: CsvMetadata): bigint | null {
|
export function getDeckId(meta: CsvMetadata): bigint {
|
||||||
return meta.deck.case === "deckId" ? meta.deck.value : null;
|
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 {
|
export class ImportCsvState {
|
||||||
|
@ -35,6 +39,7 @@ export class ImportCsvState {
|
||||||
readonly defaultIsHtml: boolean;
|
readonly defaultIsHtml: boolean;
|
||||||
readonly defaultNotetypeId: bigint | null;
|
readonly defaultNotetypeId: bigint | null;
|
||||||
readonly defaultDeckId: bigint | null;
|
readonly defaultDeckId: bigint | null;
|
||||||
|
readonly newDeckName: string | null;
|
||||||
|
|
||||||
readonly metadata: Writable<CsvMetadata>;
|
readonly metadata: Writable<CsvMetadata>;
|
||||||
readonly globalNotetype: Writable<CsvMetadata_MappedNotetype | null>;
|
readonly globalNotetype: Writable<CsvMetadata_MappedNotetype | null>;
|
||||||
|
@ -83,6 +88,7 @@ export class ImportCsvState {
|
||||||
this.defaultIsHtml = metadata.isHtml;
|
this.defaultIsHtml = metadata.isHtml;
|
||||||
this.defaultNotetypeId = this.lastGlobalNotetype?.id || null;
|
this.defaultNotetypeId = this.lastGlobalNotetype?.id || null;
|
||||||
this.defaultDeckId = this.lastDeckId;
|
this.defaultDeckId = this.lastDeckId;
|
||||||
|
this.newDeckName = getDeckName(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
doImport(): Promise<ImportResponse> {
|
doImport(): Promise<ImportResponse> {
|
||||||
|
@ -104,7 +110,7 @@ export class ImportCsvState {
|
||||||
path: this.path,
|
path: this.path,
|
||||||
delimiter: changed.delimiter,
|
delimiter: changed.delimiter,
|
||||||
notetypeId: getGlobalNotetype(changed)?.id,
|
notetypeId: getGlobalNotetype(changed)?.id,
|
||||||
deckId: getDeckId(changed) ?? undefined,
|
deckId: getDeckId(changed) || undefined,
|
||||||
isHtml: changed.isHtml,
|
isHtml: changed.isHtml,
|
||||||
});
|
});
|
||||||
// carry over tags
|
// carry over tags
|
||||||
|
@ -157,7 +163,13 @@ export class ImportCsvState {
|
||||||
this.lastDeckId = deckId;
|
this.lastDeckId = deckId;
|
||||||
if (deckId !== null) {
|
if (deckId !== null) {
|
||||||
this.metadata.update((metadata) => {
|
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;
|
return metadata;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue