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

@@ -3,31 +3,48 @@
The card carries its own poll trigger, the way admin/ecb's status card does:
while the import is running each answer is another card with the trigger
still on it, and the answer that ends the import is the success or failure
alert instead — which has no trigger, and that is what stops the polling.
still on it, and the answer that ends the import is the success dialog or
failure alert instead — which has no trigger, and that is what stops the
polling.
It is also a blocking dialog rather than an inline card: an import that is
actually done answers a stale-looking page if the tab is left alone, and a
second click on Import is how a duplicate row gets written. Pinning the
modal over the page for as long as the job runs makes "click it again" not
an option, and there is deliberately no way to dismiss it early — Escape and
a backdrop click do nothing here, unlike the dialogs in ui/dialog.html.
#}
{#
The card sits inside #submission-status, which is a polite live region: left
alone, a screen reader would read the whole card out again every second. It
silences itself instead, and the alert that replaces it when the import ends
— which is the part worth hearing — is announced by the region as usual.
#}
<div id="import-progress" class="import-progress" aria-live="off"
<div id="import-progress"
hx-get="{{ poll_url }}" hx-trigger="every 1s"
hx-target="#import-progress" hx-swap="outerHTML">
<h3>{{ heading }}</h3>
<div class="progress-line">
<progress max="100" value="{{ percent }}" aria-label="{{ label }}"></progress>
<span class="progress-percent">{{ percent }}%</span>
</div>
<p class="progress-rows"><strong>{{ rows }}</strong></p>
<dl class="progress-stats">
{% for stat in stats %}
<div>
<dt>{{ stat.label }}</dt>
<dd>{{ stat.value }}</dd>
hx-target="#import-progress" hx-swap="outerHTML"
x-data="{ importProgressOpen: true }">
<div x-cloak x-show="importProgressOpen" x-trap.inert.noscroll="importProgressOpen"
class="fixed inset-0 z-30 flex items-end justify-center bg-black/20 p-4 pb-8 backdrop-blur-md sm:items-center lg:p-8"
role="dialog" aria-modal="true" aria-labelledby="import-progress-heading">
<div class="flex w-full max-w-lg flex-col overflow-hidden rounded-radius border border-outline bg-surface text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
{#
The card sits inside a polite live region: left alone, a screen reader
would read the whole card out again every second. It silences itself
instead, and the dialog or alert that replaces it when the import ends
— which is the part worth hearing — is announced as usual.
#}
<div class="import-progress" aria-live="off">
<h3 id="import-progress-heading">{{ heading }}</h3>
<div class="progress-line">
<progress max="100" value="{{ percent }}" aria-label="{{ label }}"></progress>
<span class="progress-percent">{{ percent }}%</span>
</div>
<p class="progress-rows"><strong>{{ rows }}</strong></p>
<dl class="progress-stats">
{% for stat in stats %}
<div>
<dt>{{ stat.label }}</dt>
<dd>{{ stat.value }}</dd>
</div>
{% endfor %}
</dl>
<p class="hint">{{ hint }}</p>
</div>
</div>
{% endfor %}
</dl>
<p class="hint">{{ hint }}</p>
</div>
</div>

View File

@@ -0,0 +1,10 @@
{#
The import's success confirmation — crate::pages::import_export::import::ui::render_success.
A blocking dialog rather than the toast every other successful form uses:
the user has been staring at the progress dialog this replaces, and a toast
that shows itself and times out on its own schedule is exactly the "did it
actually finish?" ambiguity that motivated the dialog in the first place.
#}
{% import "ui/dialog.html" as dialog %}
{% call dialog::success(locale, title, message) %}{% endcall %}

View File

@@ -50,3 +50,49 @@
</div>
</div>
{% endmacro %}
{#
Blocking confirmation modal — Penguin UI's success modal, taken from
penguinui-components/modal/modal-alerts.html.
Used where a toast's own timer would leave the "did it actually finish?"
question open — a long-running import in particular, where the user has been
staring at a progress dialog and needs the same dialog to tell them it is
done, not a notification that can appear and vanish while they are looking
at the wrong corner of the screen. Dismissed the same three ways as
dialog::error: the close button, Escape, or a click on the backdrop.
{% import "ui/dialog.html" as dialog %}
{% call dialog::success(locale, "Import complete", message) %}{% endcall %}
#}
{% macro success(locale, title, message) %}
<div x-data="{ successModalIsOpen: true }">
<div x-cloak x-show="successModalIsOpen" x-transition.opacity.duration.200ms x-trap.inert.noscroll="successModalIsOpen" x-on:keydown.esc.window="successModalIsOpen = false" x-on:click.self="successModalIsOpen = false" class="fixed inset-0 z-30 flex items-end justify-center bg-black/20 p-4 pb-8 backdrop-blur-md sm:items-center lg:p-8" role="dialog" aria-modal="true" aria-labelledby="successModalTitle">
<!-- Modal Dialog -->
<div x-show="successModalIsOpen" x-transition:enter="transition ease-out duration-200 delay-100 motion-reduce:transition-opacity" x-transition:enter-start="opacity-0 scale-50" x-transition:enter-end="opacity-100 scale-100" class="flex max-w-lg flex-col gap-4 overflow-hidden rounded-radius border border-outline bg-surface text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
<!-- Dialog Header -->
<div class="flex items-center justify-between border-b border-outline bg-surface-alt/60 px-4 py-2 dark:border-outline-dark dark:bg-surface-dark/20">
<div class="flex items-center justify-center rounded-full bg-success/20 text-success p-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
</svg>
</div>
<button x-on:click="successModalIsOpen = false" aria-label="{{ locale.lookup("ui-close-dialog") }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="1.4" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Dialog Body -->
<div class="px-4 text-center">
<h3 id="successModalTitle" class="mb-2 font-semibold tracking-wide text-on-surface-strong dark:text-on-surface-dark-strong">{{ title }}</h3>
<p class="whitespace-pre-wrap">{{ message }}</p>
</div>
<!-- Dialog Footer -->
<div class="flex items-center justify-center border-outline p-4 dark:border-outline-dark">
<button x-on:click="successModalIsOpen = false" type="button" class="w-full whitespace-nowrap rounded-radius border border-success bg-success px-4 py-2 text-center text-sm font-semibold tracking-wide text-on-success transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-success active:opacity-100 active:outline-offset-0">{{ locale.lookup("import-progress-done-close") }}</button>
</div>
</div>
</div>
</div>
{% endmacro %}