diff --git a/ftl/core/importing.ftl b/ftl/core/importing.ftl index d1292e16c..b1d5d6ad0 100644 --- a/ftl/core/importing.ftl +++ b/ftl/core/importing.ftl @@ -12,6 +12,7 @@ importing-colon = Colon importing-column = Column { $val } importing-comma = Comma importing-empty-first-field = Empty first field: { $val } +importing-field-delimiter = Field delimiter importing-field-mapping = Field mapping importing-field-of-file-is = Field { $val } of file is: importing-fields-separated-by = Fields separated by: { $val } @@ -38,6 +39,7 @@ importing-notes-that-could-not-be-imported = Notes that could not be imported as importing-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val } importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip) importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz) +importing-pipe = Pipe importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } fields, expected { $expected } importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual. importing-semicolon = Semicolon diff --git a/ftl/core/notetypes.ftl b/ftl/core/notetypes.ftl index 6bcea398b..0b173fe45 100644 --- a/ftl/core/notetypes.ftl +++ b/ftl/core/notetypes.ftl @@ -1,3 +1,5 @@ +notetypes-notetype = Notetype + ## Default field names in newly created note types notetypes-front-field = Front diff --git a/proto/anki/notetypes.proto b/proto/anki/notetypes.proto index 85592cbc0..f245a7466 100644 --- a/proto/anki/notetypes.proto +++ b/proto/anki/notetypes.proto @@ -27,6 +27,7 @@ service NotetypesService { rpc GetChangeNotetypeInfo(GetChangeNotetypeInfoRequest) returns (ChangeNotetypeInfo); rpc ChangeNotetype(ChangeNotetypeRequest) returns (collection.OpChanges); + rpc GetFieldNames(NotetypeId) returns (generic.StringList); } message NotetypeId { diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 2bfe18ebf..a0be82e9e 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -34,7 +34,7 @@ BrowserRow = search_pb2.BrowserRow BrowserColumns = search_pb2.BrowserColumns StripHtmlMode = card_rendering_pb2.StripHtmlRequest ImportLogWithChanges = import_export_pb2.ImportResponse -CsvColumn = import_export_pb2.ImportCsvRequest.CsvColumn +ImportCsvRequest = import_export_pb2.ImportCsvRequest CsvMetadata = import_export_pb2.CsvMetadata Delimiter = import_export_pb2.CsvMetadata.Delimiter @@ -410,23 +410,9 @@ class Collection(DeprecatedNamesMixin): request = import_export_pb2.CsvMetadataRequest(path=path, delimiter=delimiter) return self._backend.get_csv_metadata(request) - def import_csv( - self, - path: str, - deck_id: DeckId, - notetype_id: NotetypeId, - columns: list[CsvColumn], - delimiter: Delimiter.V, - is_html: bool, - ) -> ImportLogWithChanges: - return self._backend.import_csv( - path=path, - deck_id=deck_id, - notetype_id=notetype_id, - delimiter=delimiter, - columns=columns, - is_html=is_html, - ) + def import_csv(self, request: ImportCsvRequest) -> ImportLogWithChanges: + log = self._backend.import_csv_raw(request.SerializeToString()) + return ImportLogWithChanges.FromString(log) def import_json_file(self, path: str) -> ImportLogWithChanges: return self._backend.import_json_file(path) diff --git a/qt/aqt/data/web/pages/BUILD.bazel b/qt/aqt/data/web/pages/BUILD.bazel index 699a0a46a..c957a218a 100644 --- a/qt/aqt/data/web/pages/BUILD.bazel +++ b/qt/aqt/data/web/pages/BUILD.bazel @@ -7,6 +7,7 @@ _pages = [ "change-notetype", "card-info", "fields", + "import-csv", ] [copy_files_into_group( diff --git a/qt/aqt/import_export/import_csv_dialog.py b/qt/aqt/import_export/import_csv_dialog.py new file mode 100644 index 000000000..de7f7a94d --- /dev/null +++ b/qt/aqt/import_export/import_csv_dialog.py @@ -0,0 +1,62 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import aqt +import aqt.deckconf +import aqt.main +import aqt.operations +from anki.collection import ImportCsvRequest +from aqt.qt import * +from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr +from aqt.webview import AnkiWebView + + +class ImportCsvDialog(QDialog): + + TITLE = "csv import" + silentlyClose = True + + def __init__( + self, + mw: aqt.main.AnkiQt, + path: str, + on_accepted: Callable[[ImportCsvRequest], None], + ) -> None: + QDialog.__init__(self, mw) + self.mw = mw + self._on_accepted = on_accepted + self._setup_ui(path) + self.show() + + def _setup_ui(self, path: str) -> None: + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.mw.garbage_collect_on_dialog_finish(self) + self.setMinimumSize(400, 300) + disable_help_button(self) + restoreGeom(self, self.TITLE) + addCloseShortcut(self) + + self.web = AnkiWebView(title=self.TITLE) + self.web.setVisible(False) + self.web.load_ts_page("import-csv") + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.web) + self.setLayout(layout) + + self.web.eval(f"anki.setupImportCsvPage('{path}');") + self.setWindowTitle(tr.decks_import_file()) + + def reject(self) -> None: + self.web.cleanup() + self.web = None + saveGeom(self, self.TITLE) + QDialog.reject(self) + + def do_import(self, data: bytes) -> None: + request = ImportCsvRequest() + request.ParseFromString(data) + self._on_accepted(request) + super().reject() diff --git a/qt/aqt/import_export/import_dialog.py b/qt/aqt/import_export/import_dialog.py deleted file mode 100644 index ff75a5535..000000000 --- a/qt/aqt/import_export/import_dialog.py +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -"""Stopgap / not production-ready.""" - -from __future__ import annotations - -from typing import Optional, Sequence - -import aqt.forms -import aqt.main -from anki.collection import CsvColumn, CsvMetadata, Delimiter -from anki.decks import DeckId -from anki.models import NotetypeDict, NotetypeId -from aqt.import_export.importing import import_progress_update, show_import_log -from aqt.operations import CollectionOp, QueryOp -from aqt.qt import * -from aqt.utils import HelpPage, disable_help_button, getText, openHelp, showWarning, tr - -DELIMITERS = ( - ("\\t", tr.importing_tab(), Delimiter.TAB), - (",", tr.importing_comma(), Delimiter.COMMA), - (" ", tr.studying_space(), Delimiter.SPACE), - (";", tr.importing_semicolon(), Delimiter.SEMICOLON), - (":", tr.importing_colon(), Delimiter.COLON), - ("|", tr.importing_colon(), Delimiter.PIPE), -) - - -class ChangeMap(QDialog): - def __init__(self, mw: aqt.main.AnkiQt, model: dict, current: str) -> None: - QDialog.__init__(self, mw, Qt.WindowType.Window) - self.mw = mw - self.model = model - self.frm = aqt.forms.changemap.Ui_ChangeMap() - self.frm.setupUi(self) - disable_help_button(self) - n = 0 - setCurrent = False - for field in self.model["flds"]: - item = QListWidgetItem(tr.importing_map_to(val=field["name"])) - self.frm.fields.addItem(item) - if current == field["name"]: - setCurrent = True - self.frm.fields.setCurrentRow(n) - n += 1 - self.frm.fields.addItem(QListWidgetItem(tr.importing_map_to_tags())) - self.frm.fields.addItem(QListWidgetItem(tr.importing_ignore_field())) - if not setCurrent: - if current == "_tags": - self.frm.fields.setCurrentRow(n) - else: - self.frm.fields.setCurrentRow(n + 1) - self.field: Optional[str] = None - - def getField(self) -> str: - self.exec() - return self.field - - def accept(self) -> None: - row = self.frm.fields.currentRow() - if row < len(self.model["flds"]): - self.field = self.model["flds"][row]["name"] - elif row == self.frm.fields.count() - 2: - self.field = "_tags" - else: - self.field = None - QDialog.accept(self) - - def reject(self) -> None: - self.accept() - - -# called by importFile() when importing a mappable file like .csv -# ImportType = Union[Importer,AnkiPackageImporter, TextImporter] - - -class ImportDialog(QDialog): - def __init__(self, mw: aqt.main.AnkiQt, path: str) -> None: - QDialog.__init__(self, mw, Qt.WindowType.Window) - self.mw = mw - self.path = path - self.options = CsvMetadata() - QueryOp( - parent=self, - op=lambda col: col.get_csv_metadata(path, None), - success=self._run, - ).run_in_background() - self._setup_ui() - - def _setup_ui(self) -> None: - self.frm = aqt.forms.importing.Ui_ImportDialog() - self.frm.setupUi(self) - qconnect( - self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help).clicked, - self.helpRequested, - ) - disable_help_button(self) - self.setupMappingFrame() - qconnect(self.frm.autoDetect.clicked, self.onDelimiter) - qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged) - # import button - b = QPushButton(tr.actions_import()) - self.frm.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.AcceptRole) - - def _run(self, options: CsvMetadata) -> None: - self._setup_options(options) - self._setup_choosers() - self.column_map = ColumnMap(self.columns, self.model) - self._render_mapping() - self._set_delimiter() - self.frm.allowHTML.setChecked(self.is_html) - self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get("importMode", 1)) - self.frm.tagModified.setText(self.tags) - self.frm.tagModified.setCol(self.mw.col) - self.show() - - def _setup_options(self, options: CsvMetadata) -> None: - self.delimiter = options.delimiter - self.tags = self.options.tags or self.mw.pm.profile.get("tagModified", "") - self.columns = options.columns - self.deck_id = DeckId( - self.options.deck_id or self.mw.col.get_config("curDeck", default=1) - ) - if options.notetype_id: - self.notetype_id = NotetypeId(self.options.notetype_id) - self.model = self.mw.col.models.get(self.notetype_id) - else: - self.model = self.mw.col.models.current() - self.notetype_id = self.model["id"] - if self.options.is_html is None: - self.is_html = self.mw.pm.profile.get("allowHTML", True) - else: - self.is_html = self.options.is_html - - def _setup_choosers(self) -> None: - import aqt.deckchooser - import aqt.notetypechooser - - def change_notetype(ntid: NotetypeId) -> None: - self.model = self.mw.col.models.get(ntid) - self.notetype_id = ntid - self.column_map = ColumnMap(self.columns, self.model) - self._render_mapping() - - self.modelChooser = aqt.notetypechooser.NotetypeChooser( - mw=self.mw, - widget=self.frm.modelArea, - starting_notetype_id=self.notetype_id, - on_notetype_changed=change_notetype, - ) - self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False) - - def _set_delimiter(self) -> None: - for delimiter in DELIMITERS: - if delimiter[2] == self.delimiter: - txt = tr.importing_fields_separated_by(val=delimiter[1]) - self.frm.autoDetect.setText(txt) - return - - def onDelimiter(self) -> None: - # Open a modal dialog to enter an delimiter - # Todo/Idea Constrain the maximum width, so it doesnt take up that much screen space - delim, ok = getText( - tr.importing_by_default_anki_will_detect_the(), - self, - help=HelpPage.IMPORTING, - ) - - if not ok: - return - # Check if the entered value is valid and if not fallback to default - - txt = "" - for delimiter in DELIMITERS: - if delimiter[0] == delim: - txt = tr.importing_fields_separated_by(val=delimiter[1]) - self.delimiter = delimiter[2] - break - if not txt: - showWarning( - tr.importing_multicharacter_separators_are_not_supported_please() - ) - return - - self.frm.autoDetect.setText(txt) - - def _update_columns(options: CsvMetadata) -> None: - self.columns = options.columns - self.column_map = ColumnMap(self.columns, self.model) - self._render_mapping() - - QueryOp( - parent=self, - op=lambda col: col.get_csv_metadata(self.path, self.delimiter), - success=_update_columns, - ).run_in_background() - - def accept(self) -> None: - # self.mw.pm.profile["importMode"] = self.importer.importMode - self.mw.pm.profile["allowHTML"] = self.frm.allowHTML.isChecked() - # self.mw.pm.profile["tagModified"] = self.importer.tagModified - self.mw.col.set_aux_notetype_config( - self.model["id"], "lastDeck", self.deck.selected_deck_id - ) - self.close() - - CollectionOp( - parent=self.mw, - op=lambda col: col.import_csv( - path=self.path, - deck_id=self.deck.selected_deck_id, - notetype_id=self.model["id"], - delimiter=self.delimiter, - columns=self.column_map.csv_columns(), - is_html=self.frm.allowHTML.isChecked(), - ), - ).with_backend_progress(import_progress_update).success( - show_import_log - ).run_in_background() - - def setupMappingFrame(self) -> None: - # qt seems to have a bug with adding/removing from a grid, so we add - # to a separate object and add/remove that instead - self.frame = QFrame(self.frm.mappingArea) - self.frm.mappingArea.setWidget(self.frame) - self.mapbox = QVBoxLayout(self.frame) - self.mapbox.setContentsMargins(0, 0, 0, 0) - self.mapwidget: Optional[QWidget] = None - - def hideMapping(self) -> None: - self.frm.mappingGroup.hide() - - def _render_mapping(self) -> None: - # set up the mapping grid - if self.mapwidget: - self.mapbox.removeWidget(self.mapwidget) - self.mapwidget.deleteLater() - self.mapwidget = QWidget() - self.mapbox.addWidget(self.mapwidget) - self.grid = QGridLayout(self.mapwidget) - self.mapwidget.setLayout(self.grid) - self.grid.setContentsMargins(3, 3, 3, 3) - self.grid.setSpacing(6) - for (num, column) in enumerate(self.column_map.columns): - self.grid.addWidget(QLabel(column), num, 0) - self.grid.addWidget(QLabel(self.column_map.map_label(num)), num, 1) - button = QPushButton(tr.importing_change()) - self.grid.addWidget(button, num, 2) - qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n)) - - def changeMappingNum(self, n: int) -> None: - f = ChangeMap(self.mw, self.model, self.column_map.map[n]).getField() - self.column_map.update(n, f) - self._render_mapping() - - def reject(self) -> None: - self.modelChooser.cleanup() - self.deck.cleanup() - QDialog.reject(self) - - def helpRequested(self) -> None: - openHelp(HelpPage.IMPORTING) - - def importModeChanged(self, newImportMode: int) -> None: - if newImportMode == 0: - self.frm.tagModified.setEnabled(True) - else: - self.frm.tagModified.setEnabled(False) - - -class ColumnMap: - columns: list[str] - fields: list[str] - map: list[str] - - def __init__(self, columns: Sequence[str], notetype: NotetypeDict) -> None: - self.columns = list(columns) - self.fields = [f["name"] for f in notetype["flds"]] + ["_tags"] - self.map = [""] * len(self.columns) - for i in range(min(len(self.fields), len(self.columns))): - self.map[i] = self.fields[i] - - def map_label(self, num: int) -> str: - name = self.map[num] - if not name: - return tr.importing_ignored() - if name == "_tags": - tr.importing_mapped_to_tags() - return tr.importing_mapped_to(val=name) - - def update(self, column: int, new_field: str | None) -> None: - if new_field: - try: - idx = self.map.index(new_field) - except ValueError: - pass - else: - self.map[idx] = "" - self.map[column] = new_field or "" - - def csv_columns(self) -> list[CsvColumn]: - return [self._column_for_name(name) for name in self.map] - - def _column_for_name(self, name: str) -> CsvColumn: - if not name: - return CsvColumn(other=CsvColumn.IGNORE) - if name == "_tags": - return CsvColumn(other=CsvColumn.TAGS) - return CsvColumn(field=self.fields.index(name)) diff --git a/qt/aqt/import_export/importing.py b/qt/aqt/import_export/importing.py index d059e6cbc..39c61ec5d 100644 --- a/qt/aqt/import_export/importing.py +++ b/qt/aqt/import_export/importing.py @@ -8,10 +8,11 @@ from itertools import chain from typing import Type import aqt.main -from anki.collection import Collection, ImportLogWithChanges, Progress +from anki.collection import Collection, ImportCsvRequest, ImportLogWithChanges, Progress from anki.errors import Interrupted from anki.foreign_data import mnemosyne from anki.lang import without_unicode_isolation +from aqt.import_export.import_csv_dialog import ImportCsvDialog from aqt.operations import CollectionOp, QueryOp from aqt.progress import ProgressUpdate from aqt.qt import * @@ -105,9 +106,15 @@ class CsvImporter(Importer): @staticmethod def do_import(mw: aqt.main.AnkiQt, path: str) -> None: - import aqt.import_export.import_dialog + def on_accepted(request: ImportCsvRequest) -> None: + CollectionOp( + parent=mw, + op=lambda col: col.import_csv(request), + ).with_backend_progress(import_progress_update).success( + show_import_log + ).run_in_background() - aqt.import_export.import_dialog.ImportDialog(mw, path) + ImportCsvDialog(mw, path, on_accepted) class JsonImporter(Importer): diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 36e4bdcc6..dba07a44a 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -31,6 +31,7 @@ from anki.scheduler.v3 import NextStates from anki.utils import dev_mode from aqt.changenotetype import ChangeNotetypeDialog from aqt.deckoptions import DeckOptionsDialog +from aqt.import_export.import_csv_dialog import ImportCsvDialog from aqt.operations.deck import update_deck_configs as update_deck_configs_op from aqt.qt import * @@ -438,6 +439,18 @@ def change_notetype() -> bytes: return b"" +def import_csv() -> bytes: + data = request.data + + def handle_on_main() -> None: + window = aqt.mw.app.activeWindow() + if isinstance(window, ImportCsvDialog): + window.do_import(data) + + aqt.mw.taskman.run_on_main(handle_on_main) + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -445,13 +458,19 @@ post_handler_list = [ next_card_states, set_next_card_states, change_notetype, + import_csv, ] exposed_backend_list = [ + # DeckService + "get_deck_names", # I18nService "i18n_resources", + # ImportExportService + "get_csv_metadata", # NotesService + "get_field_names", "get_note", # NotetypesService "get_notetype_names", diff --git a/rslib/src/backend/notetypes.rs b/rslib/src/backend/notetypes.rs index ee8f2938b..d90759377 100644 --- a/rslib/src/backend/notetypes.rs +++ b/rslib/src/backend/notetypes.rs @@ -168,9 +168,15 @@ impl NotetypesService for Backend { .map(Into::into) }) } + fn change_notetype(&self, input: pb::ChangeNotetypeRequest) -> Result { self.with_col(|col| col.change_notetype_of_notes(input.into()).map(Into::into)) } + + fn get_field_names(&self, input: pb::NotetypeId) -> Result { + self.with_col(|col| col.storage.get_field_names(input.into())) + .map(Into::into) + } } impl From for Notetype { diff --git a/rslib/src/import_export/text/csv/metadata.rs b/rslib/src/import_export/text/csv/metadata.rs index 5212dfc3b..b2aace394 100644 --- a/rslib/src/import_export/text/csv/metadata.rs +++ b/rslib/src/import_export/text/csv/metadata.rs @@ -130,9 +130,11 @@ impl Collection { fn column_label(&self, idx: usize, column: &str) -> String { match column.trim() { - "" => self.tr.importing_column(idx + 1).to_string(), - "tags" => self.tr.editing_tags().to_string(), - s => s.to_string(), + "" => self.tr.importing_column(idx + 1).into(), + "tags" => self.tr.editing_tags().into(), + "notetype" => self.tr.notetypes_notetype().into(), + "deck" => self.tr.decks_deck().into(), + s => s.into(), } } diff --git a/rslib/src/storage/notetype/mod.rs b/rslib/src/storage/notetype/mod.rs index 3ada340a8..f998eb382 100644 --- a/rslib/src/storage/notetype/mod.rs +++ b/rslib/src/storage/notetype/mod.rs @@ -374,4 +374,12 @@ impl SqliteStorage { self.db.execute("update col set models = ?", [json])?; Ok(()) } + + pub(crate) fn get_field_names(&self, notetype_id: NotetypeId) -> Result> { + self.db + .prepare_cached("SELECT name FROM fields WHERE ntid = ? ORDER BY ord")? + .query_and_then([notetype_id], |row| Ok(row.get(0)?))? + //.map_err(Into::into) + .collect() + } } diff --git a/ts/import-csv/BUILD.bazel b/ts/import-csv/BUILD.bazel new file mode 100644 index 000000000..0fc054feb --- /dev/null +++ b/ts/import-csv/BUILD.bazel @@ -0,0 +1,87 @@ +load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") +load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check") +load("//ts:esbuild.bzl", "esbuild") +load("//ts:generate_page.bzl", "generate_page") +load("//ts:compile_sass.bzl", "compile_sass") +load("//ts:typescript.bzl", "typescript") +load("//ts:jest.bzl", "jest_test") + +generate_page(page = "import-csv") + +compile_sass( + srcs = ["import-csv-base.scss"], + group = "base_css", + visibility = ["//visibility:public"], + deps = [ + "//sass:base_lib", + "//sass:scrollbar_lib", + "//sass/bootstrap", + ], +) + +_ts_deps = [ + "//ts/components", + "//ts/lib", + "//ts/sveltelib", + "@npm//@fluent", + "@npm//@types/jest", + "@npm//lodash-es", + "@npm//svelte", + "@npm//marked", +] + +compile_svelte(deps = _ts_deps) + +typescript( + name = "index", + deps = _ts_deps + [ + ":svelte", + ], +) + +esbuild( + name = "import-csv", + args = { + "globalName": "anki", + "loader": {".svg": "text"}, + }, + entry_point = "index.ts", + output_css = "import-csv.css", + visibility = ["//visibility:public"], + deps = [ + ":base_css", + ":index", + ":svelte", + "@npm//bootstrap-icons", + ], +) + +# Tests +################ + +prettier_test() + +eslint_test() + +svelte_check( + name = "svelte_check", + srcs = glob([ + "*.ts", + "*.svelte", + ]) + [ + "//sass:button_mixins_lib", + "//sass/bootstrap", + "@npm//@types/bootstrap", + "@npm//@types/lodash-es", + "@npm//@types/marked", + "//ts/components", + ], +) + +jest_test( + protobuf = True, + deps = [ + ":index", + ], +) diff --git a/ts/import-csv/DeckSelector.svelte b/ts/import-csv/DeckSelector.svelte new file mode 100644 index 000000000..6638588a1 --- /dev/null +++ b/ts/import-csv/DeckSelector.svelte @@ -0,0 +1,38 @@ + + + + + +
{tr.decksDeck()}
+ + + + + {#each deckNameIds as entry, idx} + + {entry.name} + + {/each} + + + +
diff --git a/ts/import-csv/DelimiterSelector.svelte b/ts/import-csv/DelimiterSelector.svelte new file mode 100644 index 000000000..29980c21d --- /dev/null +++ b/ts/import-csv/DelimiterSelector.svelte @@ -0,0 +1,54 @@ + + + + + +
{tr.importingFieldDelimiter()}
+ + + + + {#each delimiters as delimiterName, idx} + + {delimiterName[1]} + + {/each} + + + +
diff --git a/ts/import-csv/FieldMapper.svelte b/ts/import-csv/FieldMapper.svelte new file mode 100644 index 000000000..41590ac4f --- /dev/null +++ b/ts/import-csv/FieldMapper.svelte @@ -0,0 +1,43 @@ + + + + + + + {#each fieldNames as label, idx} + {#if idx === 0} + + {:else} + + {/if} + {/each} + diff --git a/ts/import-csv/Header.svelte b/ts/import-csv/Header.svelte new file mode 100644 index 000000000..03ea8d456 --- /dev/null +++ b/ts/import-csv/Header.svelte @@ -0,0 +1,21 @@ + + + + +

+ {heading} +

+
+ + diff --git a/ts/import-csv/ImportButton.svelte b/ts/import-csv/ImportButton.svelte new file mode 100644 index 000000000..453ec8162 --- /dev/null +++ b/ts/import-csv/ImportButton.svelte @@ -0,0 +1,33 @@ + + + + + {tr.actionsImport()} + + diff --git a/ts/import-csv/ImportCsvPage.svelte b/ts/import-csv/ImportCsvPage.svelte new file mode 100644 index 000000000..f3df72c71 --- /dev/null +++ b/ts/import-csv/ImportCsvPage.svelte @@ -0,0 +1,90 @@ + + + +
+ + + +
+ + + + + {tr.importingAllowHtmlInFields()} + + + + + +
+ + + + + + +
diff --git a/ts/import-csv/MapperRow.svelte b/ts/import-csv/MapperRow.svelte new file mode 100644 index 000000000..7329abd40 --- /dev/null +++ b/ts/import-csv/MapperRow.svelte @@ -0,0 +1,29 @@ + + + + + + {label} + + + + + + diff --git a/ts/import-csv/MetaMapper.svelte b/ts/import-csv/MetaMapper.svelte new file mode 100644 index 000000000..c6ece82f5 --- /dev/null +++ b/ts/import-csv/MetaMapper.svelte @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/ts/import-csv/NotetypeSelector.svelte b/ts/import-csv/NotetypeSelector.svelte new file mode 100644 index 000000000..6fd555ba8 --- /dev/null +++ b/ts/import-csv/NotetypeSelector.svelte @@ -0,0 +1,41 @@ + + + + + +
{tr.notetypesNotetype()}
+ + + + + {#each notetypeNameIds as entry, idx} + + {entry.name} + + {/each} + + + +
diff --git a/ts/import-csv/import-csv-base.scss b/ts/import-csv/import-csv-base.scss new file mode 100644 index 000000000..4172fdd13 --- /dev/null +++ b/ts/import-csv/import-csv-base.scss @@ -0,0 +1,34 @@ +@use "sass/vars"; +@use "sass/bootstrap-dark"; + +@import "sass/base"; + +@import "sass/bootstrap/scss/alert"; +@import "sass/bootstrap/scss/buttons"; +@import "sass/bootstrap/scss/button-group"; +@import "sass/bootstrap/scss/close"; +@import "sass/bootstrap/scss/grid"; +@import "sass/bootstrap-forms"; + +.night-mode { + @include bootstrap-dark.night-mode; +} + +body { + width: min(100vw, 70em); + margin: 0 auto; +} + +html { + overflow-x: hidden; +} + +#main { + padding: 0.5em 0.5em 1em 0.5em; + height: 100vh; +} + +// override the default down arrow colour in