Skip to content

feat(link-wizard): search-assisted candidate selection - #338

Merged
chiptus merged 26 commits into
mainfrom
feat/link-wizard-candidates-335
Aug 21, 2026
Merged

feat(link-wizard): search-assisted candidate selection#338
chiptus merged 26 commits into
mainfrom
feat/link-wizard-candidates-335

Conversation

@chiptus

@chiptus chiptus commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Adds search-based candidate suggestions in LinkWizard with smart data merging, custom query support, and per-provider error alerts.
Candidates appear as 3-card selection UI per provider with follower counts and genre tags; failed searches show a destructive alert instead of silently showing no candidates.

Verification

  • Open LinkWizard and see candidate cards appear with images, follower counts, and genres for missing links
  • Select a candidate card; URL field populates and image stages for commit
  • Two selections (Spotify + SoundCloud) on same artist; first provider's image wins
  • Click "Search Again" and enter custom artist name; new candidates load for that query
  • Save & Next commits the staged image along with the URL
  • Skip artist with no candidates found; falls back to plain manual URL input
  • Break provider credentials (or search while offline); a per-provider error alert appears above the URL field instead of an empty candidate list
  • Verify candidates don't overwrite existing image on artist

Closes #335

Stacked on #337 (search-artist-links edge function)


Generated by Claude Code

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
upline Ready Ready Preview Aug 21, 2026 2:58pm

claude added 3 commits August 21, 2026 11:15
Add search-based candidate suggestions in LinkWizard with card-based UI for artist selection, smart merging of candidate data (image, description) while preserving existing values, and support for custom search queries.

Implements:
- API module for search-artist-links edge function with batching support
- Pure merge logic for candidate selection with proper data precedence
- CandidateCards component with skeleton loading and genre tags
- LinkWizardStep integration showing up to 3 candidates per provider
- Search Again affordance for custom artist queries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
1. Fix query key to include artistNames and provider, preventing cache collisions
2. Implement batching: fetch first 10 artists on wizard load, prefetch next batch at position 8
3. Add dedicated custom search input for "Search Again" instead of URL field reuse
4. Remove description staging (candidates lack description field)
5. Extract ProviderLinkField component to reduce duplication and line count
6. Verify LinkWizardStep is keyed by artist.id (already correct in parent)

Changes:
- Query key now includes artistNames and provider for proper cache isolation
- LinkWizard prefetches next batch when position >= 8 within current batch
- ProviderLinkField component encapsulates provider-specific URL + custom search UI
- LinkWizardStep now ~160 lines (was 262), uses ProviderLinkField for both providers
- Removed description from CandidateUpdate interface and merge logic
- Updated all related tests to match new behavior

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
Surfaces the error field from search-artist-links results (and failed
requests) as a destructive alert per provider, so admins can tell a
failed search apart from a search with no matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
@chiptus
chiptus force-pushed the feat/link-wizard-candidates-335 branch from 3822c03 to d76fbe1 Compare August 21, 2026 11:17
Copilot AI lite review requested due to automatic review settings August 21, 2026 11:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds search-assisted candidate selection to the Link Wizard flow by querying search-artist-links, displaying up to 3 candidate cards per missing provider link, and wiring selection into staged updates + form fields.

Changes:

  • Introduces an artistSearch API module (types, query keys, and useSearchArtistLinksQuery) backed by supabase.functions.invoke("search-artist-links").
  • Updates Link Wizard batching to fetch candidates for the current 10-artist batch and prefetch the next batch near the end of the current one.
  • Adds UI for candidate cards, per-provider error alerts, and “Search Again” custom query input per provider.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/pages/admin/festivals/LinkWizard/ProviderLinkField.tsx New provider link input UI with candidate cards, per-provider error alert, and custom “Search Again” input.
src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx Wires provider fields to search results and candidate selection + staging behavior.
src/pages/admin/festivals/LinkWizard/LinkWizard.tsx Adds prefetching for the next candidate batch using React Query.
src/pages/admin/festivals/LinkWizard/CandidateCards.tsx New 3-card candidate selection UI (image/followers/genres + select action).
src/api/artistSearch/useSearchArtistLinksQuery.ts New TanStack Query wrapper around the search-artist-links edge function.
src/api/artistSearch/types.ts Defines provider/candidate/search response types and query key factory.
src/api/artistSearch/mergeCandidateSelection.ts Pure merge function to compute staged updates from a chosen candidate.
src/api/artistSearch/mergeCandidateSelection.test.ts Vitest unit tests for merge behavior.
Suppressed comments (3)

src/api/artistSearch/mergeCandidateSelection.ts:34

  • Candidate selection currently stages image_url only; Issue #335 also requires staging description (fill-if-empty, first-selection-wins, no overwrites). Extend the merge logic to stage description when artist/staged description are empty.
  const existingImage = artist.image_url || stagedUpdates.image_url || null;

  if (!existingImage && candidate.imageUrl) {
    updates.image_url = candidate.imageUrl;
  }

src/api/artistSearch/mergeCandidateSelection.test.ts:118

  • Add unit tests to cover description staging rules (fill-if-empty, no overwrite if artist/staged already has description, and first-selection-wins when selecting both providers) to match Issue #335 acceptance criteria.
  it("first provider selection wins shared image_url when both providers selected", () => {

src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx:205

  • Pass the provider-specific loading state so a SoundCloud re-search doesn’t disable the Spotify input/cards.
              isLoadingCandidates={isLoadingCandidates}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/pages/admin/festivals/LinkWizard/ProviderLinkField.tsx Outdated
Comment thread src/pages/admin/festivals/LinkWizard/CandidateCards.tsx Outdated
Comment thread src/api/artistSearch/types.ts Outdated
Comment thread src/api/artistSearch/mergeCandidateSelection.ts
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx Outdated
Comment thread src/api/artistSearch/mergeCandidateSelection.test.ts Outdated
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx Outdated
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx Outdated
Comment thread src/pages/admin/festivals/LinkWizard/ProviderLinkField.tsx Outdated
- Prevent Enter in the custom search input from submitting the form
- Drop the inaccessible clickable div on candidate cards in favor of
  the existing Select button
- Compute candidate loading state per provider instead of sharing one
  flag across Spotify and SoundCloud

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Playwright test results

passed  50 passed

Details

stats  50 tests across 14 suites
duration  59.1 seconds
commit  78a715a

Comment thread src/pages/admin/festivals/LinkWizard/ProviderLinkField.tsx Outdated
… select

Per #335's acceptance criteria, candidate selection should stage
description with the same fill-if-empty/first-selection-wins semantics
as image_url. Sources it from the SoundCloud user's bio (Spotify stays
null while its adapter is stubbed) and includes it in the Save & Next
mutation payload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploy → stagingworkflow run
Last updated: 2026-08-21 14:58:52 UTC

  • ⏭️ DB migrations skipped (no changes)
  • Edge functions succeeded

Comment thread src/api/artistSearch/mergeCandidateSelection.test.ts Outdated
Comment thread src/api/artistSearch/useSearchArtistLinksQuery.ts Outdated
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx Outdated
- Parse the search-artist-links response with a zod schema instead of
  an unsafe cast
- Split the provider-branching result lookup into getSpotifyResult/
  getSoundcloudResult backed by a shared pure helper
- Move test fixture helpers to the bottom of the test file

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
Comment thread src/pages/admin/festivals/LinkWizard/CandidateCards.tsx Outdated
toFixed(1) on candidate.followers/1000 rendered "0.3k" for a
follower count of 300; show the raw number under 1000 instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdP5NDJLLG5jBERrPbhvHT
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizard.tsx Outdated
Comment thread src/pages/admin/festivals/LinkWizard/StagedFieldsPreview.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

src/api/artistSearch/mergeCandidateSelection.ts:20

  • Mandatory: the specification says selecting any candidate always sets that provider's URL, but this makes URL staging optional and the new Image/Description buttons exercise that path. Set providerUrl unconditionally for a candidate selection and adjust the contradictory test that expects no URL.
  if (fields.includes("url")) {
    updates.providerUrl = { [provider]: candidate.url };
  }

src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx:174

  • Mandatory: this now writes both provider fields on every save, while the previous wizard only updated missing links. Because StagedFieldsPreview exposes both inputs, an existing provider URL can be edited and overwritten even though it is outside the current missing-link step. Only include a provider field when that URL was missing on the artist.
    const updates: UpdateArtistUpdates = {
      spotify_url: data.providerUrl.spotify || null,
      soundcloud_url: data.providerUrl.soundcloud || null,
    };

src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx:25

  • Mandatory: making both missing-link fields optional changes Save & Next from validated input to accepting an entirely blank form. The separate Skip action already covers advancing without a link, and the requirement says save behavior remains unchanged. Restore artist-aware required URL validation for whichever provider links are missing.

This issue also appears on line 171 of the same file.

const optionalUrlSchema = z
  .string()
  .url("Enter a valid URL")
  .optional()
  .or(z.literal(""));

src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx:63

  • The batch key is derived from the live missing-links array. After each successful save, artist-query invalidation removes the completed artist, shifts every later slice, and causes a new overlapping 10-artist search instead of reusing the original batch. Keep a stable wizard-order snapshot or cache/search by artist so saving through the wizard does not issue a fresh batch request per artist.
  const batchArtists = artists
    .slice(batchStart, batchStart + 10)
    .map((a) => a.name);

  const batchQueryResult = useSearchArtistLinksQuery(batchArtists);

src/pages/admin/festivals/LinkWizard/ProviderCandidatesPanel.tsx:72

  • This text input has no accessible name; its placeholder disappears while typing and is not a label for assistive technology. Add a provider-specific aria-label or an associated visible label.
          <Input
            type="text"
            placeholder="Enter artist name..."
            value={customSearchQuery}

src/pages/admin/festivals/LinkWizard/StagedFieldsPreview.tsx:85

  • The staged description textarea has no accessible name, so screen-reader users cannot determine what the field edits. Add a FormLabel or at least an aria-label.
                      <Textarea
                        value={field.value ?? ""}

Comment thread src/api/artistSearch/mergeCandidateSelection.ts
mergeCandidateSelection had no visibility into already-staged image_url
and description, so selecting a second candidate silently overwrote the
first pick instead of leaving it (first-selection-wins, per issue #335).
It now takes the currently-staged values and only fills fields that are
still empty.

Also add accessible names to the custom search input and the staged
description textarea (missing aria-label/FormLabel).
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx Outdated
Comment thread src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Review details

Suppressed comments (5)

src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx:25

  • Both URL fields are now optional, so “Save & Next” can advance without filling any missing link. This changes the previous behavior where missing URLs were required and “Skip” was the explicit bypass; restore required URL validation for submission.
const optionalUrlSchema = z
  .string()
  .url("Enter a valid URL")
  .optional()
  .or(z.literal(""));

src/api/artistSearch/mergeCandidateSelection.ts:26

  • Candidate selection is required to always stage the provider URL, but this conditional allows the Image and Description actions to update metadata without populating the URL. Set providerUrl unconditionally and update the test that currently expects no URL when another field is selected.
  if (fields.includes("url")) {
    updates.providerUrl = { [provider]: candidate.url };
  }

src/pages/admin/festivals/LinkWizard/LinkWizard.tsx:77

  • The passed artists array is the live “missing links” query result, which is invalidated after every save. Once the saved artist disappears, indices shift and subsequent steps request overlapping batches (for example B–K, then C–L); the current index also commonly returns to 0, so the position-8 prefetch is never reached. Keep a stable wizard-order snapshot or track batching progress independently of the shrinking query result.
                artists={artists}

src/pages/admin/festivals/LinkWizard/useProviderCandidates.ts:37

  • Returning the state setter as search means submitting the same custom query twice does not change state or re-run the React Query request. This prevents retrying the same artist name after an error; explicitly refetch when the normalized query is unchanged.
  return { candidates, error, isLoading, search: setCustomSearch };

src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx:170

  • This now submits both providers even when one link already existed. Because both inputs are exposed in the staged preview, an artist missing only SoundCloud can have its existing Spotify URL changed or cleared; preserve the previous behavior by only including fields that were missing on the artist.
    const updates: UpdateArtistUpdates = {
      spotify_url: data.providerUrl.spotify || null,
      soundcloud_url: data.providerUrl.soundcloud || null,
    };
  • Files reviewed: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…tom search

form.setValue doesn't mark a field dirty by default, so candidate
selections now pass shouldDirty so isDirty reflects staged changes.
onSubmit skips the mutation and just advances when the form isn't dirty.

useProviderCandidates' search also refetches instead of no-op'ing when
the same query is resubmitted, since a state setter with an unchanged
value doesn't retrigger the request.
@chiptus
chiptus merged commit c145918 into main Aug 21, 2026
16 checks passed
@chiptus
chiptus deleted the feat/link-wizard-candidates-335 branch August 21, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Link Wizard: search-assisted candidate selection UI

3 participants