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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- Fixed playlist share-target dropdown silently truncating to 30 tenants; it now loads every page.
- Refactored InteractiveController to use a typed `InteractiveSlideActionInput` DTO; regenerated API spec and RTK types.
- Fixed multiple InstantBook bugs: interval boundary overlap, busy-interval timezone, per-resource spam-protect
throttling, duration validation, error responses (409/4xx), resource cache TTL, and assorted
Expand Down
24 changes: 18 additions & 6 deletions assets/admin/components/playlist/playlist-form.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useContext } from "react";
import { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { Alert } from "react-bootstrap";
import UserContext from "../../context/user-context";
import Schedule from "../util/schedule/schedule";
import { useGetV2TenantsQuery } from "../../../shared/redux/enhanced-api.ts";
import { enhancedApi } from "../../../shared/redux/enhanced-api.ts";
import getAllPages from "../util/helpers/get-all-pages.js";
import ContentBody from "../util/content-body/content-body";
import TenantsDropdown from "../util/forms/multiselect-dropdown/tenants/tenants-dropdown";

Expand All @@ -24,10 +26,20 @@ function PlaylistForm({
}) {
const { t } = useTranslation("common", { keyPrefix: "playlist-form" });
const context = useContext(UserContext);
const dispatch = useDispatch();
const [tenants, setTenants] = useState(null);

const { data: tenants } = useGetV2TenantsQuery({
itemsPerPage: 30,
});
useEffect(() => {
let cancelled = false;
getAllPages(dispatch, enhancedApi.endpoints.getV2Tenants, {
itemsPerPage: 30,
}).then((all) => {
if (!cancelled) setTenants(all);
});
return () => {
cancelled = true;
};
}, [dispatch]);

return (
<>
Expand All @@ -51,7 +63,7 @@ function PlaylistForm({
name="tenants"
handleTenantSelection={handleInput}
selected={playlist.tenants}
data={tenants["hydra:member"].filter(({ tenantKey }) => {
data={tenants.filter(({ tenantKey }) => {
return context.selectedTenant.get.tenantKey !== tenantKey;
})}
/>
Expand Down
127 changes: 127 additions & 0 deletions assets/tests/admin/playlist-form-tenants-pagination.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, cleanup, waitFor } from "@testing-library/react";

const {
capturedDropdownData,
initiateMock,
dispatchMock,
useGetV2TenantsQueryMock,
} = vi.hoisted(() => ({
capturedDropdownData: { current: null },
initiateMock: vi.fn(),
dispatchMock: vi.fn(),
useGetV2TenantsQueryMock: vi.fn(),
}));

vi.mock(
"../../admin/components/util/forms/multiselect-dropdown/tenants/tenants-dropdown",
() => ({
default: (props) => {
capturedDropdownData.current = props.data;
return null;
},
}),
);

vi.mock("../../admin/components/util/schedule/schedule", () => ({
default: () => null,
}));

vi.mock("../../admin/components/util/content-body/content-body", () => ({
default: ({ children }) => children,
}));

vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key) => key }),
}));

vi.mock("react-bootstrap", () => ({
Alert: ({ children }) => children,
}));

vi.mock("react-redux", () => ({
useDispatch: () => dispatchMock,
}));

vi.mock("../../shared/redux/enhanced-api.ts", () => ({
enhancedApi: {
endpoints: { getV2Tenants: { initiate: initiateMock } },
},
useGetV2TenantsQuery: useGetV2TenantsQueryMock,
}));

import UserContext from "../../admin/context/user-context";
import PlaylistForm from "../../admin/components/playlist/playlist-form";

function makeTenants(start, count) {
return Array.from({ length: count }, (_, i) => ({
"@id": `/v2/tenants/t${start + i}`,
"@type": "Tenant",
tenantKey: `tenant-${start + i}`,
title: `Tenant ${start + i}`,
description: "",
}));
}

const page1 = makeTenants(1, 30);
const page2 = makeTenants(31, 5);
const allTenants = [...page1, ...page2];

beforeEach(() => {
capturedDropdownData.current = null;
initiateMock.mockReset();
dispatchMock.mockReset();
useGetV2TenantsQueryMock.mockReset();

initiateMock.mockImplementation((params) => ({ __initiate: true, params }));
dispatchMock.mockImplementation((action) => {
const params = action?.params ?? {};
const page = params.page ?? 1;
if (page === 1) {
return Promise.resolve({
data: {
"hydra:member": page1,
"hydra:view": { "hydra:next": "/v2/tenants?page=2" },
},
});
}
if (page === 2) {
return Promise.resolve({
data: { "hydra:member": page2, "hydra:view": {} },
});
}
return Promise.resolve({
data: { "hydra:member": [], "hydra:view": {} },
});
});

useGetV2TenantsQueryMock.mockReturnValue({
data: { "hydra:member": page1 },
});
});

afterEach(() => cleanup());

describe("PlaylistForm tenants picker pagination", () => {
it("populates the share-target dropdown with tenants from every page", async () => {
const playlist = { schedules: [], tenants: [] };
const userContextValue = {
selectedTenant: { get: { tenantKey: "current" } },
};

render(
<UserContext.Provider value={userContextValue}>
<PlaylistForm playlist={playlist} handleInput={vi.fn()} />
</UserContext.Provider>,
);

await waitFor(() => {
expect(capturedDropdownData.current).not.toBeNull();
});

expect(capturedDropdownData.current).toHaveLength(allTenants.length);
expect(capturedDropdownData.current.map((t) => t.tenantKey)).toEqual(
allTenants.map((t) => t.tenantKey),
);
});
});
Loading