Add commish right-click context menus to MiniDraftGrid team headers (#327)

* Add commish right-click context menus to MiniDraftGrid team headers

Adds onAdjustTimeBankOpen and onSetAutodraftOpen props to MiniDraftGrid
so the last-two-rounds display on the Participants tab shows the same
"Adjust Time Bank…" / "Set Autodraft…" context menu on right-click that
the full Draft Board already exposes. Also passes those callbacks from
the route's miniDraftGrid useMemo so they are wired up for commissioners.

https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1

* Address all code review issues for draft room context menus

Layout bug: flex-1 min-w-20 was nested inside headerInner instead of the
direct flex-child wrapper in MiniDraftGrid, breaking equal column sizing.
Fixed by moving the classes to the outermost element in both the menu and
non-menu paths (matching DraftGridSection's pattern).

MiniDraftGrid improvements:
- Hoist hasHeaderMenu constant above the draftSlots.map() loop
- Add optional connectedTeams prop; disconnected teams render italic +
  muted-foreground, consistent with DraftGridSection
- connectedTeams now wired through the route's miniDraftGrid useMemo

DraftGridSection interface:
- Make onForceAutopick and onForceManualPickOpen optional (?) to match
  every other commissioner callback; add null checks at all three call
  sites (context menu, mobile MoreVertical button, mobile Sheet)
- Gate those two callbacks behind isCommissioner at the route level

Tests (new files):
- app/components/__tests__/MiniDraftGrid.test.tsx — covers layout classes,
  connected/disconnected styling, and all four context menu interactions
- app/components/__tests__/DraftGridSection.test.tsx — covers all three
  context menu surfaces for both commissioner and non-commissioner roles

Storybook: add CommissionerView and DisconnectedTeams stories to
MiniDraftGrid.stories.tsx

https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1

* Move hasHeaderMenu out of IIFE into component body

Computing it before the return statement is the right place since it only
depends on props, not loop variables. Removes the IIFE entirely and fixes
the indentation of the map callback body.

https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1

* Mock HTMLElement.scrollTo in test setup

jsdom does not implement scrollTo on elements, causing any component that
calls element.scrollTo() (e.g. MiniDraftGrid's scroll-to-current-cell
effect) to throw in unit tests.

https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-04-25 07:42:43 -07:00 committed by GitHub
parent 9e822b2073
commit 1bf65e33f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 554 additions and 44 deletions

View file

@ -0,0 +1,198 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DraftGridSection } from "~/components/draft/DraftGridSection";
const draftSlots = [
{ id: "slot-1", draftOrder: 1, team: { id: "team-1", name: "Alpha", logoUrl: null } },
{ id: "slot-2", draftOrder: 2, team: { id: "team-2", name: "Bravo", logoUrl: null } },
];
const ownerMap = { "team-1": "Alpha", "team-2": "Bravo" };
const draftGrid = [
[
{ pickNumber: 1, round: 1, pickInRound: 1, teamId: "team-1", pick: { participant: { name: "Player A" }, sport: { name: "NFL" } } },
{ pickNumber: 2, round: 1, pickInRound: 2, teamId: "team-2" },
],
];
const baseProps = {
draftSlots,
draftGrid,
currentPick: 2,
teamTimers: {} as Record<string, number | undefined>,
autodraftStatus: {} as Record<string, { isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean }>,
connectedTeams: new Set(["team-1", "team-2"]),
isCommissioner: false,
ownerMap,
seasonStatus: "draft" as const,
draftPaused: false,
};
describe("DraftGridSection", () => {
beforeEach(() => vi.clearAllMocks());
describe("Team header context menus", () => {
it("shows no header context menu for non-commissioner", () => {
render(<DraftGridSection {...baseProps} />);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.queryByText("Adjust Time Bank...")).not.toBeInTheDocument();
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
it("shows Adjust Time Bank and Set Autodraft for commissioner on right-click", () => {
render(
<DraftGridSection
{...baseProps}
isCommissioner
onAdjustTimeBankOpen={vi.fn()}
onSetAutodraftOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.getByText("Adjust Time Bank...")).toBeInTheDocument();
expect(screen.getByText("Set Autodraft...")).toBeInTheDocument();
});
it("calls onAdjustTimeBankOpen with correct teamId", async () => {
const user = userEvent.setup();
const onAdjustTimeBankOpen = vi.fn();
render(
<DraftGridSection
{...baseProps}
isCommissioner
onAdjustTimeBankOpen={onAdjustTimeBankOpen}
/>
);
fireEvent.contextMenu(screen.getAllByText("Bravo")[0]);
await user.click(screen.getByText("Adjust Time Bank..."));
expect(onAdjustTimeBankOpen).toHaveBeenCalledWith("team-2");
});
it("calls onSetAutodraftOpen with correct teamId", async () => {
const user = userEvent.setup();
const onSetAutodraftOpen = vi.fn();
render(
<DraftGridSection
{...baseProps}
isCommissioner
onSetAutodraftOpen={onSetAutodraftOpen}
/>
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
await user.click(screen.getByText("Set Autodraft..."));
expect(onSetAutodraftOpen).toHaveBeenCalledWith("team-1");
});
});
describe("Current cell context menu", () => {
it("shows no context menu for non-commissioner on current cell", () => {
render(
<DraftGridSection
{...baseProps}
onForceAutopick={vi.fn()}
onForceManualPickOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
});
it("shows Force Auto Pick and Force Manual Pick for commissioner", () => {
render(
<DraftGridSection
{...baseProps}
isCommissioner
onForceAutopick={vi.fn()}
onForceManualPickOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.getByText("Force Auto Pick")).toBeInTheDocument();
expect(screen.getByText("Force Manual Pick")).toBeInTheDocument();
});
it("shows no force-pick menu when callbacks absent even for commissioner", () => {
render(<DraftGridSection {...baseProps} isCommissioner />);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
});
it("calls onForceAutopick with correct args", async () => {
const user = userEvent.setup();
const onForceAutopick = vi.fn();
render(
<DraftGridSection {...baseProps} isCommissioner onForceAutopick={onForceAutopick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Auto Pick"));
expect(onForceAutopick).toHaveBeenCalledWith(2, "team-2");
});
it("calls onForceManualPickOpen with correct args", async () => {
const user = userEvent.setup();
const onForceManualPickOpen = vi.fn();
render(
<DraftGridSection
{...baseProps}
isCommissioner
onForceManualPickOpen={onForceManualPickOpen}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Manual Pick"));
expect(onForceManualPickOpen).toHaveBeenCalledWith(2, "team-2");
});
});
describe("Picked cell context menu", () => {
it("shows Replace Pick and Roll Back for commissioner on a picked cell", () => {
render(
<DraftGridSection
{...baseProps}
isCommissioner
onReplacePick={vi.fn()}
onRollbackToPick={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
expect(screen.getByText("Replace Pick")).toBeInTheDocument();
expect(screen.getByText("Roll Back to This Pick")).toBeInTheDocument();
});
it("shows no context menu on picked cell for non-commissioner", () => {
render(
<DraftGridSection
{...baseProps}
onReplacePick={vi.fn()}
onRollbackToPick={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
expect(screen.queryByText("Replace Pick")).not.toBeInTheDocument();
});
it("calls onReplacePick with correct args", async () => {
const user = userEvent.setup();
const onReplacePick = vi.fn();
render(
<DraftGridSection {...baseProps} isCommissioner onReplacePick={onReplacePick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Replace Pick"));
expect(onReplacePick).toHaveBeenCalledWith(1, "team-1");
});
it("calls onRollbackToPick with correct pickNumber", async () => {
const user = userEvent.setup();
const onRollbackToPick = vi.fn();
render(
<DraftGridSection {...baseProps} isCommissioner onRollbackToPick={onRollbackToPick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Roll Back to This Pick"));
expect(onRollbackToPick).toHaveBeenCalledWith(1);
});
});
});

View file

@ -0,0 +1,237 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MiniDraftGrid } from "~/components/draft/MiniDraftGrid";
const draftSlots = [
{ id: "slot-1", draftOrder: 1, team: { id: "team-1", name: "Alpha", logoUrl: null } },
{ id: "slot-2", draftOrder: 2, team: { id: "team-2", name: "Bravo", logoUrl: null } },
];
const ownerMap = { "team-1": "Alpha", "team-2": "Bravo" };
// Round 1: team-1 pick 1 (filled), team-2 pick 2 (current/empty)
// Round 2 (snake): team-2 pick 3, team-1 pick 4 — both empty
const draftGrid = [
[
{ pickNumber: 1, round: 1, pickInRound: 1, teamId: "team-1", pick: { participant: { name: "Player A" }, sport: { name: "NFL" } } },
{ pickNumber: 2, round: 1, pickInRound: 2, teamId: "team-2" },
],
[
{ pickNumber: 3, round: 2, pickInRound: 1, teamId: "team-2" },
{ pickNumber: 4, round: 2, pickInRound: 2, teamId: "team-1" },
],
];
const baseProps = {
draftSlots,
draftGrid,
currentPick: 2,
currentRound: 1,
ownerMap,
};
describe("MiniDraftGrid", () => {
beforeEach(() => vi.clearAllMocks());
describe("Team header layout", () => {
it("renders all team names", () => {
render(<MiniDraftGrid {...baseProps} />);
expect(screen.getAllByText("Alpha").length).toBeGreaterThan(0);
expect(screen.getAllByText("Bravo").length).toBeGreaterThan(0);
});
it("non-commissioner headers carry flex sizing classes", () => {
render(<MiniDraftGrid {...baseProps} />);
const alphaHeader = screen.getAllByText("Alpha")[0].closest(".flex-1");
expect(alphaHeader).toHaveClass("flex-1", "min-w-20");
});
it("commissioner headers carry flex sizing classes", () => {
render(
<MiniDraftGrid
{...baseProps}
onAdjustTimeBankOpen={vi.fn()}
onSetAutodraftOpen={vi.fn()}
/>
);
const alphaHeader = screen.getAllByText("Alpha")[0].closest(".flex-1");
expect(alphaHeader).toHaveClass("flex-1", "min-w-20");
});
});
describe("Disconnected team styling", () => {
it("applies italic + muted style to disconnected teams when connectedTeams provided", () => {
render(
<MiniDraftGrid
{...baseProps}
connectedTeams={new Set(["team-1"])}
/>
);
const bravoSpan = screen.getAllByText("Bravo")[0];
expect(bravoSpan).toHaveClass("italic", "text-muted-foreground");
const alphaSpan = screen.getAllByText("Alpha")[0];
expect(alphaSpan).not.toHaveClass("italic");
});
it("treats all teams as connected when connectedTeams is not provided", () => {
render(<MiniDraftGrid {...baseProps} />);
expect(screen.getAllByText("Alpha")[0]).not.toHaveClass("italic");
expect(screen.getAllByText("Bravo")[0]).not.toHaveClass("italic");
});
});
describe("Team header context menus (commissioner)", () => {
it("shows no context menu when callbacks are absent", () => {
render(<MiniDraftGrid {...baseProps} />);
const alphaHeader = screen.getAllByText("Alpha")[0];
fireEvent.contextMenu(alphaHeader);
expect(screen.queryByText("Adjust Time Bank...")).not.toBeInTheDocument();
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
it("shows Adjust Time Bank and Set Autodraft on right-click when both callbacks provided", () => {
render(
<MiniDraftGrid
{...baseProps}
onAdjustTimeBankOpen={vi.fn()}
onSetAutodraftOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.getByText("Adjust Time Bank...")).toBeInTheDocument();
expect(screen.getByText("Set Autodraft...")).toBeInTheDocument();
});
it("shows only Adjust Time Bank when only that callback is provided", () => {
render(
<MiniDraftGrid {...baseProps} onAdjustTimeBankOpen={vi.fn()} />
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.getByText("Adjust Time Bank...")).toBeInTheDocument();
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
it("calls onAdjustTimeBankOpen with the correct teamId", async () => {
const user = userEvent.setup();
const onAdjustTimeBankOpen = vi.fn();
render(
<MiniDraftGrid {...baseProps} onAdjustTimeBankOpen={onAdjustTimeBankOpen} />
);
fireEvent.contextMenu(screen.getAllByText("Bravo")[0]);
await user.click(screen.getByText("Adjust Time Bank..."));
expect(onAdjustTimeBankOpen).toHaveBeenCalledWith("team-2");
});
it("calls onSetAutodraftOpen with the correct teamId", async () => {
const user = userEvent.setup();
const onSetAutodraftOpen = vi.fn();
render(
<MiniDraftGrid {...baseProps} onSetAutodraftOpen={onSetAutodraftOpen} />
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
await user.click(screen.getByText("Set Autodraft..."));
expect(onSetAutodraftOpen).toHaveBeenCalledWith("team-1");
});
});
describe("Current cell context menu (commissioner)", () => {
it("shows Force Auto Pick and Force Manual Pick on right-click of current cell", () => {
render(
<MiniDraftGrid
{...baseProps}
onForceAutopick={vi.fn()}
onForceManualPickOpen={vi.fn()}
/>
);
// Pick 2 is the current cell (team-2, round 1)
const currentCell = screen.getByTitle("Overall Pick #2");
fireEvent.contextMenu(currentCell);
expect(screen.getByText("Force Auto Pick")).toBeInTheDocument();
expect(screen.getByText("Force Manual Pick")).toBeInTheDocument();
});
it("calls onForceAutopick with correct args", async () => {
const user = userEvent.setup();
const onForceAutopick = vi.fn();
render(
<MiniDraftGrid {...baseProps} onForceAutopick={onForceAutopick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Auto Pick"));
expect(onForceAutopick).toHaveBeenCalledWith(2, "team-2");
});
it("calls onForceManualPickOpen with correct args", async () => {
const user = userEvent.setup();
const onForceManualPickOpen = vi.fn();
render(
<MiniDraftGrid {...baseProps} onForceManualPickOpen={onForceManualPickOpen} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Manual Pick"));
expect(onForceManualPickOpen).toHaveBeenCalledWith(2, "team-2");
});
it("shows no force-pick menu when no callbacks provided", () => {
render(<MiniDraftGrid {...baseProps} />);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
expect(screen.queryByText("Force Manual Pick")).not.toBeInTheDocument();
});
});
describe("Picked cell context menu (commissioner)", () => {
it("shows Replace Pick and Roll Back on right-click of a picked cell", () => {
render(
<MiniDraftGrid
{...baseProps}
onReplacePick={vi.fn()}
onRollbackToPick={vi.fn()}
/>
);
// Pick 1 is already picked (team-1)
const pickedCell = screen.getByTitle("Overall Pick #1");
fireEvent.contextMenu(pickedCell);
expect(screen.getByText("Replace Pick")).toBeInTheDocument();
expect(screen.getByText("Roll Back to This Pick")).toBeInTheDocument();
});
it("calls onReplacePick with correct args", async () => {
const user = userEvent.setup();
const onReplacePick = vi.fn();
render(
<MiniDraftGrid {...baseProps} onReplacePick={onReplacePick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Replace Pick"));
expect(onReplacePick).toHaveBeenCalledWith(1, "team-1");
});
it("calls onRollbackToPick with correct pickNumber", async () => {
const user = userEvent.setup();
const onRollbackToPick = vi.fn();
render(
<MiniDraftGrid {...baseProps} onRollbackToPick={onRollbackToPick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Roll Back to This Pick"));
expect(onRollbackToPick).toHaveBeenCalledWith(1);
});
it("shows no menu on picked cell when no callbacks provided", () => {
render(<MiniDraftGrid {...baseProps} />);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
expect(screen.queryByText("Replace Pick")).not.toBeInTheDocument();
});
});
describe("Empty grid", () => {
it("renders nothing when draftGrid is empty", () => {
const { container } = render(
<MiniDraftGrid {...baseProps} draftGrid={[]} />
);
expect(container.firstChild).toBeNull();
});
});
});

View file

@ -58,8 +58,8 @@ interface DraftGridSectionProps {
draftPaused?: boolean; draftPaused?: boolean;
onAdjustTimeBankOpen?: (teamId: string) => void; onAdjustTimeBankOpen?: (teamId: string) => void;
onSetAutodraftOpen?: (teamId: string) => void; onSetAutodraftOpen?: (teamId: string) => void;
onForceAutopick: (pickNumber: number, teamId: string) => void; onForceAutopick?: (pickNumber: number, teamId: string) => void;
onForceManualPickOpen: (pickNumber: number, teamId: string) => void; onForceManualPickOpen?: (pickNumber: number, teamId: string) => void;
onReplacePick?: (pickNumber: number, teamId: string) => void; onReplacePick?: (pickNumber: number, teamId: string) => void;
onRollbackToPick?: (pickNumber: number) => void; onRollbackToPick?: (pickNumber: number) => void;
ownerMap?: Record<string, string>; ownerMap?: Record<string, string>;
@ -196,7 +196,7 @@ export const DraftGridSection = memo(function DraftGridSection({
const mobileButtons = isCommissioner && ( const mobileButtons = isCommissioner && (
<> <>
{!isPicked && isCurrent && ( {!isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen) && (
<button <button
className="absolute top-1 right-1 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted" className="absolute top-1 right-1 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "current-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })} onClick={() => setMobileSheet({ type: "current-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })}
@ -215,7 +215,7 @@ export const DraftGridSection = memo(function DraftGridSection({
</> </>
); );
if (isCommissioner && !isPicked && isCurrent) { if (isCommissioner && !isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen)) {
return ( return (
<ContextMenu key={cell.pickNumber}> <ContextMenu key={cell.pickNumber}>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
@ -234,23 +234,16 @@ export const DraftGridSection = memo(function DraftGridSection({
</DraftPickCell> </DraftPickCell>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem {onForceAutopick && (
onClick={() => <ContextMenuItem onClick={() => onForceAutopick(cell.pickNumber, cell.teamId)}>
onForceAutopick(cell.pickNumber, cell.teamId) Force Auto Pick
} </ContextMenuItem>
> )}
Force Auto Pick {onForceManualPickOpen && (
</ContextMenuItem> <ContextMenuItem onClick={() => onForceManualPickOpen(cell.pickNumber, cell.teamId)}>
<ContextMenuItem Force Manual Pick
onClick={() => </ContextMenuItem>
onForceManualPickOpen( )}
cell.pickNumber,
cell.teamId
)
}
>
Force Manual Pick
</ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
); );
@ -354,23 +347,27 @@ export const DraftGridSection = memo(function DraftGridSection({
)} )}
{mobileSheet?.type === "current-cell" && ( {mobileSheet?.type === "current-cell" && (
<> <>
<Button {onForceAutopick && (
onClick={() => { <Button
onForceAutopick(mobileSheet.pickNumber, mobileSheet.teamId); onClick={() => {
setMobileSheet(null); onForceAutopick(mobileSheet.pickNumber, mobileSheet.teamId);
}} setMobileSheet(null);
> }}
Force Auto Pick >
</Button> Force Auto Pick
<Button </Button>
variant="outline" )}
onClick={() => { {onForceManualPickOpen && (
onForceManualPickOpen(mobileSheet.pickNumber, mobileSheet.teamId); <Button
setMobileSheet(null); variant="outline"
}} onClick={() => {
> onForceManualPickOpen(mobileSheet.pickNumber, mobileSheet.teamId);
Force Manual Pick setMobileSheet(null);
</Button> }}
>
Force Manual Pick
</Button>
)}
</> </>
)} )}
{mobileSheet?.type === "picked-cell" && ( {mobileSheet?.type === "picked-cell" && (

View file

@ -143,3 +143,33 @@ export const EmptyGrid: Story = {
ownerMap, ownerMap,
}, },
}; };
export const CommissionerView: Story = {
name: "Commissioner — All Actions Available (right-click headers and cells)",
args: {
draftSlots,
draftGrid: fullGrid,
currentPick: 6,
currentRound: 2,
ownerMap,
connectedTeams: new Set(["team-1", "team-2", "team-3"]), // team-4 disconnected
onAdjustTimeBankOpen: (teamId) => alert(`Adjust time bank for ${teamId}`),
onSetAutodraftOpen: (teamId) => alert(`Set autodraft for ${teamId}`),
onForceAutopick: (pickNumber, teamId) => alert(`Force autopick ${pickNumber} for ${teamId}`),
onForceManualPickOpen: (pickNumber, teamId) => alert(`Force manual pick ${pickNumber} for ${teamId}`),
onReplacePick: (pickNumber, teamId) => alert(`Replace pick ${pickNumber} for ${teamId}`),
onRollbackToPick: (pickNumber) => alert(`Roll back to pick ${pickNumber}`),
},
};
export const DisconnectedTeams: Story = {
name: "Disconnected Teams",
args: {
draftSlots,
draftGrid: fullGrid,
currentPick: 3,
currentRound: 1,
ownerMap,
connectedTeams: new Set(["team-1"]), // team-2 through team-4 disconnected
},
};

View file

@ -48,8 +48,11 @@ export interface MiniDraftGridProps {
ownerMap?: Record<string, string>; ownerMap?: Record<string, string>;
teamTimers?: Record<string, number | undefined>; teamTimers?: Record<string, number | undefined>;
autodraftStatus?: Record<string, { isEnabled: boolean }>; autodraftStatus?: Record<string, { isEnabled: boolean }>;
connectedTeams?: Set<string>;
seasonStatus?: SeasonStatus; seasonStatus?: SeasonStatus;
draftPaused?: boolean; draftPaused?: boolean;
onAdjustTimeBankOpen?: (teamId: string) => void;
onSetAutodraftOpen?: (teamId: string) => void;
onForceAutopick?: (pickNumber: number, teamId: string) => void; onForceAutopick?: (pickNumber: number, teamId: string) => void;
onForceManualPickOpen?: (pickNumber: number, teamId: string) => void; onForceManualPickOpen?: (pickNumber: number, teamId: string) => void;
onReplacePick?: (pickNumber: number, teamId: string) => void; onReplacePick?: (pickNumber: number, teamId: string) => void;
@ -64,8 +67,11 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
ownerMap = {}, ownerMap = {},
teamTimers = {}, teamTimers = {},
autodraftStatus = {}, autodraftStatus = {},
connectedTeams,
seasonStatus, seasonStatus,
draftPaused, draftPaused,
onAdjustTimeBankOpen,
onSetAutodraftOpen,
onForceAutopick, onForceAutopick,
onForceManualPickOpen, onForceManualPickOpen,
onReplacePick, onReplacePick,
@ -136,6 +142,8 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
if (draftGrid.length === 0) return null; if (draftGrid.length === 0) return null;
const hasHeaderMenu = !!(onAdjustTimeBankOpen || onSetAutodraftOpen);
// h-14 (56px) + gap-1.5 (6px) fallback until first measurement // h-14 (56px) + gap-1.5 (6px) fallback until first measurement
const effectiveRowHeight = rowHeight > 0 ? rowHeight : 56; const effectiveRowHeight = rowHeight > 0 ? rowHeight : 56;
@ -151,17 +159,51 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
{draftSlots.map((slot) => { {draftSlots.map((slot) => {
const teamTime = teamTimers[slot.team.id]; const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false; const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false;
return ( const isConnected = connectedTeams ? connectedTeams.has(slot.team.id) : true;
<div key={slot.id} className="flex-1 min-w-20 text-center">
const headerContent = (
<>
<div className="text-xs font-medium truncate px-1 flex items-center justify-center gap-0.5"> <div className="text-xs font-medium truncate px-1 flex items-center justify-center gap-0.5">
{isAutodraft && ( {isAutodraft && (
<span className="inline-flex items-center justify-center h-3.5 w-3.5 text-[9px] font-bold text-white bg-black shrink-0">A</span> <span className="inline-flex items-center justify-center h-3.5 w-3.5 text-[9px] font-bold text-white bg-black shrink-0">A</span>
)} )}
<span className="truncate">{ownerMap[slot.team.id] || slot.team.name}</span> <span className={`truncate${!isConnected ? " italic text-muted-foreground" : ""}`}>
{ownerMap[slot.team.id] || slot.team.name}
</span>
</div> </div>
<div className={`text-xs font-mono px-1 ${getTimerColorClass(teamTime)}`}> <div className={`text-xs font-mono px-1 ${getTimerColorClass(teamTime)}`}>
{formatClockTime(teamTime)} {formatClockTime(teamTime)}
</div> </div>
</>
);
if (hasHeaderMenu) {
return (
<ContextMenu key={slot.id}>
<ContextMenuTrigger asChild>
<div className="flex-1 min-w-20 text-center cursor-context-menu">
{headerContent}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{onAdjustTimeBankOpen && (
<ContextMenuItem onClick={() => onAdjustTimeBankOpen(slot.team.id)}>
Adjust Time Bank...
</ContextMenuItem>
)}
{onSetAutodraftOpen && (
<ContextMenuItem onClick={() => onSetAutodraftOpen(slot.team.id)}>
Set Autodraft...
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
);
}
return (
<div key={slot.id} className="flex-1 min-w-20 text-center">
{headerContent}
</div> </div>
); );
})} })}

View file

@ -953,13 +953,16 @@ export default function DraftRoom() {
ownerMap, ownerMap,
teamTimers, teamTimers,
autodraftStatus, autodraftStatus,
connectedTeams,
seasonStatus: season.status, seasonStatus: season.status,
draftPaused: isPaused, draftPaused: isPaused,
onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined,
onForceAutopick: isCommissioner ? handleForceAutopick : undefined, onForceAutopick: isCommissioner ? handleForceAutopick : undefined,
onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined, onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined, onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined, onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
}), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, season.status, isPaused, isCommissioner, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]); }), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, connectedTeams, season.status, isPaused, isCommissioner, handleAdjustTimeBankOpen, handleSetAutodraftOpen, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]);
const availableParticipantsSectionProps = useMemo(() => ({ const availableParticipantsSectionProps = useMemo(() => ({
participants: filteredParticipants, participants: filteredParticipants,
@ -1001,8 +1004,8 @@ export default function DraftRoom() {
draftPaused: isPaused, draftPaused: isPaused,
onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined, onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined, onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined,
onForceAutopick: handleForceAutopick, onForceAutopick: isCommissioner ? handleForceAutopick : undefined,
onForceManualPickOpen: handleForceManualPickOpen, onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined, onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined, onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
}; };

View file

@ -10,6 +10,9 @@ afterEach(() => {
// Mock environment variables // Mock environment variables
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
// jsdom does not implement scrollTo on elements
HTMLElement.prototype.scrollTo = vi.fn();
// Mock BetterAuth // Mock BetterAuth
vi.mock('~/lib/auth.server', () => ({ vi.mock('~/lib/auth.server', () => ({
auth: { auth: {