Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
`@behindthemusictree/genre-tree-view` `1.6.0`), which renders inside the library's own
info panel. This replaces the standalone `GenreDetailPanel` component, which has been
removed along with its export from `@behindthemusictree/app-kit`.
- **genre-tree**: `CriteriaDetailed` now includes `summary` (nullable string), matching the
backend's detail genre response, and `GenreTreeView`'s `renderExtraDetails` now displays it
above the essential tracks list when present.

### Fixed

Expand Down
38 changes: 37 additions & 1 deletion packages/app-kit/src/genre-tree/GenreTreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ describe("GenreTreeView", () => {
data: {
uuid: "c1",
name: "Jazz",
summary: null,
tracksCount: 5,
tracksArchivedCount: 2,
children: [],
Expand All @@ -749,6 +750,39 @@ describe("GenreTreeView", () => {
expect(screen.getByText(/2/)).toBeInTheDocument();
});

it("renders the genre summary for the selected node", () => {
useListFullGenrePlaylistsMock.mockReturnValue({
data: { results: [makePlaylist({ uuid: "gp1", criteria: { uuid: "c1", name: "Jazz" } })] },
isPending: false,
});
useFetchGenreDetailMock.mockImplementation((id: string | null) =>
id === "c1"
? {
data: {
uuid: "c1",
name: "Jazz",
summary: "Improvised music with swung rhythms.",
tracksCount: 5,
tracksArchivedCount: 0,
children: [],
essentialTracks: [],
},
isPending: false,
}
: { data: undefined, isPending: false },
);
renderView();

selectGenre();

const output = treeWheelPropsMock.mock.calls.at(-1)?.[0].renderExtraDetails({
id: "gp1",
});
render(<>{output}</>);

expect(screen.getByText("Improvised music with swung rhythms.")).toBeInTheDocument();
});

it("returns null when the node doesn't match the selected genre (e.g. info-panel chip navigation)", () => {
useListFullGenrePlaylistsMock.mockReturnValue({
data: {
Expand All @@ -765,6 +799,7 @@ describe("GenreTreeView", () => {
data: {
uuid: "c1",
name: "Jazz",
summary: null,
tracksCount: 5,
tracksArchivedCount: 0,
children: [],
Expand Down Expand Up @@ -804,7 +839,7 @@ describe("GenreTreeView", () => {
expect(output).toBeNull();
});

it("returns null when there are no essential tracks and nothing archived", () => {
it("returns null when there is no summary, no essential tracks, and nothing archived", () => {
useListFullGenrePlaylistsMock.mockReturnValue({
data: { results: [makePlaylist({ uuid: "gp1", criteria: { uuid: "c1", name: "Jazz" } })] },
isPending: false,
Expand All @@ -815,6 +850,7 @@ describe("GenreTreeView", () => {
data: {
uuid: "c1",
name: "Jazz",
summary: null,
tracksCount: 5,
tracksArchivedCount: 0,
children: [],
Expand Down
5 changes: 3 additions & 2 deletions packages/app-kit/src/genre-tree/GenreTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,14 @@ export function GenreTreeView<T extends TrackBase>({
return null;
}

const { essentialTracks, tracksArchivedCount } = selectedGenreDetail;
if (essentialTracks.length === 0 && tracksArchivedCount === 0) {
const { summary, essentialTracks, tracksArchivedCount } = selectedGenreDetail;
if (!summary && essentialTracks.length === 0 && tracksArchivedCount === 0) {
return null;
}

return (
<div className="flex flex-col gap-3 text-sm text-gray-700">
{summary && <p>{summary}</p>}
{tracksArchivedCount > 0 && (
<div>
<span className="font-semibold">Archived tracks: </span>
Expand Down
10 changes: 10 additions & 0 deletions packages/app-kit/src/genre-tree/schemas/criteria/criteria.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe("CriteriaDetailedSchema", () => {
const valid = {
uuid,
name: "Rock",
summary: "A genre summary",
parent: null,
ascendants: [],
descendants: [],
Expand All @@ -61,6 +62,15 @@ describe("CriteriaDetailedSchema", () => {
it("rejects an invalid essentialTracks entry", () => {
expect(() => CriteriaDetailedSchema.parse({ ...valid, essentialTracks: [{ uuid: "not-a-uuid" }] })).toThrow();
});

it("accepts a null summary", () => {
expect(() => CriteriaDetailedSchema.parse({ ...valid, summary: null })).not.toThrow();
});

it("rejects a shape missing summary", () => {
const { summary: _summary, ...invalid } = valid;
expect(() => CriteriaDetailedSchema.parse(invalid)).toThrow();
});
});

describe("CriteriaCreationSchema", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CriteriaLineageRelWithoutDescendantSchema } from "./lineage-rel/without

export const CriteriaDetailedSchema = UuidResourceSchema.extend({
name: z.string(),
summary: z.string().nullable(),
parent: CriteriaMinimumSchema.nullable(),
ascendants: z.array(CriteriaLineageRelWithoutDescendantSchema),
descendants: z.array(CriteriaLineageRelWithoutAscendantSchema),
Expand Down
Loading