v3: the dump page's "In collections" list only shows the owner's private badge — every card there said "public", which says nothing on a list of playlists holding the dump. PlaylistCard takes privateBadgeOnly, passed through PlaylistCardList, which hides the visibility badge except on a private playlist of the signed-in user's; the other lists of playlists keep both badges. 5 PlaylistCard tests pass (new: privateBadgeOnly), and tsc, eslint, doc:lint and deno fmt are clean.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 53s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 22s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-30 21:19:06 +00:00
parent f3f07ed1e3
commit 5f0cf86fe6
4 changed files with 45 additions and 12 deletions

View File

@@ -29,6 +29,12 @@ export interface PlaylistCardProps {
onDelete?: () => void; onDelete?: () => void;
/** Hides the owner link and the unread dot. */ /** Hides the owner link and the unread dot. */
isOwner?: boolean; isOwner?: boolean;
/**
* Used by dump pages to display "In collections" lists. Prevents the
* visibility badge from showing, except to the owner of a private playlist:
* public badges on every card there would say nothing.
*/
privateBadgeOnly?: boolean;
} }
/** /**
@@ -37,12 +43,13 @@ export interface PlaylistCardProps {
* device. The card is an `<li>`, for a `<ul>`. * device. The card is an `<li>`, for a `<ul>`.
*/ */
export function PlaylistCard( export function PlaylistCard(
{ playlist, onDelete, isOwner }: PlaylistCardProps, { playlist, onDelete, isOwner, privateBadgeOnly }: PlaylistCardProps,
): React.JSX.Element { ): React.JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const { mediaToken } = useAuth(); const { mediaToken } = useAuth();
const unread = !isOwner && isRecent(playlist.createdAt) && const unread = !isOwner && isRecent(playlist.createdAt) &&
!isPlaylistVisited(playlist.id); !isPlaylistVisited(playlist.id);
const showBadge = !privateBadgeOnly || (!!isOwner && !playlist.isPublic);
function handleNavigate() { function handleNavigate() {
markPlaylistVisited(playlist.id); markPlaylistVisited(playlist.id);
@@ -85,6 +92,7 @@ export function PlaylistCard(
<p className="playlist-card-description">{playlist.description}</p> <p className="playlist-card-description">{playlist.description}</p>
)} )}
<div className="playlist-card-meta"> <div className="playlist-card-meta">
{showBadge && (
<span <span
className={`playlist-badge${ className={`playlist-badge${
playlist.isPublic ? "" : " playlist-badge--private" playlist.isPublic ? "" : " playlist-badge--private"
@@ -94,6 +102,7 @@ export function PlaylistCard(
? <Trans>public</Trans> ? <Trans>public</Trans>
: <Trans>private</Trans>} : <Trans>private</Trans>}
</span> </span>
)}
{playlist.ownerUsername && !isOwner && ( {playlist.ownerUsername && !isOwner && (
<Link <Link
to={`/users/${playlist.ownerUsername}`} to={`/users/${playlist.ownerUsername}`}

View File

@@ -13,6 +13,8 @@ export interface PlaylistCardListProps {
playlists: Playlist[]; playlists: Playlist[];
/** Given, each card shows a delete button, which calls it. */ /** Given, each card shows a delete button, which calls it. */
onDelete?: (playlist: Playlist) => void; onDelete?: (playlist: Playlist) => void;
/** Passed to each {@link PlaylistCard}. */
privateBadgeOnly?: boolean;
} }
/** /**
@@ -20,7 +22,7 @@ export interface PlaylistCardListProps {
* its owner's when the playlist is the signed-in user's. * its owner's when the playlist is the signed-in user's.
*/ */
export function PlaylistCardList( export function PlaylistCardList(
{ playlists, onDelete }: PlaylistCardListProps, { playlists, onDelete, privateBadgeOnly }: PlaylistCardListProps,
): React.JSX.Element { ): React.JSX.Element {
const { user } = useAuth(); const { user } = useAuth();
return ( return (
@@ -31,6 +33,7 @@ export function PlaylistCardList(
playlist={p} playlist={p}
isOwner={!!user && user.id === p.userId} isOwner={!!user && user.id === p.userId}
onDelete={onDelete && (() => onDelete(p))} onDelete={onDelete && (() => onDelete(p))}
privateBadgeOnly={privateBadgeOnly}
/> />
))} ))}
</ul> </ul>

View File

@@ -569,7 +569,7 @@ export function Dump(): React.JSX.Element {
<h2 className="related-section-title"> <h2 className="related-section-title">
<Trans>In collections</Trans> <Trans>In collections</Trans>
</h2> </h2>
<PlaylistCardList playlists={collections} /> <PlaylistCardList playlists={collections} privateBadgeOnly />
</section> </section>
)} )}

View File

@@ -67,6 +67,27 @@ describe("PlaylistCard", () => {
expect(again.container.querySelector(".unread-dot")).toBeNull(); expect(again.container.querySelector(".unread-dot")).toBeNull();
}); });
test("privateBadgeOnly shows only the owner's private badge", () => {
const cases = [
{ isPublic: true, isOwner: true, shown: false },
{ isPublic: false, isOwner: false, shown: false },
{ isPublic: false, isOwner: true, shown: true },
];
for (const { isPublic, isOwner, shown } of cases) {
const { container, unmount } = renderWithContexts(
<ul>
<PlaylistCard
playlist={makePlaylist({ isPublic })}
isOwner={isOwner}
privateBadgeOnly
/>
</ul>,
);
expect(container.querySelector(".playlist-badge") !== null).toBe(shown);
unmount();
}
});
test("delete doesn't open the playlist", async () => { test("delete doesn't open the playlist", async () => {
const onDelete = vi.fn(); const onDelete = vi.fn();
const user = userEvent.setup(); const user = userEvent.setup();