refactor(ssr): Phase 3 — full Selmer migration of Transaction Bulk Code; remove the wizard
Migrates the Transaction Bulk Code modal (a single-step form wearing a full wizard costume) to a plain Selmer form, cold-applying the ssr-form-migration skill. Almost entirely reuse of the Phase-2 work: the whole `sc/*` Selmer component library, `account-typeahead*` / `location-select*`, and the `edit-modal` / `transitioner` chrome are imported wholesale. What changed - Wizard removed: deleted `BulkCodeWizard` / `AccountsStep` records, `MultiStepFormState`, the `step-params[...]` prefix, and all `mm/*` middleware. Replaced with a plain handler + flat `wrap-bulk-state` (decode straight into `bulk-code-schema`, no snapshot round-trip). - Selection round-trip: the non-editable transaction selection is resolved to a concrete not-locked id vector at open and ridden back in hidden `ids[]` fields (the bulk analog of edit's single `db/id`) — no EDN snapshot, no filter re-query, and more correct (codes exactly the rows the user saw). - 100% Selmer render path (only the shared terminal `com/success-modal` keeps Hiccup — heuristic-9 exception). New shared component `sc/select` (`location-select.html` generalized) for the status dropdown. - Routes 4 -> 3: GET `bulk-code` (open), POST `bulk-code-submit`, POST `bulk-code-form-changed` (one whole-form op dispatcher folding the old `new-account` + `vendor-changed` routes). Location swap moved off `find *` onto explicit `#account-location-<index>` + `hx-select`. - Fixed a latent correctness bug surfaced by the migration: the vendor typeahead needs `:id` (value-keyed `:key`) or its value-bound hidden goes stale across a whole-form swap and posts blank. Scorecard delta (transaction/bulk_code.clj): mm coupling 19->0, snapshot merges 4->0, wizard records 3->0, step-params 10->0, routes 4->3, OOB 0, Hiccup-in-render ->0 (bar success-modal). LOC 420->506 (documented exception: the wizard was a thin shell over mm/* defaults, so explicitness moves shared plumbing into the file). Cookbook: reused the entire Phase-2 sc/* lib + chrome, added sc/select. Verification: bulk-code-transactions.spec.ts 13/13; full Playwright suite 39/39; cljfmt clean. Skill fed: scorecard row + narrative + LOC exception; gotchas (value-bound typeahead keying, selection-as-ids round-trip); cookbook (sc/select). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ async function openBulkCodeModal(page: any) {
|
||||
const codeButton = page.locator('button:has-text("Code")').first();
|
||||
await codeButton.click();
|
||||
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
|
||||
await page.waitForSelector('#wizardmodal');
|
||||
await page.waitForSelector('#bulkcodemodal');
|
||||
}
|
||||
|
||||
async function closeBulkCodeModal(page: any) {
|
||||
@@ -156,7 +156,7 @@ async function addNewAccount(page: any) {
|
||||
}
|
||||
|
||||
async function submitBulkCodeForm(page: any) {
|
||||
const form = page.locator('#wizard-form');
|
||||
const form = page.locator('#bulk-code-form');
|
||||
await form.evaluate((el: HTMLFormElement) => {
|
||||
el.dispatchEvent(new Event('submit', { bubbles: true }));
|
||||
});
|
||||
@@ -184,7 +184,7 @@ test.describe('Bulk Code Transactions - Happy Path', () => {
|
||||
await expect(page.locator('text=Bulk editing 1 transactions')).toBeVisible();
|
||||
|
||||
// Select vendor
|
||||
const vendorHidden = page.locator('input[type="hidden"][name="step-params[vendor]"]').first();
|
||||
const vendorHidden = page.locator('input[type="hidden"][name="vendor"]').first();
|
||||
const testInfo = await getTestInfo(page);
|
||||
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||
const newInput = document.createElement('input');
|
||||
@@ -196,7 +196,7 @@ test.describe('Bulk Code Transactions - Happy Path', () => {
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Select approval status
|
||||
const statusSelect = page.locator('select[name="step-params[approval-status]"]').first();
|
||||
const statusSelect = page.locator('select[name="approval-status"]').first();
|
||||
await statusSelect.selectOption('approved');
|
||||
|
||||
// Add account
|
||||
@@ -278,7 +278,7 @@ test.describe('Bulk Code Transactions - Validation', () => {
|
||||
const testInfo = await getTestInfo(page);
|
||||
const vendorId = testInfo.accounts.vendor;
|
||||
|
||||
const vendorContainer = page.locator('div[hx-post*="vendor-changed"]').first();
|
||||
const vendorContainer = page.locator('div[hx-vals*="vendor-changed"]').first();
|
||||
const vendorHidden = vendorContainer.locator('input[type="hidden"]').first();
|
||||
|
||||
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||
@@ -293,11 +293,11 @@ test.describe('Bulk Code Transactions - Validation', () => {
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
|
||||
await page.waitForResponse(response => response.url().includes('/vendor-changed') && response.status() === 200);
|
||||
await page.waitForResponse(response => response.url().includes('/form-changed') && response.status() === 200);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Select approval status
|
||||
const statusSelect = page.locator('select[name="step-params[approval-status]"]').first();
|
||||
const statusSelect = page.locator('select[name="approval-status"]').first();
|
||||
await statusSelect.selectOption('approved');
|
||||
|
||||
// Vendor selection pre-populated a default account row at 100%.
|
||||
@@ -310,11 +310,15 @@ test.describe('Bulk Code Transactions - Validation', () => {
|
||||
// Modal should still be open
|
||||
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
|
||||
|
||||
// Vendor should still be selected
|
||||
const vendorHiddenAfter = page.locator('input[type="hidden"][name="step-params[vendor]"]').first();
|
||||
const vendorValueAfter = await vendorHiddenAfter.inputValue();
|
||||
expect(vendorValueAfter).toBe(vendorId.toString());
|
||||
|
||||
// Vendor should still be selected. The vendor typeahead is Alpine-managed and posts
|
||||
// its value via an x-bound hidden input, so the right correctness check is what the
|
||||
// form actually submits (the value that gets saved), not the lagging DOM .value of the
|
||||
// hidden read in isolation.
|
||||
await expect.poll(async () =>
|
||||
page.locator('#bulk-code-form').evaluate((f: HTMLFormElement) =>
|
||||
new FormData(f).get('vendor'))
|
||||
).toBe(vendorId.toString());
|
||||
|
||||
// Status should still be selected
|
||||
const statusValueAfter = await statusSelect.inputValue();
|
||||
expect(statusValueAfter).toBe('approved');
|
||||
@@ -464,7 +468,7 @@ test.describe('Bulk Code Transactions - Vendor Pre-population', () => {
|
||||
|
||||
// The vendor typeahead dispatches change from its parent div
|
||||
// We need to set the hidden input and dispatch change on the container
|
||||
const vendorContainer = page.locator('div[hx-post*="vendor-changed"]').first();
|
||||
const vendorContainer = page.locator('div[hx-vals*="vendor-changed"]').first();
|
||||
const vendorHidden = vendorContainer.locator('input[type="hidden"]').first();
|
||||
|
||||
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||
@@ -481,7 +485,7 @@ test.describe('Bulk Code Transactions - Vendor Pre-population', () => {
|
||||
});
|
||||
|
||||
// Wait for HTMX response
|
||||
await page.waitForResponse(response => response.url().includes('/vendor-changed') && response.status() === 200);
|
||||
await page.waitForResponse(response => response.url().includes('/form-changed') && response.status() === 200);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Account should be pre-populated - check for account row
|
||||
@@ -521,7 +525,7 @@ test.describe('Bulk Code Transactions - Vendor Pre-population', () => {
|
||||
const testInfo = await getTestInfo(page);
|
||||
const vendorId = testInfo.accounts.vendor;
|
||||
|
||||
const vendorContainer = page.locator('div[hx-post*="vendor-changed"]').first();
|
||||
const vendorContainer = page.locator('div[hx-vals*="vendor-changed"]').first();
|
||||
const vendorHidden = vendorContainer.locator('input[type="hidden"]').first();
|
||||
|
||||
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||
@@ -538,7 +542,7 @@ test.describe('Bulk Code Transactions - Vendor Pre-population', () => {
|
||||
});
|
||||
|
||||
// Wait for HTMX response
|
||||
await page.waitForResponse(response => response.url().includes('/vendor-changed') && response.status() === 200);
|
||||
await page.waitForResponse(response => response.url().includes('/form-changed') && response.status() === 200);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should pre-populate the vendor's default account (non-clientized) plus the "New account" row
|
||||
|
||||
Reference in New Issue
Block a user