refactor(web): extract page editor controllers from PagePane/TreePane #120
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!120
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/113"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #113
Extracts the page-editing operation logic out of the two largest components into testable controller stores, following the
FormControllerprecedent.What moved where
stores/mutations.ts— the shared engine. The "mutate → refetch → follow selection" dance exists exactly once:attempt(error-channel lifecycle around a throwing postOp-style mutation, optional resync on failure) andfollow(apply authoritative state, then move the selection past the unsaved-changes guard with route repair suspended for the duration).stores/treeController.ts(createTreeController) — every tree mutation (draft toggle, reorder, create/root-create, rename, drag-move, the post-delete follow) plus the state those follows manipulate: the expansion set (with its sessionStorage persistence and root-seed) and the pagetype library. ReplacesTreePane'srunwrapper andfollowMutationhandlers.stores/pageEditor.ts(createPageEditor) — the doc resource with its cross-page validity gate,applyResult's authoritative-swap-on-success / resync-on-failure discipline for element ops, and the publish/discard/undo tagged-outcome routing. Replaces the logic previously fused intoPagePane.TreePane/PagePane— now presentation only: menus, dialogs, inline rename/phantom inputs, drag geometry, and DOM wiring that delegates to the controllers. JSX and DOM structure are untouched; transient-input lifetimes (phantom row, inline rename) are preserved viaonAcceptedhooks that fire exactly when the old inline code cleared them.Acceptance criteria
catch/unguarded/followMutation/mutation-API imports remain in either pane.stores/mutations.ts) and shared by both controllers.createTreeControllerandcreatePageEditordirectly against the existing mock API — no<App/>mount — covering successful mutations with authoritative/optimistic update, failed mutations surfacing error state, and selection-changing mutations (rename/move follows, subtree-delete clearing, discard/undo of a removed page).tsc --noEmitclean,vite buildandgo build ./...+go vet ./...pass.Notes for review
setDialog(null)inside the oldhandleMovedwas dropped (moves only originate from drops; the delete flow's dialog close is preserved).🤖 Generated with Claude Code
Landing audit — PASS
Verification signal relied on: Forgejo Actions
ci / build-and-test (pull_request)— success in 6m52s (run 107). Not re-run. This repo has a single unconditional CI job, so the green light did exercise this diff's path (web unit suites + tsc + vite build + go build/vet).Mergeability:
origin/mainis already an ancestor ofafk/113; a merge-tree test resolves cleanly. No conflict to resolve.Conventions: title is Conventional Commits (
refactor(web): …); two clean conventional commits; body carriesCloses #113matching theafk/113head branch, so the merge will auto-close the issue and release the run's claim.labctl secret scanover the outgoing diff: clean. No engine-facing surface touched, so thescaffold/site/AGENTS.mddrift rule does not apply.Claims audited (all verified true):
.skip/.onlyinflating the count.mockApi.tsuntouched.catch/unguarded/followMutation/mutation-API imports) holds; remainingapi/*imports in the panes are read-only fetches, pure helpers, or types.createRootwith a hand-rolled selection store and the mock API — no<App/>mount anywhere.Behavior-drift review. Three independent passes compared each removed handler against its replacement line by line. The extraction is faithful throughout —
applyResult, the doc resource's cross-page gate, the publish tagged-outcome switch, dialog-close ordering,mutatevsrefetchchoices, and the rename/move double-refetch (matching the oldrun()trailing refetch) are all preserved.createPage/createRootcorrectly omit that trailing refetch, matching the old paths that bypassedrun().Both self-declared drifts are accurate. Two notes, neither blocking:
main, with repair live, the refetch landed a tree missing the routed path, so route repair could bounce the editor to the empty state and leave a strayreplacein history before the follow's push. Suspending repair removes that. Real improvement, but user-reachable and uncovered by the new tests. The delete andcreateRootcases genuinely are inert (the route already names no page at that point).main'sdoDiscardnever clearedopErroron entry; routing it through the sharedattemptnow does (stores/mutations.ts:53). Consequence: a stale element-op error (e.g. a 409 "placement violation") now disappears when a subsequent discard succeeds, where it used to linger. This is the more consistent idiom — every other operation already cleared on entry — but it is a behavior change in something billed as pure refactor, and no test pins it either way.Test quality: above baseline. Assertions target observable state and wire payloads, not call counts — the insert test proves the authoritative swap by asserting
/api/pagefetch count is unchanged;eventsarrays pin ordering (["suspend","prepare","select","resume"]); several tests assert wire shape negatively (an omitted move index must be absent, notundefined). All three of the issue's required cases are covered.Suggested follow-ups (not gating): reword declared drift (1) to name the create-into-open-leaf fix, and consider a test asserting the create follow runs under suspension. The panes remain large (PagePane 2570→2303, TreePane 1453→1208) since JSX and drag handlers stay by design; the issue's "whole-app suites may be slimmed down" remains open follow-up work, as the PR notes.