new form page row + import page dialog fix

This commit is contained in:
Filipriec
2026-08-22 11:10:45 +02:00
parent adaa2c4ac6
commit 2d850b12ca
11 changed files with 278 additions and 31 deletions

View File

@@ -295,10 +295,10 @@ pub(crate) async fn import_progress(
// is nothing to report, and the alert says so rather than the card polling
// a job that will never answer.
let Some(snapshot) = state.imports.snapshot(&id, session) else {
return reject(&headers, tr!(locale, "import-progress-gone"));
return no_store(reject(&headers, tr!(locale, "import-progress-gone")));
};
match &snapshot.outcome {
let response = match &snapshot.outcome {
None => Html(ui::render_progress(locale, &id, &snapshot)).into_response(),
Some(Outcome::Succeeded) => Html(ui::render_success(
locale,
@@ -336,7 +336,20 @@ pub(crate) async fn import_progress(
Html(String::new()),
)
.into_response(),
}
};
no_store(response)
}
/// A browser or an intermediary caching this poll's answer is exactly how "the
/// backend finished" and "what the page shows" come apart: the dialog's whole
/// point is that every second's answer is the server's current answer, never
/// a replay of an earlier one.
fn no_store(mut response: Response) -> Response {
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("no-store"),
);
response
}
/// One spawned import: what the walk through the prepared rows needs, and

View File

@@ -152,23 +152,35 @@ pub(crate) fn render_import_failure(
))
}
/// The confirmation a completed import answers with, once the progress dialog
/// stops polling. A blocking dialog rather than `Alert::success`'s toast: the
/// toast dismisses itself on a timer, which is the same "did this actually
/// finish?" doubt the progress dialog exists to remove.
#[derive(Template)]
#[template(path = "pages/import_export/import/success_dialog.html")]
struct ImportSuccessDialog<'a> {
locale: Locale,
title: &'a str,
message: &'a str,
}
pub(crate) fn render_success(
locale: Locale,
inserted: usize,
prepared_rows: usize,
table_name: &str,
) -> String {
render(&Alert::success(
render(&ImportSuccessDialog {
locale,
&tr!(locale, "import-success-title"),
&tr!(
title: &tr!(locale, "import-success-title"),
message: &tr!(
locale,
"import-success-message",
"inserted" => inserted as i64,
"source_rows" => prepared_rows as i64,
"table" => table_name.to_string(),
),
))
})
}
#[cfg(test)]
@@ -414,6 +426,14 @@ mod tests {
// 500 rows in 40 seconds, so the 1500 left are about two minutes away.
assert!(html.contains("13 rows/s"), "{html}");
assert!(html.contains("1 m 56 s"), "{html}");
// A running import blocks the page rather than sitting as an inline
// card the user can miss or click past.
assert!(html.contains(r#"role="dialog""#), "{html}");
assert!(html.contains("aria-modal=\"true\""), "{html}");
// Unlike ui/dialog.html's dismissible dialogs, this one has no escape
// hatch: clicking Import again would send the file a second time.
assert!(!html.contains("keydown.esc"), "{html}");
assert!(!html.contains("click.self"), "{html}");
}
/// A rate needs rows and time behind it. Before there are either, the card
@@ -460,4 +480,21 @@ mod tests {
assert!(html.contains("Nothing was imported"), "{html}");
assert!(!html.contains("already imported remain"), "{html}");
}
/// The card the progress dialog polls into once the import is done: a
/// dialog the user has to actively dismiss, not a toast on its own timer.
#[test]
fn a_finished_import_answers_with_a_dismissible_dialog_not_a_toast() {
let html = render_success(Locale::English, 500, 500, "customers");
assert!(!html.contains("Template error"), "{html}");
assert!(html.contains(r#"role="dialog""#), "{html}");
assert!(html.contains("Import complete"), "{html}");
assert!(html.contains("500"), "{html}");
assert!(html.contains("customers"), "{html}");
// The toast fires-and-forgets via a window event; this answers with
// the dialog itself instead.
assert!(!html.contains("$dispatch('notify'"), "{html}");
assert!(html.contains("keydown.esc"), "{html}");
}
}