import page jiff
This commit is contained in:
2
client
2
client
Submodule client updated: faeca44d40...9d6b35a787
2
server
2
server
Submodule server updated: d8b4bf2eeb...2bea8c151f
@@ -579,6 +579,12 @@ import-choose-scope = Vyberte globální nebo profil
|
|||||||
import-target-tables = Cílová tabulka
|
import-target-tables = Cílová tabulka
|
||||||
import-choose-table = Vyberte tabulku
|
import-choose-table = Vyberte tabulku
|
||||||
import-csv-file = Soubor CSV
|
import-csv-file = Soubor CSV
|
||||||
|
import-date-format = Formát data
|
||||||
|
import-date-format-yy-mm-dd = YY-MM-DD (26-08-18)
|
||||||
|
import-date-format-dd-mm-yy = DD-MM-YY (18-08-26)
|
||||||
|
import-date-format-yyyy-mm-dd = YYYY-MM-DD (2026-08-18)
|
||||||
|
import-date-format-dd-mm-yyyy = DD-MM-YYYY (18-08-2026)
|
||||||
|
import-date-format-hint = Používá se pro sloupce DATE. Jiff rozvine hodnoty YY 69–99 na roky 1969–1999 a 00–68 na 2000–2068 a potom každé datum před importem normalizuje.
|
||||||
import-csv-data = Data CSV
|
import-csv-data = Data CSV
|
||||||
import-csv-format-hint = První řádek se obvykle použije jako názvy sloupců souboru. Použijte CSV v UTF-8 s oddělovačem čárka a každé pole uzavřete do dvojitých uvozovek, například: "hl","he","s"
|
import-csv-format-hint = První řádek se obvykle použije jako názvy sloupců souboru. Použijte CSV v UTF-8 s oddělovačem čárka a každé pole uzavřete do dvojitých uvozovek, například: "hl","he","s"
|
||||||
import-continue = Pokračovat
|
import-continue = Pokračovat
|
||||||
@@ -590,6 +596,7 @@ import-failure-message = Před zastavením importu se importovalo { $inserted }
|
|||||||
Již importované řádky zůstávají v tabulce. Aby nevznikly duplicity, neimportujte znovu celý soubor: nejprve tyto řádky odstraňte nebo importujte pouze chybný a zbývající řádky.
|
Již importované řádky zůstávají v tabulce. Aby nevznikly duplicity, neimportujte znovu celý soubor: nejprve tyto řádky odstraňte nebo importujte pouze chybný a zbývající řádky.
|
||||||
import-failure-no-rows-message = Nic se neimportovalo. Selhal řádek CSV { $row }.
|
import-failure-no-rows-message = Nic se neimportovalo. Selhal řádek CSV { $row }.
|
||||||
Backend: { $error }
|
Backend: { $error }
|
||||||
|
import-err-bad-date = Řádek CSV { $row }, sloupec { $column }: „{ $value }“ není platné datum ve formátu { $format }.
|
||||||
import-success-title = Import dokončen
|
import-success-title = Import dokončen
|
||||||
import-success-message = Vloženo { $inserted ->
|
import-success-message = Vloženo { $inserted ->
|
||||||
[one] { $inserted } řádek
|
[one] { $inserted } řádek
|
||||||
|
|||||||
@@ -569,6 +569,12 @@ import-choose-scope = Choose global or a profile
|
|||||||
import-target-tables = Target table
|
import-target-tables = Target table
|
||||||
import-choose-table = Choose a table
|
import-choose-table = Choose a table
|
||||||
import-csv-file = CSV file
|
import-csv-file = CSV file
|
||||||
|
import-date-format = Date format
|
||||||
|
import-date-format-yy-mm-dd = YY-MM-DD (26-08-18)
|
||||||
|
import-date-format-dd-mm-yy = DD-MM-YY (18-08-26)
|
||||||
|
import-date-format-yyyy-mm-dd = YYYY-MM-DD (2026-08-18)
|
||||||
|
import-date-format-dd-mm-yyyy = DD-MM-YYYY (18-08-2026)
|
||||||
|
import-date-format-hint = Used for DATE columns. Jiff expands YY values 69–99 to 1969–1999 and 00–68 to 2000–2068, then normalizes every date before import.
|
||||||
import-csv-data = CSV data
|
import-csv-data = CSV data
|
||||||
import-csv-format-hint = The first row is normally used as the file's column names. Use UTF-8 CSV with a comma separator and every field in double quotes, for example: "hl","he","s"
|
import-csv-format-hint = The first row is normally used as the file's column names. Use UTF-8 CSV with a comma separator and every field in double quotes, for example: "hl","he","s"
|
||||||
import-continue = Continue
|
import-continue = Continue
|
||||||
@@ -580,6 +586,7 @@ import-failure-message = Imported { $inserted } of { $source_rows } rows before
|
|||||||
Rows already imported remain in the table. To avoid duplicates, do not retry the whole file: remove those rows first, or import only the failed and remaining rows.
|
Rows already imported remain in the table. To avoid duplicates, do not retry the whole file: remove those rows first, or import only the failed and remaining rows.
|
||||||
import-failure-no-rows-message = Nothing was imported. CSV row { $row } failed.
|
import-failure-no-rows-message = Nothing was imported. CSV row { $row } failed.
|
||||||
Backend: { $error }
|
Backend: { $error }
|
||||||
|
import-err-bad-date = CSV row { $row }, column { $column }: “{ $value }” is not a valid { $format } date.
|
||||||
import-success-title = Import complete
|
import-success-title = Import complete
|
||||||
import-success-message = Inserted { $inserted ->
|
import-success-message = Inserted { $inserted ->
|
||||||
[one] { $inserted } row
|
[one] { $inserted } row
|
||||||
|
|||||||
@@ -579,6 +579,12 @@ import-choose-scope = Vyberte globálne alebo profil
|
|||||||
import-target-tables = Cieľová tabuľka
|
import-target-tables = Cieľová tabuľka
|
||||||
import-choose-table = Vyberte tabuľku
|
import-choose-table = Vyberte tabuľku
|
||||||
import-csv-file = Súbor CSV
|
import-csv-file = Súbor CSV
|
||||||
|
import-date-format = Formát dátumu
|
||||||
|
import-date-format-yy-mm-dd = YY-MM-DD (26-08-18)
|
||||||
|
import-date-format-dd-mm-yy = DD-MM-YY (18-08-26)
|
||||||
|
import-date-format-yyyy-mm-dd = YYYY-MM-DD (2026-08-18)
|
||||||
|
import-date-format-dd-mm-yyyy = DD-MM-YYYY (18-08-2026)
|
||||||
|
import-date-format-hint = Používa sa pre stĺpce DATE. Jiff rozvinie hodnoty YY 69–99 na roky 1969–1999 a 00–68 na 2000–2068 a potom každý dátum pred importom normalizuje.
|
||||||
import-csv-data = Údaje CSV
|
import-csv-data = Údaje CSV
|
||||||
import-csv-format-hint = Prvý riadok sa zvyčajne použije ako názvy stĺpcov súboru. Použite CSV v UTF-8 s oddeľovačom čiarka a každé pole uzavrite do dvojitých úvodzoviek, napríklad: "hl","he","s"
|
import-csv-format-hint = Prvý riadok sa zvyčajne použije ako názvy stĺpcov súboru. Použite CSV v UTF-8 s oddeľovačom čiarka a každé pole uzavrite do dvojitých úvodzoviek, napríklad: "hl","he","s"
|
||||||
import-continue = Pokračovať
|
import-continue = Pokračovať
|
||||||
@@ -590,6 +596,7 @@ import-failure-message = Pred zastavením importu sa importovalo { $inserted } z
|
|||||||
Už importované riadky zostávajú v tabuľke. Aby nevznikli duplicity, neimportujte znova celý súbor: najprv tieto riadky odstráňte alebo importujte iba chybný a zostávajúce riadky.
|
Už importované riadky zostávajú v tabuľke. Aby nevznikli duplicity, neimportujte znova celý súbor: najprv tieto riadky odstráňte alebo importujte iba chybný a zostávajúce riadky.
|
||||||
import-failure-no-rows-message = Nič sa neimportovalo. Zlyhal riadok CSV { $row }.
|
import-failure-no-rows-message = Nič sa neimportovalo. Zlyhal riadok CSV { $row }.
|
||||||
Backend: { $error }
|
Backend: { $error }
|
||||||
|
import-err-bad-date = Riadok CSV { $row }, stĺpec { $column }: „{ $value }“ nie je platný dátum vo formáte { $format }.
|
||||||
import-success-title = Import dokončený
|
import-success-title = Import dokončený
|
||||||
import-success-message = Vložený { $inserted ->
|
import-success-message = Vložený { $inserted ->
|
||||||
[one] { $inserted } riadok
|
[one] { $inserted } riadok
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::AppState;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
super::common::loader::{LoadError, load_catalog},
|
super::common::loader::{LoadError, load_catalog},
|
||||||
state::{ImportForm, ImportPageState, Step},
|
state::{DateFormat, ImportForm, ImportPageState, Step},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) async fn load_page(
|
pub(crate) async fn load_page(
|
||||||
@@ -14,10 +14,15 @@ pub(crate) async fn load_page(
|
|||||||
step: Step,
|
step: Step,
|
||||||
) -> Result<ImportPageState, LoadError> {
|
) -> Result<ImportPageState, LoadError> {
|
||||||
let catalog = load_catalog(state, headers, crate::authz::IMPORT, None).await?;
|
let catalog = load_catalog(state, headers, crate::authz::IMPORT, None).await?;
|
||||||
|
let date_formats = DateFormat::options(
|
||||||
|
crate::i18n::Locale::from_headers(headers),
|
||||||
|
form.date_format,
|
||||||
|
);
|
||||||
Ok(ImportPageState {
|
Ok(ImportPageState {
|
||||||
nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization),
|
nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization),
|
||||||
catalog,
|
catalog,
|
||||||
form,
|
form,
|
||||||
|
date_formats,
|
||||||
step,
|
step,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ use super::{
|
|||||||
},
|
},
|
||||||
destination::{DestinationColumn, destination_columns, resolve},
|
destination::{DestinationColumn, destination_columns, resolve},
|
||||||
loader::load_page,
|
loader::load_page,
|
||||||
prepare::{Prepared, Source, canonical_csv, prepare, read_mapping, read_source},
|
prepare::{
|
||||||
|
Prepared, Source, canonical_csv, normalize_dates, prepare, read_mapping, read_source,
|
||||||
|
},
|
||||||
state::{ImportForm, MappingRow, MappingStep, PreviewStep, SourceOption, Step},
|
state::{ImportForm, MappingRow, MappingStep, PreviewStep, SourceOption, Step},
|
||||||
ui,
|
ui,
|
||||||
};
|
};
|
||||||
@@ -408,7 +410,14 @@ async fn prepared(
|
|||||||
|
|
||||||
let assignments =
|
let assignments =
|
||||||
read_mapping(locale, &chosen, &source).map_err(|message| reject(headers, message))?;
|
read_mapping(locale, &chosen, &source).map_err(|message| reject(headers, message))?;
|
||||||
let prepared = prepare(&assignments, &source, &destination.names());
|
let mut prepared = prepare(&assignments, &source, &destination.names());
|
||||||
|
normalize_dates(
|
||||||
|
locale,
|
||||||
|
&mut prepared,
|
||||||
|
&destination.types,
|
||||||
|
form.date_format,
|
||||||
|
)
|
||||||
|
.map_err(|message| reject(headers, message))?;
|
||||||
Ok((destination, source, prepared))
|
Ok((destination, source, prepared))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ use std::collections::{HashMap, HashSet};
|
|||||||
|
|
||||||
use crate::{i18n::Locale, tr};
|
use crate::{i18n::Locale, tr};
|
||||||
|
|
||||||
use super::super::common::{
|
use super::{
|
||||||
csv::{parse_csv, write_record},
|
super::common::{
|
||||||
schema::is_system_column,
|
csv::{parse_csv, write_record},
|
||||||
|
schema::is_system_column,
|
||||||
|
},
|
||||||
|
state::DateFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The uploaded file: its header, and its data.
|
/// The uploaded file: its header, and its data.
|
||||||
@@ -234,6 +237,55 @@ pub(crate) fn prepare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses source DATE cells using the order selected for this import and
|
||||||
|
/// rewrites them to Jiff's canonical `YYYY-MM-DD` representation.
|
||||||
|
///
|
||||||
|
/// Doing this on the prepared data keeps the preview, downloaded CSV and bulk
|
||||||
|
/// request identical. A downloaded prepared file can consequently be imported
|
||||||
|
/// later using the default order without carrying hidden page state with it.
|
||||||
|
pub(crate) fn normalize_dates(
|
||||||
|
locale: Locale,
|
||||||
|
prepared: &mut Prepared,
|
||||||
|
column_types: &HashMap<String, String>,
|
||||||
|
date_format: DateFormat,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let date_columns = prepared
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, column)| {
|
||||||
|
column_types
|
||||||
|
.get(*column)
|
||||||
|
.is_some_and(|data_type| data_type.eq_ignore_ascii_case("DATE"))
|
||||||
|
})
|
||||||
|
.map(|(index, column)| (index, column.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (row_index, row) in prepared.rows.iter_mut().enumerate() {
|
||||||
|
for (column_index, column) in &date_columns {
|
||||||
|
let Some(value) = row.get_mut(*column_index) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let raw = value.trim();
|
||||||
|
if raw.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let date = jiff::civil::Date::strptime(date_format.pattern(), raw).map_err(|_| {
|
||||||
|
tr!(
|
||||||
|
locale,
|
||||||
|
"import-err-bad-date",
|
||||||
|
"value" => raw.to_string(),
|
||||||
|
"column" => column.clone(),
|
||||||
|
"row" => (row_index + 2) as i64,
|
||||||
|
"format" => date_format.value().to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
*value = date.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// The prepared import as text: exactly what the importer reads, and exactly
|
/// The prepared import as text: exactly what the importer reads, and exactly
|
||||||
/// what "Download prepared CSV" hands over, so inspecting the file and
|
/// what "Download prepared CSV" hands over, so inspecting the file and
|
||||||
/// importing it cannot disagree.
|
/// importing it cannot disagree.
|
||||||
@@ -344,6 +396,93 @@ mod tests {
|
|||||||
assert_eq!(prepared.empty, strings(&["b"]));
|
assert_eq!(prepared.empty, strings(&["b"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_four_digit_date_format_is_normalized_in_the_prepared_csv() {
|
||||||
|
let mut prepared = Prepared {
|
||||||
|
columns: strings(&["issued_on", "note"]),
|
||||||
|
rows: vec![strings(&["18-08-2026", "keep me"])],
|
||||||
|
ignored: Vec::new(),
|
||||||
|
empty: Vec::new(),
|
||||||
|
};
|
||||||
|
let types = HashMap::from([
|
||||||
|
("issued_on".to_string(), "DATE".to_string()),
|
||||||
|
("note".to_string(), "TEXT".to_string()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
normalize_dates(
|
||||||
|
Locale::English,
|
||||||
|
&mut prepared,
|
||||||
|
&types,
|
||||||
|
DateFormat::DayMonthYearFourDigit,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(prepared.rows[0], strings(&["2026-08-18", "keep me"]));
|
||||||
|
assert_eq!(
|
||||||
|
canonical_csv(&prepared),
|
||||||
|
"\"issued_on\",\"note\"\n\"2026-08-18\",\"keep me\"\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn both_two_digit_year_formats_are_normalized_by_jiff() {
|
||||||
|
let types = HashMap::from([("issued_on".to_string(), "DATE".to_string())]);
|
||||||
|
let mut year_first = Prepared {
|
||||||
|
columns: strings(&["issued_on"]),
|
||||||
|
rows: vec![strings(&["26-08-18"])],
|
||||||
|
ignored: Vec::new(),
|
||||||
|
empty: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut day_first = Prepared {
|
||||||
|
columns: strings(&["issued_on"]),
|
||||||
|
rows: vec![strings(&["18-08-26"]), strings(&["01-01-69"])],
|
||||||
|
ignored: Vec::new(),
|
||||||
|
empty: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
normalize_dates(
|
||||||
|
Locale::English,
|
||||||
|
&mut year_first,
|
||||||
|
&types,
|
||||||
|
DateFormat::YearMonthDayTwoDigit,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
normalize_dates(
|
||||||
|
Locale::English,
|
||||||
|
&mut day_first,
|
||||||
|
&types,
|
||||||
|
DateFormat::DayMonthYearTwoDigit,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(year_first.rows[0], strings(&["2026-08-18"]));
|
||||||
|
assert_eq!(day_first.rows[0], strings(&["2026-08-18"]));
|
||||||
|
assert_eq!(day_first.rows[1], strings(&["1969-01-01"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_date_that_does_not_match_the_selected_format_is_refused() {
|
||||||
|
let mut prepared = Prepared {
|
||||||
|
columns: strings(&["issued_on"]),
|
||||||
|
rows: vec![strings(&["2026-08-18"])],
|
||||||
|
ignored: Vec::new(),
|
||||||
|
empty: Vec::new(),
|
||||||
|
};
|
||||||
|
let types = HashMap::from([("issued_on".to_string(), "DATE".to_string())]);
|
||||||
|
|
||||||
|
let error = normalize_dates(
|
||||||
|
Locale::English,
|
||||||
|
&mut prepared,
|
||||||
|
&types,
|
||||||
|
DateFormat::DayMonthYearFourDigit,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.contains("2026-08-18"), "{error}");
|
||||||
|
assert!(error.contains("issued_on"), "{error}");
|
||||||
|
assert!(error.contains('2'), "{error}");
|
||||||
|
}
|
||||||
|
|
||||||
/// `deleted` is not a normal form field. When it is not mapped, leaving it
|
/// `deleted` is not a normal form field. When it is not mapped, leaving it
|
||||||
/// out lets the server use the same FALSE default as an ordinary form post.
|
/// out lets the server use the same FALSE default as an ordinary form post.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,5 +1,82 @@
|
|||||||
use crate::{i18n::Locale, tr};
|
use crate::{i18n::Locale, tr};
|
||||||
|
|
||||||
|
/// The exact date format used by the source CSV.
|
||||||
|
///
|
||||||
|
/// Keep the accepted spellings, parser pattern and selector options together:
|
||||||
|
/// adding another supported input format should mean adding one variant here,
|
||||||
|
/// rather than teaching each import step about it separately.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
|
||||||
|
pub(crate) enum DateFormat {
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "yyyy-mm-dd")]
|
||||||
|
YearMonthDayFourDigit,
|
||||||
|
#[serde(rename = "dd-mm-yyyy")]
|
||||||
|
DayMonthYearFourDigit,
|
||||||
|
#[serde(rename = "yy-mm-dd")]
|
||||||
|
YearMonthDayTwoDigit,
|
||||||
|
#[serde(rename = "dd-mm-yy")]
|
||||||
|
DayMonthYearTwoDigit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateFormat {
|
||||||
|
const ALL: [Self; 4] = [
|
||||||
|
Self::YearMonthDayTwoDigit,
|
||||||
|
Self::DayMonthYearTwoDigit,
|
||||||
|
Self::YearMonthDayFourDigit,
|
||||||
|
Self::DayMonthYearFourDigit,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) const fn value(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::YearMonthDayFourDigit => "yyyy-mm-dd",
|
||||||
|
Self::DayMonthYearFourDigit => "dd-mm-yyyy",
|
||||||
|
Self::YearMonthDayTwoDigit => "yy-mm-dd",
|
||||||
|
Self::DayMonthYearTwoDigit => "dd-mm-yy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn pattern(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::YearMonthDayFourDigit => "%Y-%m-%d",
|
||||||
|
Self::DayMonthYearFourDigit => "%d-%m-%Y",
|
||||||
|
Self::YearMonthDayTwoDigit => "%y-%m-%d",
|
||||||
|
Self::DayMonthYearTwoDigit => "%d-%m-%y",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(self, locale: Locale) -> String {
|
||||||
|
match self {
|
||||||
|
Self::YearMonthDayFourDigit => tr!(locale, "import-date-format-yyyy-mm-dd"),
|
||||||
|
Self::DayMonthYearFourDigit => tr!(locale, "import-date-format-dd-mm-yyyy"),
|
||||||
|
Self::YearMonthDayTwoDigit => tr!(locale, "import-date-format-yy-mm-dd"),
|
||||||
|
Self::DayMonthYearTwoDigit => tr!(locale, "import-date-format-dd-mm-yy"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn options(locale: Locale, selected: Self) -> Vec<DateFormatOption> {
|
||||||
|
Self::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|format| DateFormatOption {
|
||||||
|
value: format.value(),
|
||||||
|
label: format.label(locale),
|
||||||
|
selected: format == selected,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DateFormat {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter.write_str(self.value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DateFormatOption {
|
||||||
|
pub value: &'static str,
|
||||||
|
pub label: String,
|
||||||
|
pub selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Every field the import form carries, at every step.
|
/// Every field the import form carries, at every step.
|
||||||
///
|
///
|
||||||
/// The whole preparation is one form posted back and forth: the page keeps no
|
/// The whole preparation is one form posted back and forth: the page keeps no
|
||||||
@@ -16,6 +93,9 @@ pub(crate) struct ImportForm {
|
|||||||
/// The uploaded file, header row and all.
|
/// The uploaded file, header row and all.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub csv_data: String,
|
pub csv_data: String,
|
||||||
|
/// The format used to interpret non-empty values mapped into DATE columns.
|
||||||
|
#[serde(default)]
|
||||||
|
pub date_format: DateFormat,
|
||||||
/// One entry per table-column row: the destination's stable identity. See
|
/// One entry per table-column row: the destination's stable identity. See
|
||||||
/// [`DestinationKey`](super::destination::DestinationKey).
|
/// [`DestinationKey`](super::destination::DestinationKey).
|
||||||
///
|
///
|
||||||
@@ -150,6 +230,7 @@ pub(crate) struct ImportPageState {
|
|||||||
pub nav: crate::ui::Nav,
|
pub nav: crate::ui::Nav,
|
||||||
pub catalog: super::super::common::loader::Catalog,
|
pub catalog: super::super::common::loader::Catalog,
|
||||||
pub form: ImportForm,
|
pub form: ImportForm,
|
||||||
|
pub date_formats: Vec<DateFormatOption>,
|
||||||
pub step: Step,
|
pub step: Step,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ mod tests {
|
|||||||
use crate::auth::AuthorizationSnapshot;
|
use crate::auth::AuthorizationSnapshot;
|
||||||
use crate::pages::import_export::common::loader::{Catalog, Profile};
|
use crate::pages::import_export::common::loader::{Catalog, Profile};
|
||||||
|
|
||||||
use super::super::state::{ImportForm, MappingRow, MappingStep, PreviewStep, SourceOption};
|
use super::super::state::{
|
||||||
|
DateFormat, ImportForm, MappingRow, MappingStep, PreviewStep, SourceOption,
|
||||||
|
};
|
||||||
|
|
||||||
fn strings(values: &[&str]) -> Vec<String> {
|
fn strings(values: &[&str]) -> Vec<String> {
|
||||||
values.iter().map(|value| value.to_string()).collect()
|
values.iter().map(|value| value.to_string()).collect()
|
||||||
@@ -124,9 +126,14 @@ mod tests {
|
|||||||
profile_name: "acme".to_string(),
|
profile_name: "acme".to_string(),
|
||||||
table_name: "customers".to_string(),
|
table_name: "customers".to_string(),
|
||||||
csv_data: "\"hl\",\"he\"\n\"value-a\",\"value-b\"\n".to_string(),
|
csv_data: "\"hl\",\"he\"\n\"value-a\",\"value-b\"\n".to_string(),
|
||||||
|
date_format: DateFormat::YearMonthDayFourDigit,
|
||||||
destination: strings(&["id:42", "id:57"]),
|
destination: strings(&["id:42", "id:57"]),
|
||||||
source_position: strings(&["1", "2"]),
|
source_position: strings(&["1", "2"]),
|
||||||
},
|
},
|
||||||
|
date_formats: DateFormat::options(
|
||||||
|
Locale::English,
|
||||||
|
DateFormat::YearMonthDayFourDigit,
|
||||||
|
),
|
||||||
step,
|
step,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,9 +186,26 @@ mod tests {
|
|||||||
assert!(html.contains(r#"action="/admin/import""#), "{html}");
|
assert!(html.contains(r#"action="/admin/import""#), "{html}");
|
||||||
assert!(html.contains(r#"name="table_name""#), "{html}");
|
assert!(html.contains(r#"name="table_name""#), "{html}");
|
||||||
assert!(html.contains(r#"name="csv_data""#), "{html}");
|
assert!(html.contains(r#"name="csv_data""#), "{html}");
|
||||||
|
assert!(html.contains(r#"name="date_format""#), "{html}");
|
||||||
|
for value in ["yy-mm-dd", "dd-mm-yy", "yyyy-mm-dd", "dd-mm-yyyy"] {
|
||||||
|
assert!(html.contains(&format!(r#"value="{value}""#)), "{html}");
|
||||||
|
}
|
||||||
|
assert!(html.contains(r#"value="yyyy-mm-dd" selected"#), "{html}");
|
||||||
assert!(!html.contains(r#"name="header_mode""#), "{html}");
|
assert!(!html.contains(r#"name="header_mode""#), "{html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_date_format_is_carried_through_later_steps() {
|
||||||
|
let mut page = page(mapping());
|
||||||
|
page.form.date_format = DateFormat::DayMonthYearTwoDigit;
|
||||||
|
let html = render_step(&page);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
html.contains(r#"name="date_format" value="dd-mm-yy""#),
|
||||||
|
"{html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The table columns stay fixed while parsed CSV columns can be connected
|
/// The table columns stay fixed while parsed CSV columns can be connected
|
||||||
/// to them or deliberately left unused.
|
/// to them or deliberately left unused.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -10,3 +10,4 @@
|
|||||||
<input type="hidden" name="profile_name" value="{{ page.form.profile_name }}">
|
<input type="hidden" name="profile_name" value="{{ page.form.profile_name }}">
|
||||||
<input type="hidden" name="table_name" value="{{ page.form.table_name }}">
|
<input type="hidden" name="table_name" value="{{ page.form.table_name }}">
|
||||||
<input type="hidden" name="csv_data" value="{{ page.form.csv_data }}">
|
<input type="hidden" name="csv_data" value="{{ page.form.csv_data }}">
|
||||||
|
<input type="hidden" name="date_format" value="{{ page.form.date_format }}">
|
||||||
|
|||||||
@@ -35,6 +35,14 @@
|
|||||||
<input type="file" accept=".csv,text/csv"
|
<input type="file" accept=".csv,text/csv"
|
||||||
onchange="this.files[0]?.text().then(value => document.getElementById('csv-data').value = value)">
|
onchange="this.files[0]?.text().then(value => document.getElementById('csv-data').value = value)">
|
||||||
</label>
|
</label>
|
||||||
|
<label>{{ nav.tr("import-date-format") }}
|
||||||
|
<select name="date_format" required>
|
||||||
|
{% for option in page.date_formats %}
|
||||||
|
<option value="{{ option.value }}"{% if option.selected %} selected{% endif %}>{{ option.label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<small>{{ nav.tr("import-date-format-hint") }}</small>
|
||||||
<label class="wide">{{ nav.tr("import-csv-data") }}<textarea id="csv-data" name="csv_data" rows="12">{{ page.form.csv_data }}</textarea></label>
|
<label class="wide">{{ nav.tr("import-csv-data") }}<textarea id="csv-data" name="csv_data" rows="12">{{ page.form.csv_data }}</textarea></label>
|
||||||
<small class="wide">{{ nav.tr("import-csv-format-hint") }}</small>
|
<small class="wide">{{ nav.tr("import-csv-format-hint") }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user