From a15d35214eb05ddec58e828c0d7d1950c343d4ed Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Mon, 4 May 2026 14:16:33 +0000 Subject: [PATCH] Stage warning actions; defer sort to explicit click Add/remove/swap warning-action handlers no longer auto-fire /api/sort. They mutate the input textarea idempotently; the sort button gets a pending cue when current input != last-sorted input. Branch-picker (/api/resort, cheap) keeps instant behavior. Spec lives in docs/specs/2026-05-04-staged-warning-actions.md. --- .../2026-05-04-staged-warning-actions.md | 137 ++++++++++++++++++ frontend/index.html | 31 ++++ frontend/sortof-app.jsx | 121 +++++++++++----- 3 files changed, 255 insertions(+), 34 deletions(-) create mode 100644 docs/specs/2026-05-04-staged-warning-actions.md diff --git a/docs/specs/2026-05-04-staged-warning-actions.md b/docs/specs/2026-05-04-staged-warning-actions.md new file mode 100644 index 0000000..db1a50c --- /dev/null +++ b/docs/specs/2026-05-04-staged-warning-actions.md @@ -0,0 +1,137 @@ +# Spec — Staged warning actions + +**Date:** 2026-05-04 +**Status:** Draft (awaiting review) +**Lineage:** Sits on top of Spec C §3 (Dep Add) and Spec A (multi-branch picker). Modifies the post-click behavior of warning action buttons; does not change the warnings themselves, the backend, or the picker contract. + +## 1. Summary + +Today, clicking `[add modId]`, `[✕ remove]`, or `[↔ swap]` in a warning row mutates the input textarea **and immediately fires a fresh `/api/sort`**. This spec defers the sort: those three handlers become pure textarea mutators, the user accumulates as many add/remove/swap edits as they want, and the existing `sort` button is the explicit "apply now" trigger. The branch-picker (cheap `/api/resort`) keeps its current instant behavior. + +Visual feedback is twofold: each warning row whose action target is now satisfied by the current input gets a strikethrough + `✓ pending` button label; the main sort button gets a "pending" cue when `current input ≠ last-sorted input`. + +## 2. Problem + +The current "instant resort on click" loop punishes the user when they want to triage warnings in a batch: + +- `/api/sort` is the **slow** endpoint — every wsid hits Steam's metadata lookup; uncached mods queue a DepotDownloader pull (~30s cold per mod). Three sequential clicks = three slow round-trips. +- Users who reconsider mid-flight (add X, change mind, remove X) eat two full sort cycles for net-zero state change. +- The chained reflows feel jumpy: each click rerenders the result panel and the warnings list, which can shuffle the row the user was about to click next. + +Branch-picker doesn't have this problem — `/api/resort` uses the cached MOD_DB and returns in ~100ms — so it's left alone. + +## 3. Behavior + +### 3.1 Action handlers + +`onAddWsid` / `onRemoveWsid` / `onSwapWsid` (sortof-app.jsx ~1348 / ~1363 / ~1381) drop their trailing `onSort(newInput)` call. They become pure mutators of `input`. The `setBranchSelections + runResort` handler `onPickBranch` is **untouched**. + +These handlers also become **idempotent toggles**: if the action's effect is already present in the current input (e.g., `add-wsid` for a wsid already in input, or `remove-wsid` for a wsid already absent), the second click reverses the first. This is what powers the per-warning "click to undo" UX without a separate undo affordance. + +| Action | First click | Second click | +|---|---|---| +| `add-wsid X` | append `X` to textarea | remove `X` from textarea | +| `remove-wsid X` | remove `X` from textarea | append `X` back to textarea | +| `swap-wsid A→B` | replace `A` with `B` | replace `B` with `A` | + +### 3.2 Warning row "staged" derivation (stateless) + +For each warning row with at least one mutating action, derive a boolean `staged` from current `input`: + +- `add-wsid` action targeting `wsid W` → `staged = (W ∈ parseWorkshopInput(input))` +- `remove-wsid` action targeting `wsid W` → `staged = (W ∉ parseWorkshopInput(input))` +- `swap-wsid` action `from A, to B` → `staged = (A ∉ input ∧ B ∈ input)` + +A warning may have multiple actions; if any one is satisfied, the row is staged. + +When `staged === true`: +- Row gets `.staged` class — strikethrough text + reduced opacity (~0.55). +- Mutating action button label flips to `✓ pending` and `title` changes to `click to undo` (or per-type analogue). +- Non-mutating action buttons on the row (`search-workshop`, branch-picker expand) are unchanged. + +This derivation runs every render against `parseWorkshopInput(input)`. No new state, no syncing logic. Free benefit: if the user manually edits the textarea, all stage indicators update on the next render automatically. + +### 3.3 Sort button "pending" cue + +A new `lastSortedInputRef` (React `useRef`) tracks the input string at the moment the most recent successful `onSort` completed. + +- On `onSort` success path (existing fetch handler): `lastSortedInputRef.current = inputThatWasSent`. +- On `onSort` failure path: do **not** update — preserves the pending cue so the user knows the resort didn't land. +- Initial value: `null`. The cue only appears once the user has done at least one sort and *then* drifted from it. + +The main sort button reads `pending = (input !== lastSortedInputRef.current && lastSortedInputRef.current !== null)` and applies a visual modifier (CSS class `.sort-pending`) when true. Concrete treatment: a small filled dot suffix or pill (`var(--info)` or `var(--warning)`, exact pick during impl). + +### 3.4 Multi-warning resolution + +Because `staged` is derived purely from `input`, a single textarea mutation marks **every** warning whose action target matches. Example: three mods all missing dep `tsarslib`; clicking `[add 2392709985]` on warning A marks warnings A, B, and C as `✓ pending` simultaneously, since all three have `add-wsid` actions targeting the same wsid. No event-broadcasting code needed; it's a free property of the stateless derivation. + +### 3.5 Branch-picker (unchanged) + +`onPickBranch` continues to call `runResort(updated)` synchronously. Rationale: `/api/resort` is fast, doesn't hit Steam, and has no slow path; the current "instant" feel is correct for it. The pending cue/strikethrough machinery does not apply to branch warnings. + +## 4. Components touched + +All changes are in **`/opt/sortof/frontend/sortof-app.jsx`** (one file). No backend, no schema, no CSS file outside the inline `