Repository navigation
fix(ui): reset the Add asset modal on every open - #3329
Conversation
The Add modal is hidden with x-show, never unmounted, so everything the last Add left in it was still there on the next open. Adding a second web asset greeted the operator with the first one's URL already in the Asset URL box — one unnoticed Enter away from a duplicate. The "From URL" input is now bound to homeApp state (addUri) and the tab selection moves out of the modal's nested x-data into the same component, so openAdd() can clear both. Resetting on open rather than on close means the form is fresh however the last one ended — saved, cancelled, Esc, or backdrop click. Two exceptions are kept deliberately: an upload hidden mid-batch keeps the file tab so the operator can still watch its progress, and the Apps pane (its own appsTab component) resets itself off an add-modal-open event rather than being left parked on the last app's filled-in config form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate issues remain in upload-tab preservation and Apps-pane error handling; one test-coverage nit also remains.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes stale Add asset modal state when reopened by resetting the URL, selected tab, and Apps configuration while preserving active upload progress.
Changes:
- Binds and resets the URL input and shared tab state.
- Resets the Apps pane on modal open.
- Adds unit and Playwright regression tests.
Review findings:
- Moderate: Active uploads may reopen on the wrong tab.
- Moderate: Apps-pane error states can be lost after reset.
- Nit: Apps-pane reset coverage lacks a DOM/integration assertion.
File summaries
| File | Summary |
|---|---|
tests/test_app.py |
Adds modal reopen regression tests. |
src/anthias_server/app/templates/_asset_modal.html |
Binds URL state, moves tab state, and adds Apps reset handling. |
src/anthias_server/app/static/src/home.ts |
Implements modal state reset logic. |
src/anthias_server/app/static/src/home.test.ts |
Adds state-reset and event tests. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Three issues from the Copilot pass: - The tab strip stays clickable while an upload runs, so `uploadState` could be truthy with `tab` on uri/apps. Skipping the reset in that case preserved the wrong tab and reopening still hid the running batch. Go to the file tab whenever an upload is in flight instead. - An empty or unreachable catalog sets `loaded` and `phase = 'error'` with its own message. Calling `back()` unconditionally swapped that for a `ready` pane showing an empty grid, which `load()` then declined to re-fetch. The pane now exposes `reset()`, which only unwinds the config phase and leaves an error state alone. - The Apps-pane coverage only asserted that `openAdd()` emitted an event, so it would have passed with the listener removed. Replaced the whole block with add-modal-reset.test.ts, which mounts real Alpine against the modal markup: it pins the `x-model` write-through to the DOM input, the cross-scope `tab` lookup from inside appsTab's nested x-data, and the listener itself. Verified it fails when the listener is dropped and when `reset()` is widened back to `back()`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
🟡 Changes recommended
Tests do not exercise the production template wiring, and the PR description reports an inaccurate test count.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
src/anthias_server/app/static/src/add-modal-reset.test.ts:20
- This test mounts
MODAL, a hand-written fragment, rather than the production_asset_modal.html, so it cannot catch a mismatch in the actual template. For example, removing or misspelling@add-modal-open.window="reset()"orx-model="addUri"in_asset_modal.htmlleaves these tests green because the copied attributes remain. Please add coverage that renders/drives the real Django template (or an end-to-end regression for the Apps pane) instead of relying solely on this duplicate markup.
// The panes the reset touches, trimmed to the attributes under test.
// Kept in sync with _asset_modal.html by hand — a bun test can't render
// a Django template.
- Files reviewed: 5/6 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The modal reset tests do not verify the rendered template or Apps reset wiring.
Review details
Suppressed comments (1)
src/anthias_server/app/static/src/add-modal-reset.test.ts:21
MODALis a hand-maintained subset rather than the Django template, so these tests can pass even if_asset_modal.htmllosesx-model, changes the scope, or removes@add-modal-open.window: the copied string still contains the expected attributes. The Playwright additions do not cover the Apps reset, leaving that production wiring unverified. Exercise the rendered template for this path (for example, add an integration case that selects an app, closes/reopens, and asserts the catalog), or render the Django template in the test setup.
// The panes the reset touches, trimmed to the attributes under test.
// Kept in sync with _asset_modal.html by hand — a bun test can't render
// a Django template.
const MODAL = `
- Files reviewed: 5/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
Swept the rest of the UI for the shape behind the Add-modal bug — an editable control in a surface that hides rather than unmounts, with nothing to reset it — and found one more instance. The Apps install form's Name box was bound one-way with `:value`, so the DOM drifted from `assetName` the moment the operator renamed an app. Re-selecting the same app re-seeds the identical string, which is no reactive change and so no re-render: the previous install's name was still in the box. `x-model` keeps the two in step, so the seed always lands. The rest of the audit came back clean, and for a reason worth recording: the edit and bulk-edit modals wrap their forms in `<template x-if>`, which tears the subtree down on close and rebuilds it from current state on the way back in. The delete and reboot confirmations hold no input at all, and the import / Screenly-migration wizards rebuild `items` and `selected` on every validate. The Add pane was the only form gated by `x-show` alone. Added tests/test_template_state_reset.py to keep it that way: a static guard, in the spirit of test_design_tokens.py, that fails any editable control bound one-way with `:value` outside a `<template x-if>`. It has no allowlist, and its docstring records why it deliberately stops short of every `x-show` subtree — /settings holds typed values across a disclosure toggle on purpose, and only the surface's meaning can tell the two apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
🔵 Needs a closer look
Expand template scanning recursively and strengthen DOM coverage for the production listener and Apps name binding.
Review details
Suppressed comments (2)
src/anthias_server/app/static/src/add-modal-reset.test.ts:20
- This DOM suite mounts a hand-copied subset rather than rendering
_asset_modal.html, so it cannot catch regressions in the production@add-modal-open.windowlistener or the Apps Namex-modelbinding; changing either attribute in the Django template leaves these tests green. The Playwright additions cover the URL and tab flows, but not these Apps-pane behaviors. Add an assertion against the rendered template (for example, an integration test) or otherwise generate this fixture from the template.
// The panes the reset touches, trimmed to the attributes under test.
// Kept in sync with _asset_modal.html by hand — a bun test can't render
// a Django template.
tests/test_template_state_reset.py:161
- The guard only scans HTML files directly under
templates/, so any editable control added in a template subdirectory will be silently omitted from this repository-wide check. Other template guards intests/test_template_views.pyandtests/test_design_tokens.pyuse recursive scans; use the same here so the test cannot pass without checking all templates.
TEMPLATE_FILES = sorted(TEMPLATES.glob('*.html'))
- Files reviewed: 6/7 changed files
- Comments generated: 0 new
- Review effort level: Lite
mypy's no-untyped-def caught _iter_tags: a generator with typed parameters but no return annotation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
🔵 Needs a closer look
The modal regression test uses a reduced fixture instead of the rendered template, so it may miss wiring regressions.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/anthias_server/app/static/src/add-modal-reset.test.ts:20
- This suite mounts a hand-maintained reduced
MODALstring rather than the rendered_asset_modal.html, so it will still pass if the production template drops or misspellsx-model="addUri"or@add-modal-open.window="reset()". Please add an assertion against the rendered Django/Playwright markup (or generate this fixture from the template) so the regression covers the actual wiring.
- Files reviewed: 6/7 changed files
- Comments generated: 0 new
- Review effort level: Lite
Both from the Copilot pass. The Alpine suite mounts a hand-written reduction of _asset_modal.html, so it was evidence about the fixture rather than about production — drop `x-model="addUri"` or rename the pane's reopen listener in the Django markup and everything stayed green. Four cases now read the real template and assert the exact attributes the fixture leans on, including that the Add pane has not re-grown a nested `x-data` declaring `tab` (which is what put it out of openAdd()'s reach to begin with). Verified by mutating the template: dropping either binding fails the suite. The static guard globbed only `templates/*.html`, so a template in a subdirectory would have dropped out of a repo-wide check without a word. rglob, matching test_design_tokens.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
|
Both points from the last pass are addressed in 71ea200. Fixture drift: you're right that the reduced I kept the reduced fixture rather than driving this through Playwright — the Apps pane needs a live app-store index to reach the config phase, so a browser test would need a stubbed store to cover what these cases cover directly. The template assertions close the wiring gap that made the fixture untrustworthy. Non-recursive glob: fixed, now |
|
Both branches hoisted the Add modal's pane selection out of its nested x-data and onto homeApp -- master (#3329) so openAdd() can reset the pane between opens, this branch so a file dropped on the page can open the modal straight onto the upload pane. Same move, two names. Takes master's `tab` and drops this branch's `addTab`. The drop path now goes through openAdd() and overrides the tab afterwards, so a dropped file gets the same fresh pane every other way in does -- no stale URL in the From-URL box, no Apps pane parked on the last config form -- rather than reaching past the reset that #3329 just added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>



The bug
Add a web asset, then open Add asset again: the first asset's URL is still sitting in the Asset URL box.
The modal is hidden with
x-show, never unmounted, so the plain<input>— and the nestedx-data="{ tab: 'uri' }"behind the tab strip — outlive a close. Nothing ever cleared them. Hitting Enter on that pre-filled field is one keystroke away from a duplicate asset.The fix
The URL input is now bound to
homeAppstate (addUri), and the tab selection moves out of the modal's nestedx-datainto the same component, soopenAdd()can reset both. Resetting on open rather than on close means the form is fresh however the last one ended — saved, cancelled, Esc, or backdrop click.Two behaviours are handled deliberately:
appsTab()component, so it resets itself off anadd-modal-openevent rather than being left parked on the last app's filled-in config form — the same staleness, one tab over. It exposesreset(), which only unwinds the config phase: an empty or unreachable catalog setsloadedplus its ownerrormessage, and bouncing that toreadywould show an empty grid thatload()then declines to re-fetch.Also in scope: the rest of the category
A sweep for the same shape — an editable control in a surface that hides rather than unmounts, with nothing to reset it — turned up one more instance. The Apps install form's Name box was bound one-way with
:value, so the DOM drifted fromassetNameas soon as the operator renamed an app. Re-selecting the same app re-seeds the identical string, which is no reactive change and therefore no re-render: the previous install's name was still sitting in the box.x-modelkeeps them in step.The rest came back clean, for a reason worth stating: the edit and bulk-edit modals wrap their forms in
<template x-if>, which tears the subtree down on close and rebuilds it from current state on reopen. The delete and reboot confirmations hold no input. The import and Screenly-migration wizards rebuilditems/selectedon every validate. The Add pane was the only form gated byx-showalone — which is why it was the only one that broke.tests/test_template_state_reset.pykeeps it that way: a static guard (no DB, no browser, no build — same model astest_design_tokens.py) that fails any editable control bound one-way with:valueoutside a<template x-if>. No allowlist. Its docstring records why it deliberately stops short of policing everyx-showsubtree: /settings holds typed values across a disclosure toggle on purpose, and only what the surface means can tell that apart from the Add modal, where every open is a fresh action. That shape stays pinned behaviourally instead.Testing
bun test— 140 pass. Newadd-modal-reset.test.tsmounts real Alpine against the modal markup rather than callingopenAdd()directly, so it pins the parts that aren't observable from the component object: thex-modelwrite-through to the DOM input, the cross-scopetablookup from insideappsTab's nestedx-data, and the@add-modal-open.windowlistener. Six cases — URL cleared, tab reset, file tab kept during an upload, Apps pane back on the catalog, app-store error left alone, and the Apps Name box re-seeded on re-select.reset()is widened back to an unconditionalback(), and when the Name box reverts to:value. The static guard was likewise confirmed to fire on the real pre-fix template.tests/test_app.pyreproducing the reported flow (add via URL, reopen, assert the field is empty) and the tab reset.bunx tsc --noEmit,ruff check,ruff format --checkall clean.The Playwright suite could not be run in this agent runtime (out of disk space, so the Docker test stack wouldn't build) — CI was its first execution, and the integration job is green.
🤖 Generated with Claude Code