refactor(ssr): Phase 5 — full Selmer migration of Invoice Bulk Edit; remove the wizard; implement live totals
Migrates the Invoice Bulk Edit modal off the wizard to a plain Selmer form, building on the parity gate. Structurally Phase 3's bulk-code applied to invoices (selected entities -> expense-account rows), so near-pure reuse of bulk-code's flat-state plumbing + edit's account-totals-tbody. What changed - Wizard removed: deleted BulkEditWizard/AccountsStep records, MultiStepFormState, the step-params[...] prefix, the EDN snapshot, and all mm/* for this modal. Replaced with a plain handler + flat wrap-bulk-state (decode straight into bulk-edit-schema, no snapshot). - Selection-as-ids round-trip: the non-editable invoice 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 filter re-query. - De-cursored bulk-edit-account-row* to Selmer (sc/*), explicit-id location swap (#account-location-<index>, replacing the old find * swap), reusing tx-edit/location-select*. - 100% Selmer modal render path; the surgical edit was done with the text-based Edit tool (the clojure-mcp structural tools reformat the whole 1812-line file), so the diff is contained to the requires + the bulk-edit region. - Routes 5 -> 3: GET bulk-edit, PUT bulk-edit-submit, POST bulk-edit-form-changed (one whole-form op dispatcher folding the old new-account route). Implemented the dead totals - The wizard's TOTAL/BALANCE percentage rows were commented out (#_(...)) with a duplicate id="total". Implemented as a #expense-totals sibling-<tbody> refreshed by a Rule-4 percentage-keyup swap (the new-account + total + balance routes all fold into form-changed / the sibling-tbody). Scorecard delta (bulk-edit modal): wizard records 2->0, bulk-edit routes 5->3, step-params/fc-cursor (modal) ->0, location swap find *-> explicit-id, totals dead->implemented. Verification: invoice-bulk-edit spec 5/5 (incl. add-row, save, validation, the implemented totals); full Playwright suite 50/50; cljfmt clean; diff confined to the modal region. Skill fed: scorecard row + settled repeated-row target-selector convention; gotcha (structural tools reformat large files -> use text Edit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ async function addNewAccount(page: any) {
|
||||
// one (Solr-backed typeahead is unavailable in tests), then dispatching the location
|
||||
// reload -- the same approach the bulk-code spec uses.
|
||||
async function setRowAccount(page: any, rowIndex: number, accountId: string) {
|
||||
const rows = page.locator('#wizard-form tbody tr');
|
||||
const rows = page.locator('#bulk-edit-form tbody tr');
|
||||
const row = rows.nth(rowIndex);
|
||||
const hidden = row.locator('input[type="hidden"][name*="[account]"]').first();
|
||||
await hidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||
@@ -53,7 +53,7 @@ async function setRowAccount(page: any, rowIndex: number, accountId: string) {
|
||||
}
|
||||
|
||||
async function setRowPercentage(page: any, rowIndex: number, pct: string) {
|
||||
const row = page.locator('#wizard-form tbody tr').nth(rowIndex);
|
||||
const row = page.locator('#bulk-edit-form tbody tr').nth(rowIndex);
|
||||
const input = row.locator('input.amount-field, input[name*="percentage"]').first();
|
||||
await input.fill(pct);
|
||||
await input.dispatchEvent('change');
|
||||
@@ -61,7 +61,7 @@ async function setRowPercentage(page: any, rowIndex: number, pct: string) {
|
||||
}
|
||||
|
||||
async function submitForm(page: any) {
|
||||
await page.locator('#wizard-form').evaluate((f: HTMLFormElement) =>
|
||||
await page.locator('#bulk-edit-form').evaluate((f: HTMLFormElement) =>
|
||||
f.dispatchEvent(new Event('submit', { bubbles: true })));
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe('Invoice Bulk Edit (characterization)', () => {
|
||||
await expect(modal).toContainText('TOTAL');
|
||||
await expect(modal).toContainText('BALANCE');
|
||||
// a default expense-account row is present, plus the New account button
|
||||
expect(await modal.locator('input[name*="[expense-accounts]"][name*="[account]"]').count()).toBeGreaterThanOrEqual(1);
|
||||
expect(await modal.locator('input[name*="expense-accounts"][name*="[account]"]').count()).toBeGreaterThanOrEqual(1);
|
||||
await expect(modal.locator('a:has-text("New account")')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ test.describe('Invoice Bulk Edit (characterization)', () => {
|
||||
await selectFirstInvoice(page);
|
||||
await openBulkEditModal(page);
|
||||
|
||||
const accountRows = () => page.locator('#wizard-form input[name*="[expense-accounts]"][name*="[account]"]');
|
||||
const accountRows = () => page.locator('#bulk-edit-form input[name*="expense-accounts"][name*="[account]"]');
|
||||
const before = await accountRows().count();
|
||||
await addNewAccount(page);
|
||||
expect(await accountRows().count()).toBe(before + 1);
|
||||
@@ -110,6 +110,23 @@ test.describe('Invoice Bulk Edit (characterization)', () => {
|
||||
await expect(page.locator('#wizardmodal')).toBeHidden({ timeout: 8000 });
|
||||
});
|
||||
|
||||
// The TOTAL/BALANCE percentage rows were dead code in the wizard (commented out, with a
|
||||
// duplicate id="total"); the migration implements them as a sibling-<tbody> Rule-4 swap.
|
||||
test('TOTAL/BALANCE percentages render and recompute on edit (implemented)', async ({ page }) => {
|
||||
await navigateToInvoices(page);
|
||||
await selectFirstInvoice(page);
|
||||
await openBulkEditModal(page);
|
||||
|
||||
// default row is 100% -> TOTAL 100.0%
|
||||
await expect(page.locator('#expense-totals')).toContainText('100.0%');
|
||||
// edit to 50% -> the totals tbody refreshes via the targeted swap
|
||||
const pct = page.locator('#bulk-edit-form input.amount-field').first();
|
||||
await pct.click();
|
||||
await pct.fill('');
|
||||
await pct.pressSequentially('50'); // keyup -> Rule-4 swap of #expense-totals
|
||||
await expect(page.locator('#expense-totals')).toContainText('50.0%', { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('rejects when account percentages do not total 100%', async ({ page }) => {
|
||||
const info = await getTestInfo(page);
|
||||
await navigateToInvoices(page);
|
||||
|
||||
Reference in New Issue
Block a user