refactor: apply DRY improvements to Party.svelte

- Add runPartyMutation helper for generic mutation handling
- Add type-indexed maps for handleSwap/handleMove operations
- Add mergeUpdatedGridItem helper for uncap state merging
- Add setErrorAndLog helper for centralized error handling
- Refactor clientGridService methods to use new helpers
- Reduces code duplication significantly (~130 lines removed)

Co-Authored-By: Justin Edmund <justin@jedmund.com>
This commit is contained in:
Devin AI 2025-11-29 09:34:16 +00:00
parent bb1193c5af
commit 5b61207494

View file

@ -123,6 +123,104 @@
const updateJobSkillsMutation = useUpdatePartyJobSkills() const updateJobSkillsMutation = useUpdatePartyJobSkills()
const removeJobSkillMutation = useRemovePartyJobSkill() const removeJobSkillMutation = useRemovePartyJobSkill()
// ============================================================================
// DRY Helper Functions
// ============================================================================
// Generic mutation wrapper that handles common patterns for mutations returning Party
type PartyMutation<TVars> = {
mutate: (
vars: TVars,
options: {
onSuccess: (updated: Party) => void
onError: (err: unknown) => void
}
) => void
}
function runPartyMutation<TVars>(
mutation: PartyMutation<TVars>,
vars: TVars,
actionLabel: string
): Promise<Party> {
return new Promise((resolve, reject) => {
mutation.mutate(vars, {
onSuccess: (updated) => {
party = updated
resolve(updated)
},
onError: (err) => {
console.error(`Failed to ${actionLabel}:`, err)
reject(err)
}
})
})
}
// Centralized error handling helper
function setErrorAndLog(e: any, defaultMessage: string) {
error = extractErrorMessage(e, defaultMessage)
console.error(defaultMessage, e)
}
// Type-indexed maps for grid operations
const deleteMutations = {
weapon: deleteWeaponMutation,
character: deleteCharacterMutation,
summon: deleteSummonMutation
} as const
const updateMutations = {
weapon: updateWeaponMutation,
character: updateCharacterMutation,
summon: updateSummonMutation
} as const
const moveMutations = {
weapon: moveWeaponMutation,
character: moveCharacterMutation,
summon: moveSummonMutation
} as const
// Generic helper to merge updated grid item into party state
type GridKind = 'weapons' | 'characters' | 'summons'
function mergeUpdatedGridItem(
kind: GridKind,
itemId: string,
response: any
): Party {
// Find the updated item in the response (handles both camelCase and snake_case)
const responseKeys = ['gridWeapon', 'grid_weapon', 'gridCharacter', 'grid_character', 'gridSummon', 'grid_summon']
const updatedKey = Object.keys(response).find((k) => responseKeys.includes(k))
const updatedItem = updatedKey ? response[updatedKey] : null
if (!updatedItem) return party
const updatedParty: Party = { ...party }
const list = updatedParty[kind] as any[] | undefined
if (!list) return party
const index = list.findIndex((x: any) => x.id === itemId)
if (index === -1) return party
const existing = list[index]
list[index] = {
...existing,
id: existing.id,
position: existing.position,
// Preserve the base object (weapon/character/summon)
...(kind === 'weapons' && { weapon: existing.weapon }),
...(kind === 'characters' && { character: existing.character }),
...(kind === 'summons' && { summon: existing.summon }),
// Update uncap fields from response (handles both camelCase and snake_case)
uncapLevel: updatedItem.uncapLevel ?? updatedItem.uncap_level,
transcendenceStep: updatedItem.transcendenceStep ?? updatedItem.transcendence_step
}
return updatedParty
}
// Create drag-drop context // Create drag-drop context
const dragContext = createDragDropContext({ const dragContext = createDragDropContext({
onLocalUpdate: async (operation) => { onLocalUpdate: async (operation) => {
@ -191,35 +289,21 @@
throw new Error('Invalid swap operation - missing items') throw new Error('Invalid swap operation - missing items')
} }
// Call appropriate grid service method based on type // Use type-indexed map to get the appropriate move function
if (source.type === 'weapon') { const gridMoveFns = {
await gridService.moveWeapon(party.id, source.itemId, target.position, editKey || undefined, { weapon: () => gridService.moveWeapon(party.id, source.itemId, target.position, editKey || undefined, { shortcode: party.shortcode }),
shortcode: party.shortcode character: () => gridService.moveCharacter(party.id, source.itemId, target.position, editKey || undefined, { shortcode: party.shortcode }),
}) summon: () => gridService.moveSummon(party.id, source.itemId, target.position, editKey || undefined, { shortcode: party.shortcode })
} else if (source.type === 'character') { } as const
await gridService.moveCharacter(
party.id, const moveFn = gridMoveFns[source.type as keyof typeof gridMoveFns]
source.itemId, if (!moveFn) throw new Error(`Unknown item type: ${source.type}`)
target.position,
editKey || undefined, await moveFn()
{
shortcode: party.shortcode
}
)
} else if (source.type === 'summon') {
await gridService.moveSummon(party.id, source.itemId, target.position, editKey || undefined, {
shortcode: party.shortcode
})
} else {
throw new Error(`Unknown item type: ${source.type}`)
}
// Clear cache and refresh party data // Clear cache and refresh party data
partyService.clearPartyCache(party.shortcode) partyService.clearPartyCache(party.shortcode)
const updated = await partyService.getByShortcode(party.shortcode) return await partyService.getByShortcode(party.shortcode)
return updated
throw new Error(`Unknown item type: ${source.type}`)
} }
async function handleMove(source: any, target: any): Promise<Party> { async function handleMove(source: any, target: any): Promise<Party> {
@ -232,31 +316,21 @@
throw new Error('Invalid move operation') throw new Error('Invalid move operation')
} }
// Call appropriate grid service method based on type // Use type-indexed map to get the appropriate move function
if (source.type === 'character') { const gridMoveFns = {
await gridService.moveCharacter( weapon: () => gridService.moveWeapon(party.id, source.itemId, target.position, editKey || undefined, { shortcode: party.shortcode }),
party.id, character: () => gridService.moveCharacter(party.id, source.itemId, target.position, editKey || undefined, { shortcode: party.shortcode }),
source.itemId, summon: () => gridService.moveSummon(party.id, source.itemId, target.position, editKey || undefined, { shortcode: party.shortcode })
target.position, } as const
editKey || undefined,
{ shortcode: party.shortcode } const moveFn = gridMoveFns[source.type as keyof typeof gridMoveFns]
) if (!moveFn) throw new Error(`Unknown item type: ${source.type}`)
} else if (source.type === 'weapon') {
await gridService.moveWeapon(party.id, source.itemId, target.position, editKey || undefined, { await moveFn()
shortcode: party.shortcode
})
} else if (source.type === 'summon') {
await gridService.moveSummon(party.id, source.itemId, target.position, editKey || undefined, {
shortcode: party.shortcode
})
} else {
throw new Error(`Unknown item type: ${source.type}`)
}
// Clear cache and refresh party data // Clear cache and refresh party data
partyService.clearPartyCache(party.shortcode) partyService.clearPartyCache(party.shortcode)
const updated = await partyService.getByShortcode(party.shortcode) return await partyService.getByShortcode(party.shortcode)
return updated
} }
// Localized name helper: accepts either an object with { name: { en, ja } } // Localized name helper: accepts either an object with { name: { en, ja } }
@ -663,114 +737,51 @@
// Create client-side wrappers for grid operations using mutations // Create client-side wrappers for grid operations using mutations
// These return promises that resolve when the mutation completes // These return promises that resolve when the mutation completes
// Uses runPartyMutation helper for DRY code
const clientGridService = { const clientGridService = {
removeWeapon(partyId: string, gridWeaponId: string, _editKey?: string): Promise<Party> { removeWeapon(partyId: string, gridWeaponId: string, _editKey?: string): Promise<Party> {
return new Promise((resolve, reject) => { return runPartyMutation(
deleteWeaponMutation.mutate( deleteWeaponMutation,
{ id: gridWeaponId, partyId, partyShortcode: party.shortcode }, { id: gridWeaponId, partyId, partyShortcode: party.shortcode },
{ 'remove weapon'
onSuccess: (updated) => { )
party = updated
resolve(updated)
},
onError: (err) => {
console.error('Failed to remove weapon:', err)
reject(err)
}
}
)
})
}, },
removeSummon(partyId: string, gridSummonId: string, _editKey?: string): Promise<Party> { removeSummon(partyId: string, gridSummonId: string, _editKey?: string): Promise<Party> {
return new Promise((resolve, reject) => { return runPartyMutation(
deleteSummonMutation.mutate( deleteSummonMutation,
{ id: gridSummonId, partyId, partyShortcode: party.shortcode }, { id: gridSummonId, partyId, partyShortcode: party.shortcode },
{ 'remove summon'
onSuccess: (updated) => { )
party = updated
resolve(updated)
},
onError: (err) => {
console.error('Failed to remove summon:', err)
reject(err)
}
}
)
})
}, },
removeCharacter(partyId: string, gridCharacterId: string, _editKey?: string): Promise<Party> { removeCharacter(partyId: string, gridCharacterId: string, _editKey?: string): Promise<Party> {
return new Promise((resolve, reject) => { return runPartyMutation(
deleteCharacterMutation.mutate( deleteCharacterMutation,
{ id: gridCharacterId, partyId, partyShortcode: party.shortcode }, { id: gridCharacterId, partyId, partyShortcode: party.shortcode },
{ 'remove character'
onSuccess: (updated) => { )
party = updated
resolve(updated)
},
onError: (err) => {
console.error('Failed to remove character:', err)
reject(err)
}
}
)
})
}, },
updateWeapon(partyId: string, gridWeaponId: string, updates: any, _editKey?: string): Promise<Party> { updateWeapon(_partyId: string, gridWeaponId: string, updates: any, _editKey?: string): Promise<Party> {
return new Promise((resolve, reject) => { return runPartyMutation(
updateWeaponMutation.mutate( updateWeaponMutation,
{ id: gridWeaponId, partyShortcode: party.shortcode, updates }, { id: gridWeaponId, partyShortcode: party.shortcode, updates },
{ 'update weapon'
onSuccess: (updated) => { )
party = updated
resolve(updated)
},
onError: (err) => {
console.error('Failed to update weapon:', err)
reject(err)
}
}
)
})
}, },
updateSummon(partyId: string, gridSummonId: string, updates: any, _editKey?: string): Promise<Party> { updateSummon(_partyId: string, gridSummonId: string, updates: any, _editKey?: string): Promise<Party> {
return new Promise((resolve, reject) => { return runPartyMutation(
updateSummonMutation.mutate( updateSummonMutation,
{ id: gridSummonId, partyShortcode: party.shortcode, updates }, { id: gridSummonId, partyShortcode: party.shortcode, updates },
{ 'update summon'
onSuccess: (updated) => { )
party = updated
resolve(updated)
},
onError: (err) => {
console.error('Failed to update summon:', err)
reject(err)
}
}
)
})
}, },
updateCharacter( updateCharacter(_partyId: string, gridCharacterId: string, updates: any, _editKey?: string): Promise<Party> {
partyId: string, return runPartyMutation(
gridCharacterId: string, updateCharacterMutation,
updates: any, { id: gridCharacterId, partyShortcode: party.shortcode, updates },
_editKey?: string 'update character'
): Promise<Party> { )
return new Promise((resolve, reject) => {
updateCharacterMutation.mutate(
{ id: gridCharacterId, partyShortcode: party.shortcode, updates },
{
onSuccess: (updated) => {
party = updated
resolve(updated)
},
onError: (err) => {
console.error('Failed to update character:', err)
reject(err)
}
}
)
})
}, },
// Uncap methods use mergeUpdatedGridItem helper for DRY state merging
updateCharacterUncap( updateCharacterUncap(
gridCharacterId: string, gridCharacterId: string,
uncapLevel?: number, uncapLevel?: number,
@ -788,34 +799,9 @@
}, },
{ {
onSuccess: (response) => { onSuccess: (response) => {
// The API returns {gridCharacter: {...}} with the updated item only const updatedParty = mergeUpdatedGridItem('characters', gridCharacterId, response)
// We need to update just that character in the current party state party = updatedParty
if (response.gridCharacter || response.grid_character) { resolve(updatedParty)
const updatedChar = response.gridCharacter || response.grid_character
const updatedParty = { ...party }
if (updatedParty.characters) {
const charIndex = updatedParty.characters.findIndex(
(c: any) => c.id === gridCharacterId
)
if (charIndex !== -1) {
const existingChar = updatedParty.characters[charIndex]
if (existingChar) {
updatedParty.characters[charIndex] = {
...existingChar,
id: existingChar.id,
position: existingChar.position,
character: existingChar.character,
uncapLevel: updatedChar.uncapLevel ?? updatedChar.uncap_level,
transcendenceStep: updatedChar.transcendenceStep ?? updatedChar.transcendence_step
}
}
party = updatedParty
resolve(updatedParty)
return
}
}
}
resolve(party)
}, },
onError: (err) => { onError: (err) => {
console.error('Failed to update character uncap:', err) console.error('Failed to update character uncap:', err)
@ -842,33 +828,9 @@
}, },
{ {
onSuccess: (response) => { onSuccess: (response) => {
// The API returns {gridWeapon: {...}} with the updated item only const updatedParty = mergeUpdatedGridItem('weapons', gridWeaponId, response)
// We need to update just that weapon in the current party state party = updatedParty
if (response.gridWeapon || response.grid_weapon) { resolve(updatedParty)
const updatedWeapon = response.gridWeapon || response.grid_weapon
const updatedParty = { ...party }
if (updatedParty.weapons) {
const weaponIndex = updatedParty.weapons.findIndex((w: any) => w.id === gridWeaponId)
if (weaponIndex !== -1) {
const existingWeapon = updatedParty.weapons[weaponIndex]
if (existingWeapon) {
updatedParty.weapons[weaponIndex] = {
...existingWeapon,
id: existingWeapon.id,
position: existingWeapon.position,
weapon: existingWeapon.weapon,
uncapLevel: updatedWeapon.uncapLevel ?? updatedWeapon.uncap_level,
transcendenceStep:
updatedWeapon.transcendenceStep ?? updatedWeapon.transcendence_step
}
}
party = updatedParty
resolve(updatedParty)
return
}
}
}
resolve(party)
}, },
onError: (err) => { onError: (err) => {
console.error('Failed to update weapon uncap:', err) console.error('Failed to update weapon uncap:', err)
@ -895,33 +857,9 @@
}, },
{ {
onSuccess: (response) => { onSuccess: (response) => {
// The API returns {gridSummon: {...}} with the updated item only const updatedParty = mergeUpdatedGridItem('summons', gridSummonId, response)
// We need to update just that summon in the current party state party = updatedParty
if (response.gridSummon || response.grid_summon) { resolve(updatedParty)
const updatedSummon = response.gridSummon || response.grid_summon
const updatedParty = { ...party }
if (updatedParty.summons) {
const summonIndex = updatedParty.summons.findIndex((s: any) => s.id === gridSummonId)
if (summonIndex !== -1) {
const existingSummon = updatedParty.summons[summonIndex]
if (existingSummon) {
updatedParty.summons[summonIndex] = {
...existingSummon,
id: existingSummon.id,
position: existingSummon.position,
summon: existingSummon.summon,
uncapLevel: updatedSummon.uncapLevel ?? updatedSummon.uncap_level,
transcendenceStep:
updatedSummon.transcendenceStep ?? updatedSummon.transcendence_step
}
}
party = updatedParty
resolve(updatedParty)
return
}
}
}
resolve(party)
}, },
onError: (err) => { onError: (err) => {
console.error('Failed to update summon uncap:', err) console.error('Failed to update summon uncap:', err)