add optimistic updates and description tile to teams/new
This commit is contained in:
parent
2c2580fba3
commit
4fc7af85a6
1 changed files with 174 additions and 49 deletions
|
|
@ -46,6 +46,12 @@
|
||||||
} from '$lib/api/mutations/grid.mutations'
|
} from '$lib/api/mutations/grid.mutations'
|
||||||
import { Dialog } from 'bits-ui'
|
import { Dialog } from 'bits-ui'
|
||||||
import { replaceState } from '$app/navigation'
|
import { replaceState } from '$app/navigation'
|
||||||
|
import DescriptionTile from '$lib/components/party/info/DescriptionTile.svelte'
|
||||||
|
import { openDescriptionPane } from '$lib/features/description/openDescriptionPane.svelte'
|
||||||
|
import {
|
||||||
|
openPartyEditSidebar,
|
||||||
|
type PartyEditValues
|
||||||
|
} from '$lib/features/party/openPartyEditSidebar.svelte'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -127,22 +133,25 @@
|
||||||
openJobSelectionSidebar({
|
openJobSelectionSidebar({
|
||||||
currentJobId: party.job?.id,
|
currentJobId: party.job?.id,
|
||||||
onSelectJob: async (job) => {
|
onSelectJob: async (job) => {
|
||||||
// If party exists, update via API
|
// Get the cache key being used by the query
|
||||||
|
const cacheKey = shortcode || 'new'
|
||||||
|
|
||||||
|
// Optimistically update cache first for immediate UI response
|
||||||
|
queryClient.setQueryData(partyKeys.detail(cacheKey), (old: Party | undefined) => {
|
||||||
|
if (!old) return { ...placeholderParty, job }
|
||||||
|
return { ...old, job }
|
||||||
|
})
|
||||||
|
|
||||||
|
// If party exists, persist to API
|
||||||
if (partyId && shortcode) {
|
if (partyId && shortcode) {
|
||||||
try {
|
try {
|
||||||
await partyAdapter.updateJob(shortcode, job.id)
|
await partyAdapter.updateJob(shortcode, job.id)
|
||||||
// Cache will be updated via invalidation
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to update job:', e)
|
console.error('Failed to update job:', e)
|
||||||
errorMessage = e instanceof Error ? e.message : 'Failed to update job'
|
errorMessage = e instanceof Error ? e.message : 'Failed to update job'
|
||||||
errorDialogOpen = true
|
errorDialogOpen = true
|
||||||
|
// Revert on error would go here if needed
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Update cache locally for new party
|
|
||||||
queryClient.setQueryData(partyKeys.detail('new'), (old: Party | undefined) => {
|
|
||||||
if (!old) return placeholderParty
|
|
||||||
return { ...old, job }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -154,11 +163,20 @@
|
||||||
currentSkills: party.jobSkills,
|
currentSkills: party.jobSkills,
|
||||||
targetSlot: slot,
|
targetSlot: slot,
|
||||||
onSelectSkill: async (skill) => {
|
onSelectSkill: async (skill) => {
|
||||||
// If party exists, update via API
|
// Get the cache key being used by the query
|
||||||
if (partyId && shortcode) {
|
const cacheKey = shortcode || 'new'
|
||||||
try {
|
|
||||||
const updatedSkills = { ...party.jobSkills }
|
const updatedSkills = { ...party.jobSkills }
|
||||||
updatedSkills[String(slot) as keyof typeof updatedSkills] = skill
|
updatedSkills[String(slot) as keyof typeof updatedSkills] = skill
|
||||||
|
|
||||||
|
// Optimistically update cache first for immediate UI response
|
||||||
|
queryClient.setQueryData(partyKeys.detail(cacheKey), (old: Party | undefined) => {
|
||||||
|
if (!old) return { ...placeholderParty, jobSkills: updatedSkills }
|
||||||
|
return { ...old, jobSkills: updatedSkills }
|
||||||
|
})
|
||||||
|
|
||||||
|
// If party exists, persist to API
|
||||||
|
if (partyId && shortcode) {
|
||||||
|
try {
|
||||||
const skillsArray = transformSkillsToArray(updatedSkills)
|
const skillsArray = transformSkillsToArray(updatedSkills)
|
||||||
await partyAdapter.updateJobSkills(shortcode, skillsArray)
|
await partyAdapter.updateJobSkills(shortcode, skillsArray)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -166,14 +184,6 @@
|
||||||
errorMessage = e instanceof Error ? e.message : 'Failed to update skill'
|
errorMessage = e instanceof Error ? e.message : 'Failed to update skill'
|
||||||
errorDialogOpen = true
|
errorDialogOpen = true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Update cache locally for new party
|
|
||||||
queryClient.setQueryData(partyKeys.detail('new'), (old: Party | undefined) => {
|
|
||||||
if (!old) return placeholderParty
|
|
||||||
const updatedSkills = { ...old.jobSkills }
|
|
||||||
updatedSkills[String(slot) as keyof typeof updatedSkills] = skill
|
|
||||||
return { ...old, jobSkills: updatedSkills }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRemoveSkill: async () => {
|
onRemoveSkill: async () => {
|
||||||
|
|
@ -183,10 +193,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveJobSkill(slot: number) {
|
async function handleRemoveJobSkill(slot: number) {
|
||||||
if (partyId && shortcode) {
|
// Get the cache key being used by the query
|
||||||
try {
|
const cacheKey = shortcode || 'new'
|
||||||
const updatedSkills = { ...party.jobSkills }
|
const updatedSkills = { ...party.jobSkills }
|
||||||
delete updatedSkills[String(slot) as keyof typeof updatedSkills]
|
delete updatedSkills[String(slot) as keyof typeof updatedSkills]
|
||||||
|
|
||||||
|
// Optimistically update cache first for immediate UI response
|
||||||
|
queryClient.setQueryData(partyKeys.detail(cacheKey), (old: Party | undefined) => {
|
||||||
|
if (!old) return { ...placeholderParty, jobSkills: updatedSkills }
|
||||||
|
return { ...old, jobSkills: updatedSkills }
|
||||||
|
})
|
||||||
|
|
||||||
|
// If party exists, persist to API
|
||||||
|
if (partyId && shortcode) {
|
||||||
|
try {
|
||||||
const skillsArray = transformSkillsToArray(updatedSkills)
|
const skillsArray = transformSkillsToArray(updatedSkills)
|
||||||
await partyAdapter.updateJobSkills(shortcode, skillsArray)
|
await partyAdapter.updateJobSkills(shortcode, skillsArray)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -194,15 +214,130 @@
|
||||||
errorMessage = e instanceof Error ? e.message : 'Failed to remove skill'
|
errorMessage = e instanceof Error ? e.message : 'Failed to remove skill'
|
||||||
errorDialogOpen = true
|
errorDialogOpen = true
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Update cache locally for new party
|
}
|
||||||
queryClient.setQueryData(partyKeys.detail('new'), (old: Party | undefined) => {
|
|
||||||
|
// Helper to ensure party exists before saving metadata
|
||||||
|
async function ensurePartyExists(): Promise<{ id: string; shortcode: string }> {
|
||||||
|
if (partyId && shortcode) {
|
||||||
|
return { id: partyId, shortcode }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create party with current metadata from cache
|
||||||
|
const partyPayload: any = {
|
||||||
|
name: party.name || 'New Team',
|
||||||
|
visibility: 1,
|
||||||
|
element: party.element || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
partyPayload.localId = getLocalId()
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreatingParty = true
|
||||||
|
try {
|
||||||
|
const createdParty = await createPartyMutation.mutateAsync(partyPayload)
|
||||||
|
|
||||||
|
partyId = createdParty.id
|
||||||
|
shortcode = createdParty.shortcode
|
||||||
|
|
||||||
|
if (createdParty.editKey) {
|
||||||
|
storeEditKey(createdParty.shortcode, createdParty.editKey)
|
||||||
|
storeEditKey(createdParty.id, createdParty.editKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update URL
|
||||||
|
replaceState(`/teams/${createdParty.shortcode}`, {})
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
queryClient.setQueryData(partyKeys.detail(createdParty.shortcode), createdParty)
|
||||||
|
|
||||||
|
return { id: createdParty.id, shortcode: createdParty.shortcode }
|
||||||
|
} finally {
|
||||||
|
isCreatingParty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description/Edit handlers
|
||||||
|
function handleOpenDescription() {
|
||||||
|
openDescriptionPane({
|
||||||
|
title: party.name || 'New Team',
|
||||||
|
description: party.description,
|
||||||
|
videoUrl: party.videoUrl,
|
||||||
|
canEdit: true,
|
||||||
|
partyId: partyId ?? undefined,
|
||||||
|
partyShortcode: shortcode ?? undefined,
|
||||||
|
onSave: async (description) => {
|
||||||
|
const { id, shortcode: sc } = await ensurePartyExists()
|
||||||
|
await partyAdapter.update({ id, shortcode: sc, description })
|
||||||
|
// Update cache
|
||||||
|
queryClient.setQueryData(partyKeys.detail(sc), (old: Party | undefined) => {
|
||||||
if (!old) return placeholderParty
|
if (!old) return placeholderParty
|
||||||
const updatedSkills = { ...old.jobSkills }
|
return { ...old, description }
|
||||||
delete updatedSkills[String(slot) as keyof typeof updatedSkills]
|
|
||||||
return { ...old, jobSkills: updatedSkills }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenEdit() {
|
||||||
|
const initialValues: PartyEditValues = {
|
||||||
|
name: party.name ?? 'New Team',
|
||||||
|
description: party.description ?? null,
|
||||||
|
fullAuto: party.fullAuto ?? false,
|
||||||
|
autoGuard: party.autoGuard ?? false,
|
||||||
|
autoSummon: party.autoSummon ?? false,
|
||||||
|
chargeAttack: party.chargeAttack ?? true,
|
||||||
|
clearTime: party.clearTime ?? null,
|
||||||
|
buttonCount: party.buttonCount ?? null,
|
||||||
|
chainCount: party.chainCount ?? null,
|
||||||
|
summonCount: party.summonCount ?? null,
|
||||||
|
videoUrl: party.videoUrl ?? null,
|
||||||
|
raid: party.raid ?? null,
|
||||||
|
raidId: party.raid?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
openPartyEditSidebar({
|
||||||
|
initialValues,
|
||||||
|
onSave: async (values) => {
|
||||||
|
const { id, shortcode: sc } = await ensurePartyExists()
|
||||||
|
await partyAdapter.update({
|
||||||
|
id,
|
||||||
|
shortcode: sc,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description ?? undefined,
|
||||||
|
fullAuto: values.fullAuto,
|
||||||
|
autoGuard: values.autoGuard,
|
||||||
|
autoSummon: values.autoSummon,
|
||||||
|
chargeAttack: values.chargeAttack,
|
||||||
|
clearTime: values.clearTime ?? undefined,
|
||||||
|
buttonCount: values.buttonCount ?? undefined,
|
||||||
|
chainCount: values.chainCount ?? undefined,
|
||||||
|
summonCount: values.summonCount ?? undefined,
|
||||||
|
videoUrl: values.videoUrl ?? undefined,
|
||||||
|
raidId: values.raidId ?? undefined
|
||||||
|
})
|
||||||
|
// Update cache
|
||||||
|
queryClient.setQueryData(partyKeys.detail(sc), (old: Party | undefined) => {
|
||||||
|
if (!old) return placeholderParty
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
fullAuto: values.fullAuto,
|
||||||
|
autoGuard: values.autoGuard,
|
||||||
|
autoSummon: values.autoSummon,
|
||||||
|
chargeAttack: values.chargeAttack,
|
||||||
|
clearTime: values.clearTime,
|
||||||
|
buttonCount: values.buttonCount,
|
||||||
|
chainCount: values.chainCount,
|
||||||
|
summonCount: values.summonCount,
|
||||||
|
videoUrl: values.videoUrl,
|
||||||
|
raid: values.raid,
|
||||||
|
raidId: values.raidId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Party state
|
// Party state
|
||||||
|
|
@ -622,12 +757,15 @@
|
||||||
<main>
|
<main>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<section class="party-content">
|
<section class="party-content">
|
||||||
<header class="party-header">
|
<div class="description-tile-wrapper">
|
||||||
<div class="party-info">
|
<DescriptionTile
|
||||||
<h1>Create a new team</h1>
|
name={party.name}
|
||||||
<p class="description">Search and click items to add them to your grid</p>
|
description={party.description}
|
||||||
|
canEdit={true}
|
||||||
|
onOpenDescription={handleOpenDescription}
|
||||||
|
onOpenEdit={handleOpenEdit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<PartySegmentedControl
|
<PartySegmentedControl
|
||||||
selectedTab={activeTab}
|
selectedTab={activeTab}
|
||||||
|
|
@ -718,23 +856,10 @@
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.party-header {
|
.description-tile-wrapper {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: start;
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.party-info h1 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
color: #666;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-content {
|
.party-content {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue