From 0a2a3894bfc4c0ce2f407478e353fd70c075d064 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 23:23:24 -0800 Subject: [PATCH] Add sync from collection menu and highlight owned items in search - Party.svelte: Add 'Sync from collection' to overflow menu for parties with linked collection items, with proper menu grouping - SearchContent.svelte: Highlight items user owns in 'All Items' mode with subtle green background and checkmark indicator --- src/lib/components/party/Party.svelte | 42 ++++++++++++++- .../components/sidebar/SearchContent.svelte | 52 ++++++++++++++++--- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/lib/components/party/Party.svelte b/src/lib/components/party/Party.svelte index 1b90f745..957dcab0 100644 --- a/src/lib/components/party/Party.svelte +++ b/src/lib/components/party/Party.svelte @@ -20,7 +20,8 @@ useUpdateSummonUncap, useSwapWeapons, useSwapCharacters, - useSwapSummons + useSwapSummons, + useSyncAllPartyItems } from '$lib/api/mutations/grid.mutations' // TanStack Query mutations - Party @@ -132,6 +133,7 @@ const swapWeapons = useSwapWeapons() const swapCharacters = useSwapCharacters() const swapSummons = useSwapSummons() + const syncAllItems = useSyncAllPartyItems() // TanStack Query mutations - Party const updatePartyMutation = useUpdateParty() @@ -269,6 +271,17 @@ const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element) const partyElement = $derived((party as any)?.element) + // Check if any items in the party are linked to collection (for sync menu option) + const hasCollectionLinks = $derived.by(() => { + const hasLinkedWeapons = (party?.weapons ?? []).some((w) => w?.collectionWeaponId) + const hasLinkedCharacters = (party?.characters ?? []).some((c) => c?.collectionCharacterId) + const hasLinkedSummons = (party?.summons ?? []).some((s) => s?.collectionSummonId) + return hasLinkedWeapons || hasLinkedCharacters || hasLinkedSummons + }) + + // Check if syncing is in progress + const isSyncingAll = $derived(syncAllItems.isPending) + function handleTabChange(tab: GridType) { activeTab = tab // Instant UI update @@ -368,6 +381,25 @@ } } + async function syncFromCollection() { + if (!canEdit() || !hasCollectionLinks) return + + loading = true + error = null + + try { + await syncAllItems.mutateAsync({ + partyId: party.id, + partyShortcode: party.shortcode + }) + // Party will be updated via cache invalidation + } catch (err: any) { + error = err.message || 'Failed to sync from collection' + } finally { + loading = false + } + } + let deleteDialogOpen = $state(false) let deleting = $state(false) @@ -846,6 +878,14 @@ + {#if hasCollectionLinks} + + + + {/if} + {/if} {#if authUserId} diff --git a/src/lib/components/sidebar/SearchContent.svelte b/src/lib/components/sidebar/SearchContent.svelte index 73ed22b5..8303aec1 100644 --- a/src/lib/components/sidebar/SearchContent.svelte +++ b/src/lib/components/sidebar/SearchContent.svelte @@ -151,7 +151,8 @@ } }) - // Collection query - only enabled when in collection mode and authUserId is provided + // Collection query - enabled when authUserId is provided + // Used both for collection mode AND for highlighting owned items in "all" mode // Type assertion needed because different types have different query result types // but they all share the same structure with different content types const collectionQueryResult = createInfiniteQuery(() => { @@ -163,26 +164,27 @@ } as ReturnType } - const currentFilters = { + // For collection mode, apply filters; for "all" mode, fetch all to build owned set + const currentFilters = searchMode === 'collection' ? { element: elementFilters.length > 0 ? elementFilters : undefined, rarity: rarityFilters.length > 0 ? rarityFilters : undefined - } + } : {} switch (type) { case 'weapon': return { ...collectionQueries.weapons(authUserId, currentFilters), - enabled: searchMode === 'collection' + enabled: true // Always enabled when authUserId exists } as unknown as ReturnType case 'character': return { ...collectionQueries.characters(authUserId, currentFilters), - enabled: searchMode === 'collection' + enabled: true } case 'summon': return { ...collectionQueries.summons(authUserId, currentFilters), - enabled: searchMode === 'collection' + enabled: true } as unknown as ReturnType } }) @@ -199,6 +201,25 @@ return filterCollectionByQuery(allItems, debouncedSearchQuery) }) + // Set of owned item granblue IDs for fast lookup (used in "all" mode to highlight owned items) + const ownedItemIds = $derived.by(() => { + const pages = collectionQueryResult.data?.pages ?? [] + const allItems = pages.flatMap((page) => page.results) + const ids = new Set() + for (const item of allItems) { + const entity = 'character' in item ? item.character : 'weapon' in item ? item.weapon : item.summon + if (entity.granblueId) { + ids.add(String(entity.granblueId)) + } + } + return ids + }) + + // Helper to check if an item is owned (in user's collection) + function isOwned(item: AddItemResult): boolean { + return ownedItemIds.has(String(item.granblueId)) + } + // Deduplicate by id - needed because the API may return the same item across pages // (e.g., due to items being added/removed between page fetches) const searchResults = $derived.by(() => { @@ -404,11 +425,13 @@ {:else if searchResults.length > 0}
    {#each searchResults as item (item.id)} + {@const owned = searchMode === 'all' && authUserId && isOwned(item)}