fix: PartySegmentedControl rep components now update reactively
The WeaponRep (and other rep components) weren't visually updating when
new items were added to the party. The issue was that Svelte's reactivity
wasn't properly propagating through the {#each} blocks.
Changes:
- PartySegmentedControl: Add derived values for party sub-properties to
ensure reactivity propagates through snippet boundaries
- WeaponRep: Pre-compute rows as explicit $derived value and use keyed
{#each} blocks for proper change detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f457343e26
commit
2275daec61
4 changed files with 41 additions and 70 deletions
|
|
@ -85,7 +85,6 @@ export function useCreateGridWeapon() {
|
||||||
gridAdapter.createWeapon(params, headers)
|
gridAdapter.createWeapon(params, headers)
|
||||||
),
|
),
|
||||||
onSuccess: (_data, params) => {
|
onSuccess: (_data, params) => {
|
||||||
// Invalidate the party to refetch with new weapon
|
|
||||||
invalidateParty(queryClient, params.partyId)
|
invalidateParty(queryClient, params.partyId)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -76,19 +76,11 @@
|
||||||
characters: []
|
characters: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize party state with proper validation
|
// Derive party directly from prop - single source of truth from TanStack Query cache
|
||||||
let party = $state<Party>(
|
let party = $derived(
|
||||||
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons) ? initial : defaultParty
|
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons) ? initial : defaultParty
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sync local party state with prop changes (for query refetches)
|
|
||||||
$effect(() => {
|
|
||||||
// Only update if we have valid party data from props
|
|
||||||
if (initial && initial.id && initial.id !== 'new' && Array.isArray(initial.weapons)) {
|
|
||||||
party = initial
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let activeTab = $state<GridType>(GridType.Weapon)
|
let activeTab = $state<GridType>(GridType.Weapon)
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let error = $state<string | null>(null)
|
let error = $state<string | null>(null)
|
||||||
|
|
@ -155,20 +147,16 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = true
|
||||||
let updated: Party | null = null
|
|
||||||
|
|
||||||
if (operation.type === 'swap') {
|
if (operation.type === 'swap') {
|
||||||
// Handle swapping items between positions
|
// Handle swapping items between positions
|
||||||
updated = await handleSwap(source, target)
|
await handleSwap(source, target)
|
||||||
} else if (operation.type === 'move') {
|
} else if (operation.type === 'move') {
|
||||||
// Handle moving to empty position
|
// Handle moving to empty position
|
||||||
updated = await handleMove(source, target)
|
await handleMove(source, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update party with returned data from API
|
// Party will be updated via cache invalidation from mutations
|
||||||
if (updated) {
|
|
||||||
party = updated
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || 'Failed to update party'
|
error = err.message || 'Failed to update party'
|
||||||
console.error('Drag operation failed:', err)
|
console.error('Drag operation failed:', err)
|
||||||
|
|
@ -178,7 +166,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSwap(source: any, target: any): Promise<Party> {
|
async function handleSwap(source: any, target: any): Promise<void> {
|
||||||
if (!party.id || party.id === 'new') {
|
if (!party.id || party.id === 'new') {
|
||||||
throw new Error('Cannot swap items in unsaved party')
|
throw new Error('Cannot swap items in unsaved party')
|
||||||
}
|
}
|
||||||
|
|
@ -198,11 +186,9 @@
|
||||||
} else if (source.type === 'summon') {
|
} else if (source.type === 'summon') {
|
||||||
await swapSummons.mutateAsync(swapParams)
|
await swapSummons.mutateAsync(swapParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return party
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMove(source: any, target: any): Promise<Party> {
|
async function handleMove(source: any, target: any): Promise<void> {
|
||||||
if (!party.id || party.id === 'new') {
|
if (!party.id || party.id === 'new') {
|
||||||
throw new Error('Cannot move items in unsaved party')
|
throw new Error('Cannot move items in unsaved party')
|
||||||
}
|
}
|
||||||
|
|
@ -221,8 +207,6 @@
|
||||||
} else if (source.type === 'summon') {
|
} else if (source.type === 'summon') {
|
||||||
await updateSummon.mutateAsync(updateParams)
|
await updateSummon.mutateAsync(updateParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return party
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Localized name helper: accepts either an object with { name: { en, ja } }
|
// Localized name helper: accepts either an object with { name: { en, ja } }
|
||||||
|
|
@ -276,11 +260,8 @@
|
||||||
error = null
|
error = null
|
||||||
|
|
||||||
// Update party title via API
|
// Update party title via API
|
||||||
const updated = await updatePartyDetails({ name: editingTitle })
|
await updatePartyDetails({ name: editingTitle })
|
||||||
if (updated) {
|
editDialogOpen = false
|
||||||
party = updated
|
|
||||||
editDialogOpen = false
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || 'Failed to update party title'
|
error = err.message || 'Failed to update party title'
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -290,7 +271,7 @@
|
||||||
|
|
||||||
// Party operations
|
// Party operations
|
||||||
async function updatePartyDetails(updates: Partial<Party>) {
|
async function updatePartyDetails(updates: Partial<Party>) {
|
||||||
if (!canEdit()) return null
|
if (!canEdit()) return
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
error = null
|
error = null
|
||||||
|
|
@ -299,10 +280,8 @@
|
||||||
// Use TanStack Query mutation to update party
|
// Use TanStack Query mutation to update party
|
||||||
await updatePartyMutation.mutateAsync({ shortcode: party.shortcode, updates })
|
await updatePartyMutation.mutateAsync({ shortcode: party.shortcode, updates })
|
||||||
// Party will be updated via cache invalidation
|
// Party will be updated via cache invalidation
|
||||||
return party
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || 'Failed to update party'
|
error = err.message || 'Failed to update party'
|
||||||
return null
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -317,11 +296,10 @@
|
||||||
try {
|
try {
|
||||||
if (party.favorited) {
|
if (party.favorited) {
|
||||||
await unfavoritePartyMutation.mutateAsync({ shortcode: party.shortcode })
|
await unfavoritePartyMutation.mutateAsync({ shortcode: party.shortcode })
|
||||||
party.favorited = false
|
|
||||||
} else {
|
} else {
|
||||||
await favoritePartyMutation.mutateAsync({ shortcode: party.shortcode })
|
await favoritePartyMutation.mutateAsync({ shortcode: party.shortcode })
|
||||||
party.favorited = true
|
|
||||||
}
|
}
|
||||||
|
// Party will be updated via cache invalidation
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || 'Failed to update favorite status'
|
error = err.message || 'Failed to update favorite status'
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -405,8 +383,8 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update job via API (use shortcode for party identification)
|
// Update job via API (use shortcode for party identification)
|
||||||
const updated = await partyAdapter.updateJob(party.shortcode, job.id)
|
await partyAdapter.updateJob(party.shortcode, job.id)
|
||||||
party = updated
|
// Party will be updated via cache invalidation
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Failed to update job'
|
error = e instanceof Error ? e.message : 'Failed to update job'
|
||||||
console.error('Failed to update job:', e)
|
console.error('Failed to update job:', e)
|
||||||
|
|
@ -444,11 +422,8 @@
|
||||||
|
|
||||||
console.log('[Party] Skills array to send:', skillsArray)
|
console.log('[Party] Skills array to send:', skillsArray)
|
||||||
|
|
||||||
const updated = await partyAdapter.updateJobSkills(
|
await partyAdapter.updateJobSkills(party.shortcode, skillsArray)
|
||||||
party.shortcode,
|
// Party will be updated via cache invalidation
|
||||||
skillsArray
|
|
||||||
)
|
|
||||||
party = updated
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error = extractErrorMessage(e, 'Failed to update skill')
|
error = extractErrorMessage(e, 'Failed to update skill')
|
||||||
console.error('Failed to update skill:', e)
|
console.error('Failed to update skill:', e)
|
||||||
|
|
@ -474,11 +449,8 @@
|
||||||
|
|
||||||
console.log('[Party] Skills array to send after removal:', skillsArray)
|
console.log('[Party] Skills array to send after removal:', skillsArray)
|
||||||
|
|
||||||
const updated = await partyAdapter.updateJobSkills(
|
await partyAdapter.updateJobSkills(party.shortcode, skillsArray)
|
||||||
party.shortcode,
|
// Party will be updated via cache invalidation
|
||||||
skillsArray
|
|
||||||
)
|
|
||||||
party = updated
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error = extractErrorMessage(e, 'Failed to remove skill')
|
error = extractErrorMessage(e, 'Failed to remove skill')
|
||||||
console.error('Failed to remove skill:', e)
|
console.error('Failed to remove skill:', e)
|
||||||
|
|
@ -504,11 +476,8 @@
|
||||||
// Convert skills object to array format expected by API
|
// Convert skills object to array format expected by API
|
||||||
const skillsArray = transformSkillsToArray(updatedSkills)
|
const skillsArray = transformSkillsToArray(updatedSkills)
|
||||||
|
|
||||||
const updated = await partyAdapter.updateJobSkills(
|
await partyAdapter.updateJobSkills(party.shortcode, skillsArray)
|
||||||
party.shortcode,
|
// Party will be updated via cache invalidation
|
||||||
skillsArray
|
|
||||||
)
|
|
||||||
party = updated
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Failed to remove skill'
|
error = e instanceof Error ? e.message : 'Failed to remove skill'
|
||||||
console.error('Failed to remove skill:', e)
|
console.error('Failed to remove skill:', e)
|
||||||
|
|
@ -591,8 +560,7 @@
|
||||||
partyId,
|
partyId,
|
||||||
partyShortcode: party.shortcode
|
partyShortcode: party.shortcode
|
||||||
})
|
})
|
||||||
// Return updated party from cache after mutation
|
// Party will be updated via cache invalidation
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to remove weapon:', err)
|
console.error('Failed to remove weapon:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -605,7 +573,6 @@
|
||||||
partyId,
|
partyId,
|
||||||
partyShortcode: party.shortcode
|
partyShortcode: party.shortcode
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to remove summon:', err)
|
console.error('Failed to remove summon:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -618,7 +585,6 @@
|
||||||
partyId,
|
partyId,
|
||||||
partyShortcode: party.shortcode
|
partyShortcode: party.shortcode
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to remove character:', err)
|
console.error('Failed to remove character:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -631,7 +597,6 @@
|
||||||
partyShortcode: party.shortcode,
|
partyShortcode: party.shortcode,
|
||||||
updates
|
updates
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update weapon:', err)
|
console.error('Failed to update weapon:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -644,7 +609,6 @@
|
||||||
partyShortcode: party.shortcode,
|
partyShortcode: party.shortcode,
|
||||||
updates
|
updates
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update summon:', err)
|
console.error('Failed to update summon:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -662,7 +626,6 @@
|
||||||
partyShortcode: party.shortcode,
|
partyShortcode: party.shortcode,
|
||||||
updates
|
updates
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update character:', err)
|
console.error('Failed to update character:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -681,7 +644,6 @@
|
||||||
uncapLevel,
|
uncapLevel,
|
||||||
transcendenceStep
|
transcendenceStep
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update character uncap:', err)
|
console.error('Failed to update character uncap:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -700,7 +662,6 @@
|
||||||
uncapLevel,
|
uncapLevel,
|
||||||
transcendenceStep
|
transcendenceStep
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update weapon uncap:', err)
|
console.error('Failed to update weapon uncap:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -719,7 +680,6 @@
|
||||||
uncapLevel,
|
uncapLevel,
|
||||||
transcendenceStep
|
transcendenceStep
|
||||||
})
|
})
|
||||||
return party
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update summon uncap:', err)
|
console.error('Failed to update summon uncap:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -730,7 +690,6 @@
|
||||||
// Provide services to child components via context
|
// Provide services to child components via context
|
||||||
setContext('party', {
|
setContext('party', {
|
||||||
getParty: () => party,
|
getParty: () => party,
|
||||||
updateParty: (p: Party) => (party = p),
|
|
||||||
canEdit: () => canEdit(),
|
canEdit: () => canEdit(),
|
||||||
getEditKey: () => editKey,
|
getEditKey: () => editKey,
|
||||||
services: {
|
services: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<!-- PartySegmentedControl Component -->
|
<!-- PartySegmentedControl Component -->
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import type { Party, GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
import type { Party, GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||||
|
|
@ -19,6 +21,14 @@
|
||||||
|
|
||||||
let { selectedTab = GridType.Character, onTabChange, party, class: className }: Props = $props()
|
let { selectedTab = GridType.Character, onTabChange, party, class: className }: Props = $props()
|
||||||
|
|
||||||
|
// Derived values to ensure reactivity propagates through snippet boundaries
|
||||||
|
// When party updates from TanStack Query cache, these will trigger re-renders
|
||||||
|
const weapons = $derived(party.weapons)
|
||||||
|
const summons = $derived(party.summons)
|
||||||
|
const characters = $derived(party.characters)
|
||||||
|
const job = $derived(party.job)
|
||||||
|
const element = $derived(party.element)
|
||||||
|
|
||||||
// Handle value changes
|
// Handle value changes
|
||||||
let value = $state(selectedTab)
|
let value = $state(selectedTab)
|
||||||
|
|
||||||
|
|
@ -45,10 +55,10 @@
|
||||||
selected={value === GridType.Character}
|
selected={value === GridType.Character}
|
||||||
>
|
>
|
||||||
<CharacterRep
|
<CharacterRep
|
||||||
job={party.job}
|
job={job}
|
||||||
element={party.element}
|
element={element}
|
||||||
gender={userGender}
|
gender={userGender}
|
||||||
characters={party.characters}
|
characters={characters}
|
||||||
/>
|
/>
|
||||||
</RepSegment>
|
</RepSegment>
|
||||||
|
|
||||||
|
|
@ -57,7 +67,7 @@
|
||||||
label={m.party_segmented_control_weapons()}
|
label={m.party_segmented_control_weapons()}
|
||||||
selected={value === GridType.Weapon}
|
selected={value === GridType.Weapon}
|
||||||
>
|
>
|
||||||
<WeaponRep weapons={party.weapons} />
|
<WeaponRep weapons={weapons} />
|
||||||
</RepSegment>
|
</RepSegment>
|
||||||
|
|
||||||
<RepSegment
|
<RepSegment
|
||||||
|
|
@ -65,7 +75,7 @@
|
||||||
label={m.party_segmented_control_summons()}
|
label={m.party_segmented_control_summons()}
|
||||||
selected={value === GridType.Summon}
|
selected={value === GridType.Summon}
|
||||||
>
|
>
|
||||||
<SummonRep summons={party.summons} />
|
<SummonRep summons={summons} />
|
||||||
</RepSegment>
|
</RepSegment>
|
||||||
</SegmentedControl>
|
</SegmentedControl>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@
|
||||||
const grid = $derived(
|
const grid = $derived(
|
||||||
Array.from({ length: 9 }, (_, i) => weapons.find((w: GridWeapon) => w?.position === i))
|
Array.from({ length: 9 }, (_, i) => weapons.find((w: GridWeapon) => w?.position === i))
|
||||||
)
|
)
|
||||||
|
// Pre-compute rows as explicit derived to ensure reactivity propagates to template
|
||||||
|
const rows = $derived(
|
||||||
|
Array.from({ length: 3 }, (_, rowIndex) => grid.slice(rowIndex * 3, (rowIndex + 1) * 3))
|
||||||
|
)
|
||||||
|
|
||||||
function weaponImageUrl(w?: GridWeapon, isMain = false): string {
|
function weaponImageUrl(w?: GridWeapon, isMain = false): string {
|
||||||
const variant = isMain ? 'main' : 'grid'
|
const variant = isMain ? 'main' : 'grid'
|
||||||
|
|
@ -35,9 +38,9 @@
|
||||||
/>{/if}
|
/>{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="weapons">
|
<div class="weapons">
|
||||||
{#each Array.from( { length: 3 }, (_, rowIndex) => grid.slice(rowIndex * 3, (rowIndex + 1) * 3) ) as row, rowIndex}
|
{#each rows as row, rowIndex (rowIndex)}
|
||||||
<ul class="weapon-row">
|
<ul class="weapon-row">
|
||||||
{#each row as w, colIndex}
|
{#each row as w, colIndex (rowIndex * 3 + colIndex)}
|
||||||
<li class="weapon" class:empty={!w}>
|
<li class="weapon" class:empty={!w}>
|
||||||
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} loading="lazy" decoding="async" />{/if}
|
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} loading="lazy" decoding="async" />{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue