fix(web): dedupe multi-library collection options (#601)
This commit is contained in:
@@ -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" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user