diff --git a/web/src/hooks/queries/useAllUserCollections.test.ts b/web/src/hooks/queries/useAllUserCollections.test.ts new file mode 100644 index 00000000..70b4eac6 --- /dev/null +++ b/web/src/hooks/queries/useAllUserCollections.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import type { LibraryCollection } from "@/api/types"; +import { buildAllUserCollectionOptions } from "./useAllUserCollections"; + +function libraryCollection(id: string, title: string): LibraryCollection { + return { + id, + title, + collection_type: "smart", + source_config: {}, + last_sync_status: "success", + } as LibraryCollection; +} + +describe("buildAllUserCollectionOptions", () => { + it("keeps personal collections first and identifies their source", () => { + const options = buildAllUserCollectionOptions( + [{ id: 1, name: "Movies" }], + [{ id: "personal", name: "Weekend Picks" }], + [[libraryCollection("library", "Staff Picks")]], + ); + + expect(options.map(({ id, source, group }) => ({ id, source, group }))).toEqual([ + { id: "personal", source: "user", group: "My Collections" }, + { id: "library", source: "library", group: "Movies" }, + ]); + }); + + it("lists a multi-library collection once with its combined scope", () => { + const shared = libraryCollection("shared", "Network Originals"); + + const options = buildAllUserCollectionOptions( + [ + { id: 1, name: "Movies" }, + { id: 2, name: "TV Shows" }, + ], + undefined, + [[shared], [shared]], + ); + + expect(options).toHaveLength(1); + expect(options[0]).toMatchObject({ + id: "shared", + title: "Network Originals", + source: "library", + group: "Movies, TV Shows", + library_name: "Movies, TV Shows", + }); + }); + + it("keeps separate same-titled collections distinct", () => { + const options = buildAllUserCollectionOptions( + [ + { id: 1, name: "Movies" }, + { id: 2, name: "TV Shows" }, + ], + undefined, + [ + [libraryCollection("movies", "Network Originals")], + [libraryCollection("shows", "Network Originals")], + ], + ); + + expect(options.map(({ id, group }) => ({ id, group }))).toEqual([ + { id: "movies", group: "Movies" }, + { id: "shows", group: "TV Shows" }, + ]); + }); +}); diff --git a/web/src/hooks/queries/useAllUserCollections.ts b/web/src/hooks/queries/useAllUserCollections.ts index 99e0495b..51095dfa 100644 --- a/web/src/hooks/queries/useAllUserCollections.ts +++ b/web/src/hooks/queries/useAllUserCollections.ts @@ -1,5 +1,5 @@ import { useQueries } from "@tanstack/react-query"; -import type { LibraryCollection } from "@/api/types"; +import type { Collection, LibraryCollection } from "@/api/types"; import { useUserLibraries } from "./libraries"; import { useCollections } from "./collections"; import { getLibraryCollectionList, libraryCollectionsQueryOptions } from "./libraryCollections"; @@ -16,6 +16,60 @@ export interface CollectionOption { last_sync_status?: LibraryCollection["last_sync_status"]; } +type LibrarySummary = { id: number; name: string }; +type UserCollectionSummary = Pick; + +export function buildAllUserCollectionOptions( + libraries: readonly LibrarySummary[], + userCollections: readonly UserCollectionSummary[] | undefined, + libraryCollectionsByLibrary: ReadonlyArray, +): CollectionOption[] { + const collections: CollectionOption[] = []; + + for (const collection of userCollections ?? []) { + collections.push({ + id: collection.id, + title: collection.name, + source: "user", + group: "My Collections", + }); + } + + const libraryOptions = new Map(); + + for (let i = 0; i < libraries.length; i++) { + const library = libraries[i]!; + for (const collection of libraryCollectionsByLibrary[i] ?? []) { + const existing = libraryOptions.get(collection.id); + if (existing) { + if (!existing.libraryNames.includes(library.name)) { + existing.libraryNames.push(library.name); + const group = existing.libraryNames.join(", "); + existing.option.group = group; + existing.option.library_name = group; + } + continue; + } + + const option: CollectionOption = { + id: collection.id, + title: collection.title, + source: "library", + group: library.name, + library_id: library.id, + library_name: library.name, + collection_type: collection.collection_type, + source_config: collection.source_config, + last_sync_status: collection.last_sync_status, + }; + libraryOptions.set(collection.id, { option, libraryNames: [library.name] }); + collections.push(option); + } + } + + return collections; +} + export function useAllUserCollections() { const { data: libraries } = useUserLibraries(); const { data: userCollections, isLoading: userCollectionsLoading } = useCollections(); @@ -29,41 +83,14 @@ export function useAllUserCollections() { const isLoading = libraryQueries.some((q) => q.isLoading) || userCollectionsLoading; - const collections: CollectionOption[] = []; - - // Add personal user collections first (grouped under "My Collections"). - if (userCollections) { - for (const c of userCollections) { - collections.push({ - id: c.id, - title: c.name, - source: "user", - group: "My Collections", - }); - } - } - - // Add library collections grouped by library name. - if (libraries) { - for (let i = 0; i < libraries.length; i++) { - const lib = libraries[i]!; - const result = libraryQueries[i]; - const libraryCollections = Array.isArray(result?.data) ? result.data : []; - for (const c of libraryCollections) { - collections.push({ - id: c.id, - title: c.title, - source: "library", - group: lib.name, - library_id: lib.id, - library_name: lib.name, - collection_type: c.collection_type, - source_config: c.source_config, - last_sync_status: c.last_sync_status, - }); - } - } - } + const libraryCollectionsByLibrary = libraryQueries.map((result) => + Array.isArray(result.data) ? result.data : undefined, + ); + const collections = buildAllUserCollectionOptions( + libraries ?? [], + userCollections, + libraryCollectionsByLibrary, + ); return { collections, isLoading }; }