Skip to content

Give the Link Wizard queue a module (useLinkWizardQueue) #445

Description

@chiptus

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), 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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
    • selectArtist(artist)
    • selectedStages, toggleStage(stageId), clearStages()
  • 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.
  • Candidates feat(admin): import sets with csv #4 and fix(infra): open ip and dev domains #5 (unifying candidate-acquisition flows; a shared provider registry) — separate subsystems (provider metadata fetching), 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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions