refactor(web): shared errorMessage and createAsyncAction primitives #125

Merged
dominik.polakovics merged 1 commit from afk/117 into main 2026-07-19 01:35:38 +02:00

Closes #117

What

Two mechanical extractions in the web editor (Solid.js), per the issue's agent brief:

  1. One canonical errorMessage. The helper now lives in a new leaf module web/src/asyncAction.ts; the declaration in stores/mutations.ts and the five verbatim local copies (MediaModule, PageSettings, LinkPicker, TreeBrowser, MediaPicker) are gone. Every consumer — including stores/mutations.ts, stores/pageEditor.ts, and PagePane — imports from the new home, so UI components no longer import error formatting from the mutations store. grep -rn "function errorMessage" web/src yields exactly one hit.

  2. createAsyncAction(fn) wraps the dialog submit dance once: { busy, error, setError, run }, where run is a no-op while busy, clears the previous error, awaits fn, records errorMessage(err) on rejection, and always clears busy. All seven MediaModule dialogs adopt it and drop their per-dialog busy/error signals and try/catch shells (net −105 lines in MediaModule):

    • NewFolderDialog keeps its client-side slug pre-flight, recorded via setError without flipping busy or calling the API;
    • DeleteFileDialog keeps its structured blocked outcome and takes only busy/run.

Dialog UI, data-testids, disabled conditions, error rendering, and parent-facing props are unchanged. The one behavioral tightening — sanctioned by the issue — is that Enter-to-submit during a pending call is now a no-op, matching what the busy-disabled buttons already enforce.

Tests

  • New web/src/test/asyncAction.test.ts (vitest): busy lifecycle around a hand-settled promise, Error and non-Error rejection recording, error cleared before the next run's fn executes, re-entrant run while busy invokes fn exactly once, setError pre-flight + clear on success, argument forwarding. The errorMessage cases moved here from mutations.test.ts.
  • Full verification run locally: tsc --noEmit clean, web suite 47 files / 954 tests green, vite build OK, go vet ./... + go test ./... green, go build ./cmd/harness OK.

scaffold/site/AGENTS.md is untouched on purpose: this changes web-editor internals only, nothing the scaffolded manual documents.

🤖 Generated with Claude Code

Closes #117 ## What Two mechanical extractions in the web editor (Solid.js), per the issue's agent brief: 1. **One canonical `errorMessage`.** The helper now lives in a new leaf module `web/src/asyncAction.ts`; the declaration in `stores/mutations.ts` and the five verbatim local copies (MediaModule, PageSettings, LinkPicker, TreeBrowser, MediaPicker) are gone. Every consumer — including `stores/mutations.ts`, `stores/pageEditor.ts`, and PagePane — imports from the new home, so UI components no longer import error formatting from the mutations store. `grep -rn "function errorMessage" web/src` yields exactly one hit. 2. **`createAsyncAction(fn)`** wraps the dialog submit dance once: `{ busy, error, setError, run }`, where `run` is a no-op while busy, clears the previous error, awaits `fn`, records `errorMessage(err)` on rejection, and always clears busy. All seven MediaModule dialogs adopt it and drop their per-dialog `busy`/`error` signals and try/catch shells (net −105 lines in MediaModule): - NewFolderDialog keeps its client-side slug pre-flight, recorded via `setError` without flipping busy or calling the API; - DeleteFileDialog keeps its structured `blocked` outcome and takes only `busy`/`run`. Dialog UI, data-testids, disabled conditions, error rendering, and parent-facing props are unchanged. The one behavioral tightening — sanctioned by the issue — is that Enter-to-submit during a pending call is now a no-op, matching what the busy-disabled buttons already enforce. ## Tests - New `web/src/test/asyncAction.test.ts` (vitest): busy lifecycle around a hand-settled promise, Error and non-Error rejection recording, error cleared before the next run's fn executes, re-entrant `run` while busy invokes fn exactly once, `setError` pre-flight + clear on success, argument forwarding. The `errorMessage` cases moved here from `mutations.test.ts`. - Full verification run locally: `tsc --noEmit` clean, web suite 47 files / 954 tests green, `vite build` OK, `go vet ./...` + `go test ./...` green, `go build ./cmd/harness` OK. `scaffold/site/AGENTS.md` is untouched on purpose: this changes web-editor internals only, nothing the scaffolded manual documents. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor(web): extract shared errorMessage and createAsyncAction primitives
All checks were successful
ci / build-and-test (pull_request) Successful in 6m19s
4f65ed1091
Five components re-declared the mutations store's errorMessage helper
verbatim, and every MediaModule dialog hand-rolled the same busy/error/
try-catch submit shell around one API call.

Both now live once in web/src/asyncAction.ts, a leaf module with no store
dependencies: errorMessage moves there from stores/mutations.ts (every
former call site imports it), and the new createAsyncAction(fn) primitive
exposes { busy, error, setError, run } — run is a no-op while busy, clears
the previous error, records errorMessage(err) on rejection, and always
clears busy. All seven MediaModule dialogs adopt it; NewFolderDialog keeps
its pre-flight slug check via setError, and DeleteFileDialog keeps its
structured blocked outcome, taking only busy/run.

Dialog UI, testids, disabled conditions, and parent callbacks are
unchanged; the one behavioral tightening is that Enter-to-submit during a
pending call is now a no-op, matching what the busy-disabled buttons
already enforce.

Closes #117

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

This was generated by AI while landing a PR.

Landing audit — PASS

Verification signal relied on: Forgejo Actions ci / build-and-testsuccess (6m19s, run 115). Not re-run. The workflow is unconditional and frontend-first (npm ci && npm run build && npm test, then go vet/test/build), so this web-only diff was fully exercised by the green light.

Checks performed

  • Conventions: title refactor(web): shared errorMessage and createAsyncAction primitives is Conventional Commits. Head is afk/117; body carries a working Closes #117.
  • Mergeable: no conflict against main (merge-tree clean).
  • Acceptance criteria (#117):
    • grep -rn "function errorMessage" web/src → exactly 1 hit (web/src/asyncAction.ts:14); all six former sites import it, and PagePane/pageEditor no longer pull error formatting from the mutations store.
    • createAsyncAction unit-tested in web/src/test/asyncAction.test.ts: busy lifecycle over a hand-settled deferred, Error and non-Error rejection recording, error cleared before the next run's fn executes, re-entrant run invoking fn exactly once, setError pre-flight, argument forwarding.
    • All 7 MediaModule dialogs converted; no dialog retains a busy/error signal or try/catch shell. The two createSignal survivors (gridError, uploading, MediaModule.tsx:962-963) are grid-level upload state, correctly out of scope.
    • Dialog UI, data-testids, disabled conditions and parent-facing props unchanged.
  • Edge cases read closely:
    • NewFolderDialog keeps its slug pre-flight via setError without flipping busy or calling the API; a later successful run clears it — matches the pre-refactor sequencing.
    • DeleteFileDialog takes only busy/run, keeping its structured blocked outcome. Its unrendered error channel now swallows what was previously an unhandled rejection — a small improvement, and documented in-line.
    • Enter-to-submit is now a no-op during a pending call. Behavioral tightening explicitly sanctioned by the issue, and consistent with the busy-disabled buttons.

Non-blocking nit: MediaModule.tsx:34 places the ../asyncAction import after ../pagemanage, breaking the alphabetical ordering the other five converted files follow. Cosmetic; no lint enforces it.

Verdict: PASS — awaiting the human's merge go-ahead.

> *This was generated by AI while landing a PR.* ## Landing audit — PASS **Verification signal relied on:** Forgejo Actions `ci / build-and-test` — **success** (6m19s, run 115). Not re-run. The workflow is unconditional and frontend-first (`npm ci && npm run build && npm test`, then `go vet/test/build`), so this web-only diff was fully exercised by the green light. **Checks performed** - **Conventions:** title `refactor(web): shared errorMessage and createAsyncAction primitives` is Conventional Commits. Head is `afk/117`; body carries a working `Closes #117`. - **Mergeable:** no conflict against `main` (merge-tree clean). - **Acceptance criteria (#117):** - `grep -rn "function errorMessage" web/src` → exactly **1** hit (`web/src/asyncAction.ts:14`); all six former sites import it, and PagePane/pageEditor no longer pull error formatting from the mutations store. - `createAsyncAction` unit-tested in `web/src/test/asyncAction.test.ts`: busy lifecycle over a hand-settled deferred, `Error` and non-`Error` rejection recording, error cleared *before* the next run's fn executes, re-entrant `run` invoking fn exactly once, `setError` pre-flight, argument forwarding. - All **7** MediaModule dialogs converted; no dialog retains a `busy`/`error` signal or try/catch shell. The two `createSignal` survivors (`gridError`, `uploading`, MediaModule.tsx:962-963) are grid-level upload state, correctly out of scope. - Dialog UI, `data-testid`s, disabled conditions and parent-facing props unchanged. - **Edge cases read closely:** - `NewFolderDialog` keeps its slug pre-flight via `setError` without flipping busy or calling the API; a later successful `run` clears it — matches the pre-refactor sequencing. - `DeleteFileDialog` takes only `busy`/`run`, keeping its structured `blocked` outcome. Its unrendered error channel now swallows what was previously an unhandled rejection — a small improvement, and documented in-line. - Enter-to-submit is now a no-op during a pending call. Behavioral tightening explicitly sanctioned by the issue, and consistent with the busy-disabled buttons. **Non-blocking nit:** `MediaModule.tsx:34` places the `../asyncAction` import after `../pagemanage`, breaking the alphabetical ordering the other five converted files follow. Cosmetic; no lint enforces it. **Verdict: PASS** — awaiting the human's merge go-ahead.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!125
No description provided.