fix(web): dedupe multi-library collection options (#601)

This commit is contained in:
Quick
2026-08-11 21:12:13 -04:00
committed by GitHub
parent 2dc7d5e36c
commit 1dcdd4b27a
2 changed files with 132 additions and 36 deletions
@@ -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" },
]);
});
});
+63 -36
View File
@@ -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<Collection, "id" | "name">;
export function buildAllUserCollectionOptions(
libraries: readonly LibrarySummary[],
userCollections: readonly UserCollectionSummary[] | undefined,
libraryCollectionsByLibrary: ReadonlyArray<readonly LibraryCollection[] | undefined>,
): 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<string, { option: CollectionOption; libraryNames: string[] }>();
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 };
}