You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Understanding "the Link Wizard queue" — which artists remain, which one is current, and how skip/save/filter affect it — requires bouncing between LinkWizard.tsx (holds current-artist and stage-filter state, does index math), useLinkWizardSkipped (skip/save storage), filterArtistsByStage (stage filtering), and LinkWizardQueue.tsx (renders it, receiving 11 props). No single place owns "give me the current artist and let me navigate." That makes queue behavior hard to test (nothing exercises stage filtering, skip-then-advance, or save-then-advance today) and hard to change safely — recent PRs (#415 stage filter, #416 skip/save persistence, #425 flakes) have each had to touch this same ad-hoc assembly in LinkWizard.tsx.
Solution
Introduce one deep module, useLinkWizardQueue, that owns queue state end to end: stage filtering, current position, and skip/save actions that also advance position. It composes the existing useLinkWizardSkipped module internally (kept separate) rather than absorbing it, since skip/save storage also serves the unrelated "review and restore" popover flow. LinkWizard.tsx and LinkWizardQueue.tsx become thin callers of this one interface instead of each holding a slice of the state.
User Stories
As a Core Team member linking artists, I want the queue to reflect only artists matching my selected stage filter, so that I only work through the artists I'm currently focused on.
As a Core Team member, I want skipping an artist to both record the skip and advance me to the next artist in one action, so that I don't have to think about the two as separate steps.
As a Core Team member, I want saving an artist's links to both record the save and advance me to the next artist, so that I keep moving through the queue without manual navigation.
As a Core Team member on the last artist in the queue, I want skip/save to move me to the previous artist (since there's no next), so that I'm not left on a dead end.
As a Core Team member, I want to click any artist in the queue list to jump directly to it, so that I can work out of order when I want to.
As a Core Team member, I want the "previous" action to move to the prior artist in the filtered list, so that I can revisit an artist I just passed.
As a Core Team member, I want the queue's current position to always match the filtered list (never desync when I toggle a stage filter mid-session), so that the artist I'm viewing and the highlighted queue row never contradict each other.
As a Core Team member, I want to toggle and clear stage filters from the queue, so that I can narrow or widen who I'm working through.
As a developer maintaining the Link Wizard, I want queue navigation, filtering, and skip/save-then-advance logic to be testable through one hook interface, so that I can verify behavior without mounting the full page.
As a developer, I want LinkWizard.tsx to hold only data-fetching and the loading guard, delegating all queue state to useLinkWizardQueue, so that the page component stays a thin composition point.
As a developer, I want the mobile "view all" queue preview toggle to live in LinkWizardQueue.tsx (not the queue module or the page), since it's a display concern with no effect on which artist is current.
As a developer, I want useLinkWizardSkipped to remain usable directly by SkippedArtistsPopover (restore, clear all, list skipped), so that the review/undo flow isn't coupled to queue navigation.
Implementation Decisions
New module: useLinkWizardQueue(editionId, allArtists), colocated at src/pages/admin/festivals/LinkWizard/useLinkWizardQueue.ts (not src/hooks/, despite useLinkWizardSkipped living there — this hook is Link-Wizard-private and nothing outside the flow imports it).
Interface is semantic only — no index or raw position ever crosses the seam:
artists — filtered, unskipped list
currentArtist, position, total
prev()
skip() — marks the current artist skipped (via useLinkWizardSkipped) and advances
save() — marks the current artist saved (via useLinkWizardSkipped) and advances
Advance-after-skip/save behavior: move to the next artist in the filtered list; if the current artist is the last one, move to the previous artist instead (matches today's nextIndexAfterRemoval behavior).
useLinkWizardQueue composes useLinkWizardSkipped internally for exclusion and for skip()/save() writes. useLinkWizardSkipped is not modified and is not absorbed — it stays a standalone module, still consumed directly by SkippedArtistsPopover for restore/clear/list.
filterArtistsByStage stays a standalone pure function, called internally by useLinkWizardQueue — not part of the module's public interface, not otherwise changed.
Data fetching stays outside the module: LinkWizard.tsx keeps calling useArtistsMissingLinksByEditionQuery and keeps its own loading guard. useLinkWizardQueue accepts the already-fetched artist list as an argument rather than fetching internally, so it can be tested with a fixed array and isn't coupled to TanStack Query.
showFullQueue (the mobile "view all" preview toggle) moves out of LinkWizard.tsx and down into LinkWizardQueue.tsx as local state — it doesn't affect which artist is current, only how many rows render, and nothing outside LinkWizardQueue.tsx needs it once moved.
LinkWizard.tsx after the change: fetches artists, renders the loading guard, calls useLinkWizardQueue(editionId, allArtists), and wires its returned actions into LinkWizardQueue and LinkWizardStep. It no longer holds currentArtistId, selectedStages, index math, or the goTo/nextIndexAfterRemoval/handleSelectArtist/handleStageToggle closures.
LinkWizardQueue.tsx's prop surface shrinks correspondingly: stage-filter props and current-artist/select props come from the queue module's interface; skippedArtists/allArtists/onRestoreSkipped/onClearAllSkipped continue to be sourced from useLinkWizardSkipped directly (unchanged), since that's a separate concern from queue navigation.
No change to useLinkWizardSkipped's own interface or storage schema.
No CONTEXT.md changes — "Link Wizard queue" is already a defined term. No ADR conflicts: ADR-0006 (left-rail layout) and ADR-0007 (skip/save is browser-local) are both about decisions this spec doesn't touch.
Testing Decisions
Single seam: useLinkWizardQueue's own interface, exercised via renderHook (Vitest + React Testing Library, matching existing hook-test patterns in this codebase, e.g. useLocalStorageState.test.ts).
Tests should only assert on the module's public interface (returned artists, currentArtist, position, total, selectedStages) and its actions (prev, skip, save, selectArtist, toggleStage, clearStages) — never on internal state shape or on filterArtistsByStage/useLinkWizardSkipped directly.
Cases to cover:
Stage filtering narrows artists and keeps currentArtist consistent with the filtered list.
skip() marks the artist skipped (verify via the same useLinkWizardSkipped storage the module composes) and advances to the next artist.
save() marks the artist saved and advances to the next artist.
skip()/save() on the last artist in the list moves to the previous artist instead of going out of bounds.
selectArtist(artist) jumps directly to that artist regardless of current position.
prev() moves to the prior artist in the filtered list.
Toggling a stage filter after skip/save keeps currentArtist valid (no desync, no reference to a filtered-out artist).
LinkWizardQueue.tsx and LinkWizard.tsx get no new tests as part of this spec — they become thin composition points once the hook owns the logic, and the existing manual/e2e coverage (tests/e2e/link-wizard-candidate-verification.spec.ts) continues to cover them at the page level.
Out of Scope
Candidate feat(tests): add e2e tests #2 (deepening useLinkWizardSkipped's interface) — that hook is composed as-is, unmodified.
Candidate feat(timeline): fix design #3 (unifying the duplicated "batch of 10" math in useArtistBatchQuery/usePrefetchNextBatchLinks) — separate concern, not touched here.
Any change to useLinkWizardSkipped's storage schema, versioning, or the SkippedArtistsPopover UI/behavior.
Any change to stage-filter UI components (LinkWizardStageFilterDropdown, LinkWizardFilterSheet) beyond wiring them to the new hook's toggleStage/clearStages/selectedStages.
Further Notes
This spec implements Candidate 1 ("Give the Link Wizard queue a module") from the 2026-08-27 architecture review of the Link Wizard subsystem. It was worked out via a full grilling session with the user; the decisions above reflect explicit user confirmations, not defaults chosen unilaterally.
Problem Statement
Understanding "the Link Wizard queue" — which artists remain, which one is current, and how skip/save/filter affect it — requires bouncing between
LinkWizard.tsx(holds current-artist and stage-filter state, does index math),useLinkWizardSkipped(skip/save storage),filterArtistsByStage(stage filtering), andLinkWizardQueue.tsx(renders it, receiving 11 props). No single place owns "give me the current artist and let me navigate." That makes queue behavior hard to test (nothing exercises stage filtering, skip-then-advance, or save-then-advance today) and hard to change safely — recent PRs (#415 stage filter, #416 skip/save persistence, #425 flakes) have each had to touch this same ad-hoc assembly inLinkWizard.tsx.Solution
Introduce one deep module,
useLinkWizardQueue, that owns queue state end to end: stage filtering, current position, and skip/save actions that also advance position. It composes the existinguseLinkWizardSkippedmodule internally (kept separate) rather than absorbing it, since skip/save storage also serves the unrelated "review and restore" popover flow.LinkWizard.tsxandLinkWizardQueue.tsxbecome thin callers of this one interface instead of each holding a slice of the state.User Stories
LinkWizard.tsxto hold only data-fetching and the loading guard, delegating all queue state touseLinkWizardQueue, so that the page component stays a thin composition point.LinkWizardQueue.tsx(not the queue module or the page), since it's a display concern with no effect on which artist is current.useLinkWizardSkippedto remain usable directly bySkippedArtistsPopover(restore, clear all, list skipped), so that the review/undo flow isn't coupled to queue navigation.Implementation Decisions
useLinkWizardQueue(editionId, allArtists), colocated atsrc/pages/admin/festivals/LinkWizard/useLinkWizardQueue.ts(notsrc/hooks/, despiteuseLinkWizardSkippedliving there — this hook is Link-Wizard-private and nothing outside the flow imports it).artists— filtered, unskipped listcurrentArtist,position,totalprev()skip()— marks the current artist skipped (viauseLinkWizardSkipped) and advancessave()— marks the current artist saved (viauseLinkWizardSkipped) and advancesselectArtist(artist)selectedStages,toggleStage(stageId),clearStages()nextIndexAfterRemovalbehavior).useLinkWizardQueuecomposesuseLinkWizardSkippedinternally for exclusion and forskip()/save()writes.useLinkWizardSkippedis not modified and is not absorbed — it stays a standalone module, still consumed directly bySkippedArtistsPopoverfor restore/clear/list.filterArtistsByStagestays a standalone pure function, called internally byuseLinkWizardQueue— not part of the module's public interface, not otherwise changed.LinkWizard.tsxkeeps callinguseArtistsMissingLinksByEditionQueryand keeps its own loading guard.useLinkWizardQueueaccepts the already-fetched artist list as an argument rather than fetching internally, so it can be tested with a fixed array and isn't coupled to TanStack Query.showFullQueue(the mobile "view all" preview toggle) moves out ofLinkWizard.tsxand down intoLinkWizardQueue.tsxas local state — it doesn't affect which artist is current, only how many rows render, and nothing outsideLinkWizardQueue.tsxneeds it once moved.LinkWizard.tsxafter the change: fetches artists, renders the loading guard, callsuseLinkWizardQueue(editionId, allArtists), and wires its returned actions intoLinkWizardQueueandLinkWizardStep. It no longer holdscurrentArtistId,selectedStages, index math, or thegoTo/nextIndexAfterRemoval/handleSelectArtist/handleStageToggleclosures.LinkWizardQueue.tsx's prop surface shrinks correspondingly: stage-filter props and current-artist/select props come from the queue module's interface;skippedArtists/allArtists/onRestoreSkipped/onClearAllSkippedcontinue to be sourced fromuseLinkWizardSkippeddirectly (unchanged), since that's a separate concern from queue navigation.useLinkWizardSkipped's own interface or storage schema.Testing Decisions
useLinkWizardQueue's own interface, exercised viarenderHook(Vitest + React Testing Library, matching existing hook-test patterns in this codebase, e.g.useLocalStorageState.test.ts).artists,currentArtist,position,total,selectedStages) and its actions (prev,skip,save,selectArtist,toggleStage,clearStages) — never on internal state shape or onfilterArtistsByStage/useLinkWizardSkippeddirectly.artistsand keepscurrentArtistconsistent with the filtered list.skip()marks the artist skipped (verify via the sameuseLinkWizardSkippedstorage the module composes) and advances to the next artist.save()marks the artist saved and advances to the next artist.skip()/save()on the last artist in the list moves to the previous artist instead of going out of bounds.selectArtist(artist)jumps directly to that artist regardless of current position.prev()moves to the prior artist in the filtered list.currentArtistvalid (no desync, no reference to a filtered-out artist).LinkWizardQueue.tsxandLinkWizard.tsxget no new tests as part of this spec — they become thin composition points once the hook owns the logic, and the existing manual/e2e coverage (tests/e2e/link-wizard-candidate-verification.spec.ts) continues to cover them at the page level.Out of Scope
useLinkWizardSkipped's interface) — that hook is composed as-is, unmodified.useArtistBatchQuery/usePrefetchNextBatchLinks) — separate concern, not touched here.useLinkWizardSkipped's storage schema, versioning, or theSkippedArtistsPopoverUI/behavior.LinkWizardStageFilterDropdown,LinkWizardFilterSheet) beyond wiring them to the new hook'stoggleStage/clearStages/selectedStages.Further Notes
This spec implements Candidate 1 ("Give the Link Wizard queue a module") from the 2026-08-27 architecture review of the Link Wizard subsystem. It was worked out via a full grilling session with the user; the decisions above reflect explicit user confirmations, not defaults chosen unilaterally.