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
This commit is contained in:
parent
3cfd6ca654
commit
a2cbedbb82
6 changed files with 555 additions and 81 deletions
198
app/components/__tests__/DraftGridSection.test.tsx
Normal file
198
app/components/__tests__/DraftGridSection.test.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
237
app/components/__tests__/MiniDraftGrid.test.tsx
Normal file
237
app/components/__tests__/MiniDraftGrid.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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" && (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ 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;
|
onAdjustTimeBankOpen?: (teamId: string) => void;
|
||||||
|
|
@ -66,6 +67,7 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
||||||
ownerMap = {},
|
ownerMap = {},
|
||||||
teamTimers = {},
|
teamTimers = {},
|
||||||
autodraftStatus = {},
|
autodraftStatus = {},
|
||||||
|
connectedTeams,
|
||||||
seasonStatus,
|
seasonStatus,
|
||||||
draftPaused,
|
draftPaused,
|
||||||
onAdjustTimeBankOpen,
|
onAdjustTimeBankOpen,
|
||||||
|
|
@ -152,51 +154,60 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
||||||
<div className="inline-block min-w-full">
|
<div className="inline-block min-w-full">
|
||||||
{/* Team header */}
|
{/* Team header */}
|
||||||
<div className="flex gap-1.5 mb-1.5">
|
<div className="flex gap-1.5 mb-1.5">
|
||||||
{draftSlots.map((slot) => {
|
{(() => {
|
||||||
const teamTime = teamTimers[slot.team.id];
|
|
||||||
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false;
|
|
||||||
const hasHeaderMenu = !!(onAdjustTimeBankOpen || onSetAutodraftOpen);
|
const hasHeaderMenu = !!(onAdjustTimeBankOpen || onSetAutodraftOpen);
|
||||||
|
return draftSlots.map((slot) => {
|
||||||
|
const teamTime = teamTimers[slot.team.id];
|
||||||
|
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false;
|
||||||
|
const isConnected = connectedTeams ? connectedTeams.has(slot.team.id) : true;
|
||||||
|
|
||||||
const headerInner = (
|
const headerContent = (
|
||||||
<div className="flex-1 min-w-20 text-center">
|
<>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div className={`text-xs font-mono px-1 ${getTimerColorClass(teamTime)}`}>
|
|
||||||
{formatClockTime(teamTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasHeaderMenu) {
|
|
||||||
return (
|
|
||||||
<ContextMenu key={slot.id}>
|
|
||||||
<ContextMenuTrigger asChild>
|
|
||||||
<div className="cursor-context-menu">
|
|
||||||
{headerInner}
|
|
||||||
</div>
|
|
||||||
</ContextMenuTrigger>
|
|
||||||
<ContextMenuContent>
|
|
||||||
{onAdjustTimeBankOpen && (
|
|
||||||
<ContextMenuItem onClick={() => onAdjustTimeBankOpen(slot.team.id)}>
|
|
||||||
Adjust Time Bank...
|
|
||||||
</ContextMenuItem>
|
|
||||||
)}
|
)}
|
||||||
{onSetAutodraftOpen && (
|
<span className={`truncate${!isConnected ? " italic text-muted-foreground" : ""}`}>
|
||||||
<ContextMenuItem onClick={() => onSetAutodraftOpen(slot.team.id)}>
|
{ownerMap[slot.team.id] || slot.team.name}
|
||||||
Set Autodraft...
|
</span>
|
||||||
</ContextMenuItem>
|
</div>
|
||||||
)}
|
<div className={`text-xs font-mono px-1 ${getTimerColorClass(teamTime)}`}>
|
||||||
</ContextMenuContent>
|
{formatClockTime(teamTime)}
|
||||||
</ContextMenu>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return <div key={slot.id}>{headerInner}</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>
|
</div>
|
||||||
|
|
||||||
{/* Rows — clipped to 2-row height, slides to reveal new round */}
|
{/* Rows — clipped to 2-row height, slides to reveal new round */}
|
||||||
|
|
|
||||||
|
|
@ -953,6 +953,7 @@ 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,
|
onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
|
||||||
|
|
@ -961,7 +962,7 @@ export default function DraftRoom() {
|
||||||
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, handleAdjustTimeBankOpen, handleSetAutodraftOpen, 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,
|
||||||
|
|
@ -1003,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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue