audit: cycle copy-close tidy — reconcile spec to shipped structure
Cycle-close audit for the case-page copy+close cycle (spec 0002, plan
0003, feat 0308245). Architect drift review + scripts/check.sh.
Regression gate (scripts/check.sh): exit 0 — 0 failures across 19
stages (common/server/clients-desktop/experiments fmt+clippy+build+test,
wearos assembleDebug+lintDebug+testDebugUnitTest), 74s. The single
`wearos :: lintDebug` WARN is pre-existing and untouched by this
frontend-only cycle; not introduced here, carried forward.
Architect findings and resolution:
- [high] template emits two separate close forms, not the spec's single
#copy-close-form with a has_document label switch. Resolved as a SPEC
defect, not a code defect: the spec's single-form snippet was
infeasible — #doc-body exists only in the has-document match arm, so a
form anchored after it can never serve the documentless case, and it
must precede the copy <script> to be bound. The shipped two-form split
is canonical; spec 0002 §Concrete code shapes + §Data flow reconciled
to it (this commit).
- [high] the spec's `if (!getElementById("doc-body")) return` guard is
absent from the shipped handler. Not a defect: the no-document form
carries no id, so the handler (which binds #copy-close-form) never
attaches to it — the guard is structurally unnecessary. Spec script
block + prose updated to state this mechanism.
- [medium] the documentless form's distinctness was unpinned. Added an
assertion to case_page_empty_case_shows_placeholder that the empty
case renders no #copy-close-form (it is the bare close form).
- [low] plan counter 0003 vs spec counter 0002: not drift — the naming
policy is per-directory counters (stable_per_directory_4digit over
[docs/specs, docs/plans]); independent sequences are expected.
- [note] deferred case-list half of #14 leaves a coherent state
(refs #14, my_cases.html untouched). No drift.
Recommendation: carry-on. No fix iteration needed; the high items were
spec-vs-code reconciliation (docs) and a test guard, both done here.
refs #14
This commit is contained in:
@@ -41,7 +41,18 @@ server code moves.
|
||||
|
||||
What renders directly under the document for an **open** case. For a
|
||||
case *with* a document the button copies then closes; for an open case
|
||||
*without* a document it is a bare close (nothing to copy):
|
||||
*without* a document it is a bare close (nothing to copy).
|
||||
|
||||
**Two physically separate forms, not one with a label switch**
|
||||
(reconciled to the shipped implementation at audit). `#doc-body` only
|
||||
exists in the `{% when Some %}` (has-document) arm of the template's
|
||||
`{% match document_html %}`, so a single form anchored after it could
|
||||
never serve the documentless case — and it must be emitted *before* the
|
||||
copy `<script>` so the submit handler can bind it. The two cases
|
||||
therefore live in their two respective match arms:
|
||||
|
||||
In the has-document arm, after `#doc-body` — the combined control,
|
||||
wired to `copyDocText()`:
|
||||
|
||||
```html
|
||||
<div id="doc-body">{{ html|safe }}</div>
|
||||
@@ -49,11 +60,7 @@ case *with* a document the button copies then closes; for an open case
|
||||
<div class="doc-footer-actions">
|
||||
<form id="copy-close-form" method="post" action="/cases/{{ case_id }}/close">
|
||||
{% call csrf::field(csrf_token) %}
|
||||
{% if has_document %}
|
||||
<button type="submit" class="copy-close-btn">Kopieren + Schließen</button>
|
||||
{% else %}
|
||||
<button type="submit" class="copy-close-btn">Schließen</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<p id="copy-close-error" class="copy-error" role="alert" hidden>
|
||||
Kopieren fehlgeschlagen — Fall nicht geschlossen.
|
||||
@@ -62,6 +69,21 @@ case *with* a document the button copies then closes; for an open case
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
In the no-document arm — a bare close form. It carries **no** `id`, so
|
||||
the submit handler below (which binds `#copy-close-form`) never attaches
|
||||
to it and it submits normally (nothing to copy):
|
||||
|
||||
```html
|
||||
{% if !is_closed %}
|
||||
<div class="doc-footer-actions">
|
||||
<form method="post" action="/cases/{{ case_id }}/close">
|
||||
{% call csrf::field(csrf_token) %}
|
||||
<button type="submit" class="copy-close-btn">Schließen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
The inline script, with the two-path clipboard write extracted into a
|
||||
shared function used by both the icon copy button and the close form:
|
||||
|
||||
@@ -115,11 +137,12 @@ shared function used by both the icon copy button and the close form:
|
||||
// the user-gesture context before the form navigates, and a failed
|
||||
// copy aborts the close so the user never loses a document they
|
||||
// believed was on the clipboard.
|
||||
// Only the has-document form carries id="copy-close-form", so this
|
||||
// handler binds only there; the bare no-document "Schließen" form is
|
||||
// never matched and submits normally (no copy).
|
||||
const closeForm = document.getElementById("copy-close-form");
|
||||
if (closeForm) {
|
||||
closeForm.addEventListener("submit", async (e) => {
|
||||
// Bare "Schließen" (no document) submits normally, no copy.
|
||||
if (!document.getElementById("doc-body")) return;
|
||||
e.preventDefault();
|
||||
if (await copyDocText()) {
|
||||
closeForm.submit(); // close → 303 → /cases
|
||||
@@ -184,9 +207,9 @@ No new template variables: `is_closed`, `has_document`, `case_id`, and
|
||||
5. On `false`: `#copy-close-error` is unhidden; no submit; case stays
|
||||
open.
|
||||
|
||||
The bare **Schließen** button (open case, no document) has no
|
||||
`#doc-body`, so the handler returns early and the form submits
|
||||
normally → same close → `/cases`.
|
||||
The bare **Schließen** button (open case, no document) lives in a
|
||||
separate form with no `id`, so the submit handler never binds it; it
|
||||
submits normally → same close → `/cases`.
|
||||
|
||||
## Error handling
|
||||
|
||||
|
||||
@@ -299,6 +299,13 @@ async fn case_page_empty_case_shows_placeholder() {
|
||||
body.contains(r#">Schließen</button>"#),
|
||||
"documentless open case must offer a bare Schließen button"
|
||||
);
|
||||
// The documentless close is the BARE form, not the copy-close form:
|
||||
// with no document there is nothing to copy, so it must not bind the
|
||||
// clipboard-gated submit handler.
|
||||
assert!(
|
||||
!body.contains(r#"id="copy-close-form""#),
|
||||
"documentless open case must use the bare close form, not the copy-close form"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user