feat(link-wizard): search-assisted candidate selection - #338
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
3822c03 to
d76fbe1
Compare
There was a problem hiding this comment.
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
artistSearchAPI module (types, query keys, anduseSearchArtistLinksQuery) backed bysupabase.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.
- 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
Playwright test resultsDetails
|
… 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
|
Deploy →
|
- 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
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
There was a problem hiding this comment.
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
providerUrlunconditionally 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-labelor 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
FormLabelor at least anaria-label.
<Textarea
value={field.value ?? ""}
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).
There was a problem hiding this comment.
🔵 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
providerUrlunconditionally 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
artistsarray 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
searchmeans 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.
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
Closes #335
Stacked on #337 (search-artist-links edge function)
Generated by Claude Code