Skip to content

Commit 367d146

Browse files
committed
fix: add card modal uses partial from search
1 parent ae80981 commit 367d146

4 files changed

Lines changed: 84 additions & 12 deletions

File tree

‎assets/react/src/components/card-name-search-field.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type CardNameSearchFieldProps = Omit<
1717
onClear?: () => void
1818
onSuggestionSelect?: (name: string) => void
1919
onValueChange: (value: string) => void
20+
selectFirstSuggestionOnEnter?: boolean
2021
suggestionLimit?: number
2122
value: string
2223
}
@@ -28,6 +29,7 @@ export function CardNameSearchField({
2829
onKeyDown,
2930
onSuggestionSelect,
3031
onValueChange,
32+
selectFirstSuggestionOnEnter = false,
3133
suggestionLimit = 5,
3234
value,
3335
...props
@@ -144,9 +146,9 @@ export function CardNameSearchField({
144146
} else if (event.key === "ArrowUp") {
145147
event.preventDefault()
146148
setActiveIndex((index) => (index <= 0 ? suggestions.length - 1 : index - 1))
147-
} else if (event.key === "Enter" && activeIndex >= 0) {
149+
} else if (event.key === "Enter" && (activeIndex >= 0 || selectFirstSuggestionOnEnter)) {
148150
event.preventDefault()
149-
selectSuggestion(suggestions[activeIndex])
151+
selectSuggestion(suggestions[Math.max(activeIndex, 0)])
150152
} else if (event.key === "Enter") {
151153
setIsOpen(false)
152154
} else if (event.key === "Escape") {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function selectedDeckCardNameForMutation(
2+
typedName: string,
3+
selectedCard?: { name?: string | null } | null,
4+
) {
5+
const selectedName = selectedCard?.name?.trim()
6+
return selectedName || typedName.trim()
7+
}

‎assets/react/src/pages/decks/add-card-dialog.tsx‎

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { pluralize, present, titleize } from "../../lib/utils"
1616
import { CardsDocument } from "../cards/data"
1717
import type { DeckDetail, DeckZone } from "./deck-types"
1818
import { ADD_CARD_ZONES, NON_COMMANDER_ADD_CARD_ZONES } from "./deck-types"
19+
import { selectedDeckCardNameForMutation } from "./add-card-dialog-model"
1920
import { AddDeckCardDocument } from "./queries"
2021

2122
const ADD_CARD_SEARCH_DEBOUNCE_MS = 250
@@ -46,12 +47,19 @@ export function AddDeckCardDialog({
4647
variables: { q: cardSearchTerm, limit: 5 },
4748
skip: !open || cardSearchTerm.length < 2,
4849
})
49-
const cardOptions = isCardSearchSettled
50-
? cardSearchQuery.data?.cards?.edges?.map((edge) => edge?.node).filter(present) || []
51-
: []
50+
const cardOptions =
51+
isCardSearchSettled && !cardSearchQuery.loading
52+
? cardSearchQuery.data?.cards?.edges?.map((edge) => edge?.node).filter(present) || []
53+
: []
5254
const selectedCard =
5355
cardOptions.find((card) => card.name.toLowerCase() === cardSearchTerm.toLowerCase()) ||
5456
cardOptions[0]
57+
const selectedCardName = selectedDeckCardNameForMutation(name, selectedCard)
58+
const isCardSearchPending =
59+
open && cardSearchDraftTerm.length >= 2 && (!isCardSearchSettled || cardSearchQuery.loading)
60+
const selectedCardMatchesInput = Boolean(
61+
selectedCard && selectedCardName.toLowerCase() === cardSearchDraftTerm.toLowerCase(),
62+
)
5563
const printingOptions =
5664
selectedCard?.printings?.edges?.map((edge) => edge?.node).filter(present) || []
5765
const selectedPrinting =
@@ -71,7 +79,7 @@ export function AddDeckCardDialog({
7179
variables: {
7280
deckId: deck?.id || "",
7381
input: {
74-
name: name.trim(),
82+
name: selectedCardName,
7583
quantity,
7684
zone,
7785
finish,
@@ -93,6 +101,7 @@ export function AddDeckCardDialog({
93101
setError(error instanceof Error ? error.message : "Could not add card to deck"),
94102
}),
95103
}
104+
const canSubmit = Boolean(deck && selectedCard && !isCardSearchPending && !addDeckCard.isPending)
96105

97106
useEffect(() => {
98107
const timeout = window.setTimeout(() => setDebouncedName(name), ADD_CARD_SEARCH_DEBOUNCE_MS)
@@ -130,10 +139,21 @@ export function AddDeckCardDialog({
130139

131140
function submit(event: FormEvent<HTMLFormElement>) {
132141
event.preventDefault()
133-
if (!name.trim()) {
142+
if (!cardSearchDraftTerm) {
134143
setError("Choose a card.")
135144
return
136145
}
146+
147+
if (isCardSearchPending) {
148+
setError("Wait for the card search to finish.")
149+
return
150+
}
151+
152+
if (!selectedCard) {
153+
setError("Choose a matching card.")
154+
return
155+
}
156+
137157
addDeckCard.mutate()
138158
}
139159

@@ -149,19 +169,49 @@ export function AddDeckCardDialog({
149169
</DialogHeader>
150170

151171
<form className="space-y-4 p-5" onSubmit={submit}>
152-
<label className="form-control">
153-
<span className="label-text mb-1 text-sm font-semibold">Card</span>
172+
<div className="form-control">
173+
<label
174+
htmlFor="add-deck-card-search"
175+
className="label-text mb-1 text-sm font-semibold"
176+
>
177+
Card
178+
</label>
154179
<CardNameSearchField
180+
id="add-deck-card-search"
155181
value={name}
156182
onValueChange={setName}
157183
onSuggestionSelect={setName}
158184
placeholder="Search card name"
185+
selectFirstSuggestionOnEnter
159186
disabled={addDeckCard.isPending}
160187
/>
161-
</label>
188+
<p className="mt-1 text-xs text-base-content/60">
189+
{isCardSearchPending
190+
? "Searching for the matching card..."
191+
: selectedCard
192+
? selectedCardMatchesInput
193+
? "Exact match selected."
194+
: `Selected match: ${selectedCard.name}`
195+
: !cardSearchDraftTerm
196+
? "Type a card name; the selected match below is what will be added."
197+
: cardSearchDraftTerm.length < 2
198+
? "Enter at least 2 characters."
199+
: "No matching card found."}
200+
</p>
201+
</div>
162202

163203
{selectedPrinting ? (
164204
<div className="rounded-box border border-base-300 bg-base-200/35 p-3">
205+
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
206+
<p className="text-xs font-black uppercase tracking-[0.18em] text-accent">
207+
Selected card
208+
</p>
209+
{!selectedCardMatchesInput ? (
210+
<p className="rounded-full border border-primary/30 px-2 py-0.5 text-xs font-semibold text-primary">
211+
Matched from search
212+
</p>
213+
) : null}
214+
</div>
165215
<div className="flex gap-3">
166216
{selectedPrinting.imageUrl ? (
167217
<img
@@ -269,8 +319,12 @@ export function AddDeckCardDialog({
269319
>
270320
Cancel
271321
</Button>
272-
<Button type="submit" disabled={addDeckCard.isPending || !name.trim()}>
273-
{addDeckCard.isPending ? "Adding..." : "Add card"}
322+
<Button type="submit" disabled={!canSubmit}>
323+
{addDeckCard.isPending
324+
? "Adding..."
325+
: selectedCard
326+
? `Add ${selectedCard.name}`
327+
: "Add card"}
274328
</Button>
275329
</div>
276330
</form>

‎assets/react/test/deck-card-model.test.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import test from "node:test"
22
import assert from "node:assert/strict"
33

44
import { hasMainboardAllocationAvailable } from "../src/pages/decks/deck-allocation-model.ts"
5+
import { selectedDeckCardNameForMutation } from "../src/pages/decks/add-card-dialog-model.ts"
56

67
function deckCard(zone, available, allocated, required = 1) {
78
return {
@@ -31,3 +32,11 @@ test("hasMainboardAllocationAvailable only considers unallocated mainboard cards
3132

3233
assert.equal(hasMainboardAllocationAvailable([deckCard("mainboard", 1, 0)]), true)
3334
})
35+
36+
test("selectedDeckCardNameForMutation submits the shown card match", () => {
37+
assert.equal(
38+
selectedDeckCardNameForMutation("fire lord a", { name: "Fire Lord Azula" }),
39+
"Fire Lord Azula",
40+
)
41+
assert.equal(selectedDeckCardNameForMutation(" Lightning Bolt ", null), "Lightning Bolt")
42+
})

0 commit comments

Comments
 (0)