refactor(ssr): full Selmer migration of Transaction Edit; remove the wizard

Migrate every part of the Transaction Edit modal's HTML to Selmer templates
(zero Hiccup in the render path) and delete the mm multi-modal "wizard"
abstraction entirely -- there was only ever one step.

- New auto-ap.ssr.components.selmer (sc) + ~22 shared component partials under
  resources/templates/components/ (typeahead, button-group, radio-card,
  data-grid, validated-field, modal, buttons, inputs, SVGs). Each wrapper renders
  its own partial; dynamic HTMX/Alpine attrs bridge via attrs->str -> {{attrs|safe}}.
- 15 modal templates under resources/templates/transaction-edit/.
- Delete EditWizard/LinksStep records + all mm/* usage. Plain handlers: flat
  wrap-decode-edit (fields renamed off step-params[...], stray keys stripped),
  flat wrap-derive-state, *errors*-based field errors, generic wrap-form-4xx-2.
- Drop the edit-wizard-navigate route (routes ~12 -> 5).
- Fix: stray `method` (tab button-group hidden) leaked into the upsert -> 500;
  strip decoded map to schema keys.
- e2e selectors updated (#wizard-form->#edit-form, #wizardmodal->#editmodal,
  step-params[...] field names). Parity: swap 6/6, edit 8/8, suite 38/1
  (1 pre-existing unrelated nav test).
- ssr-form-migration skill updated with the learnings (composition mechanics,
  sc/* library, drop-the-wizard recipe, scorecard row, 3 new gotchas).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 07:47:47 -07:00
parent c892719bd1
commit a01dfc197e
47 changed files with 1161 additions and 659 deletions

View File

@@ -5,7 +5,7 @@ import { test, expect } from '@playwright/test';
// re-renders the entire form, and the client selects what to swap back -- with
// no out-of-band swaps and no morph extension:
// - discrete changes (vendor, account, location, mode, add/remove row) swap
// all of #wizard-form (the active action/tab round-trips through the form,
// all of #edit-form (the active action/tab round-trips through the form,
// so it survives the swap);
// - typed fields never swap the input the user is in -- the amount field swaps
// only the #account-totals tbody (a sibling of the input rows), and the memo
@@ -32,7 +32,7 @@ async function openManualAdvanced(page: any, transactionIndex = 0) {
.nth(transactionIndex)
.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
await page.waitForSelector('#editmodal');
await page.click('button:has-text("Manual")');
// First transaction has no accounts so it opens in "simple" mode. Switch to
@@ -107,7 +107,7 @@ test.describe('Transaction Edit whole-form swap', () => {
.toBeGreaterThan(0);
// The form must survive the swap intact.
await expect(page.locator('#wizard-form')).toHaveCount(1);
await expect(page.locator('#edit-form')).toHaveCount(1);
expect(errors, errors.join('\n')).toEqual([]);
});
@@ -192,7 +192,7 @@ test.describe('Transaction Edit whole-form swap', () => {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
await page.waitForSelector('#wizardmodal');
await page.waitForSelector('#editmodal');
const memo = page.locator('#edit-memo');
await memo.waitFor();
@@ -301,7 +301,7 @@ test.describe('Transaction Edit whole-form swap', () => {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
await page.waitForSelector('#wizardmodal');
await page.waitForSelector('#editmodal');
await page.click('button:has-text("Manual")');
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
@@ -350,7 +350,7 @@ test.describe('Transaction Edit whole-form swap', () => {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
await page.waitForSelector('#wizardmodal');
await page.waitForSelector('#editmodal');
await page.click('button:has-text("Manual")');
await page.waitForSelector('div[hx-vals*="vendor-changed"]');