diff --git a/package.json b/package.json index 25ae5f3..3210321 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modaq", - "version": "1.17.0", + "version": "1.18.0", "description": "Quiz Bowl Reader using TypeScript, React, and MobX", "repository": { "type": "git", diff --git a/src/components/EventViewer.tsx b/src/components/EventViewer.tsx index f6059ca..7cdff2a 100644 --- a/src/components/EventViewer.tsx +++ b/src/components/EventViewer.tsx @@ -1,17 +1,20 @@ import * as React from "react"; import { observer } from "mobx-react-lite"; -import { DetailsList, CheckboxVisibility, SelectionMode, IColumn, Label, Text } from "@fluentui/react"; +import { DetailsList, CheckboxVisibility, SelectionMode, IColumn, Label, Text, ISelection } from "@fluentui/react"; import { mergeStyleSets } from "@fluentui/react"; import { CycleItemList } from "./cycleItems/CycleItemList"; import { Cycle } from "../state/Cycle"; import { AppState } from "../state/AppState"; -import { GameState } from "../state/GameState"; import { StateContext } from "../contexts/StateContext"; const numberKey = "number"; const cycleKey = "cycle"; +// Needed for filling in dummy values into an interface +// eslint-disable-next-line @typescript-eslint/no-empty-function +function dummyFunction() {} + export const EventViewer = observer(function EventViewer(): JSX.Element | null { const appState: AppState = React.useContext(StateContext); const classes: IEventViewerClassNames = getClassNames(appState.uiState.isEventLogHidden); @@ -31,7 +34,7 @@ export const EventViewer = observer(function EventViewer(): JSX.Element | null { return <>; } - return onRenderItemColumn(item, appState.game, index, column); + return onRenderItemColumn(item, appState, index, column); }, [appState] ); @@ -46,6 +49,7 @@ export const EventViewer = observer(function EventViewer(): JSX.Element | null { ariaLabel: "Question number", isResizable: true, isRowHeader: true, + data: appState.uiState.cycleIndex, }, { key: cycleKey, @@ -63,6 +67,35 @@ export const EventViewer = observer(function EventViewer(): JSX.Element | null { }, ]; + // DetailsList doesn't know how to change its selection when the cycle index changes unless we tell it how to select it + const selection: ISelection = { + count: 1, + mode: SelectionMode.single, + canSelectItem: () => true, + setChangeEvents: dummyFunction, + setItems: dummyFunction, + getItems: () => [], + getSelection: () => [{}], + getSelectedIndices: () => [appState.uiState.cycleIndex], + getSelectedCount: () => 1, + isRangeSelected: (): boolean => false, + isAllSelected: () => appState.uiState.cycleIndex === appState.game.playableCycles.length, + isIndexSelected: (index) => index === appState.uiState.cycleIndex, + isKeySelected: () => false, + setAllSelected: dummyFunction, + setKeySelected: dummyFunction, + setIndexSelected: (index: number): void => appState.uiState.setCycleIndex(index), + selectToKey: dummyFunction, + selectToIndex: (index: number): void => appState.uiState.setCycleIndex(index), + toggleAllSelected: dummyFunction, + toggleKeySelected: dummyFunction, + toggleIndexSelected: (index: number): void => { + appState.uiState.setCycleIndex(index); + }, + toggleRangeSelected: dummyFunction, + }; + + // This needs to re-render based on cycleIndex so it can select the current one return (
); }); -function onRenderItemColumn(item: Cycle, game: GameState, index: number, column: IColumn): JSX.Element { +function onRenderItemColumn(item: Cycle, appState: AppState, index: number, column: IColumn): JSX.Element { switch (column?.key) { case numberKey: if (index == undefined) { return <>; } + if (column?.data === index) { + return ( + + + + ); + } + return ; case cycleKey: const scores: [number, number][] = column.data; @@ -91,7 +133,7 @@ function onRenderItemColumn(item: Cycle, game: GameState, index: number, column: return ( <> - + {`(${scoreInCurrentCycle[0]} - ${scoreInCurrentCycle[1]})`} ); diff --git a/src/components/GameBar.tsx b/src/components/GameBar.tsx index 332b164..331067d 100644 --- a/src/components/GameBar.tsx +++ b/src/components/GameBar.tsx @@ -91,6 +91,10 @@ export const GameBar = observer(function GameBar(): JSX.Element { const openHelpHandler = React.useCallback(() => appState.uiState.dialogState.showHelpDialog(), [appState]); + const reorderPlayersHandler = React.useCallback(() => { + uiState.dialogState.showReorderPlayersDialog(game.players); + }, [uiState, game]); + const items: ICommandBarItemProps[] = appState.uiState.hideNewGame ? [] : [ @@ -142,6 +146,7 @@ export const GameBar = observer(function GameBar(): JSX.Element { appState, addPlayerHandler, protestBonusHandler, + reorderPlayersHandler, addQuestionsHandler ); items.push({ @@ -213,6 +218,7 @@ function getActionSubMenuItems( appState: AppState, addPlayerHandler: () => void, protestBonusHandler: () => void, + reorderPlayersHandler: () => void, addQuestionsHandler: () => void ): ICommandBarItemProps[] { const items: ICommandBarItemProps[] = []; @@ -223,7 +229,8 @@ function getActionSubMenuItems( appState, game, uiState, - addPlayerHandler + addPlayerHandler, + reorderPlayersHandler ); items.push(playerManagementSection); @@ -382,7 +389,8 @@ function getPlayerManagementSubMenuItems( appState: AppState, game: GameState, uiState: UIState, - addPlayerHandler: () => void + addPlayerHandler: () => void, + reorderPlayersHandler: () => void ): ICommandBarItemProps { const teamNames: string[] = game.teamNames; const swapActivePlayerMenus: ICommandBarItemProps[] = []; @@ -477,13 +485,20 @@ function getPlayerManagementSubMenuItems( disabled: appState.game.cycles.length === 0, }; + const reorderPlayersItem: ICommandBarItemProps = { + key: "reorderPlayers", + text: "Reorder players...", + onClick: reorderPlayersHandler, + disabled: appState.game.cycles.length === 0, + }; + return { key: "playerManagement", itemType: ContextualMenuItemType.Section, sectionProps: { bottomDivider: true, title: "Player Management", - items: [swapPlayerItem, addPlayerItem], + items: [swapPlayerItem, addPlayerItem, reorderPlayersItem], }, }; } diff --git a/src/components/ModalDialogContainer.tsx b/src/components/ModalDialogContainer.tsx index b46225d..b70da79 100644 --- a/src/components/ModalDialogContainer.tsx +++ b/src/components/ModalDialogContainer.tsx @@ -12,6 +12,7 @@ import { CustomizeGameFormatDialog } from "./dialogs/CustomizeGameFormatDialog"; import { AddQuestionsDialog } from "./dialogs/AddQuestionsDialog"; import { MessageDialog } from "./dialogs/MessageDialog"; import { RenamePlayerDialog } from "./dialogs/RenamePlayerDialog"; +import { ReorderPlayerDialog } from "./dialogs/ReorderPlayerDialog"; export const ModalDialogContainer = observer(function ModalDialogContainer() { // The Protest dialogs aren't here because they require extra information @@ -29,6 +30,7 @@ export const ModalDialogContainer = observer(function ModalDialogContainer() { + ); }); diff --git a/src/components/cycleItems/TossupAnswerCycleItem.tsx b/src/components/cycleItems/TossupAnswerCycleItem.tsx index e276258..8802f88 100644 --- a/src/components/cycleItems/TossupAnswerCycleItem.tsx +++ b/src/components/cycleItems/TossupAnswerCycleItem.tsx @@ -38,9 +38,7 @@ export const TossupAnswerCycleItem = observer(function TossupAnswerCycleItem( } } - const text = `${props.buzz.marker.player.name} (${ - props.buzz.marker.player.teamName - }) ${buzzDescription} on tossup #${props.buzz.tossupIndex + 1} at word ${props.buzz.marker.position + 1}`; + const text = `${props.buzz.marker.player.name} (${props.buzz.marker.player.teamName}) ${buzzDescription}`; return ; }); diff --git a/src/components/dialogs/ReorderPlayerDialog.tsx b/src/components/dialogs/ReorderPlayerDialog.tsx new file mode 100644 index 0000000..d1091e1 --- /dev/null +++ b/src/components/dialogs/ReorderPlayerDialog.tsx @@ -0,0 +1,249 @@ +import * as React from "react"; +import { observer } from "mobx-react-lite"; +import { + Dropdown, + IDropdownOption, + IDialogContentProps, + DialogType, + IModalProps, + ContextualMenu, + Dialog, + DialogFooter, + PrimaryButton, + DefaultButton, + Stack, + Label, + StackItem, + IconButton, + IIconProps, + IStackTokens, + DetailsList, + SelectionMode, + IColumn, + CheckboxVisibility, + IDragDropEvents, +} from "@fluentui/react"; + +import * as ReorderPlayersDialogController from "../../components/dialogs/ReorderPlayersDialogController"; +import { Player } from "../../state/TeamState"; +import { AppState } from "../../state/AppState"; +import { StateContext } from "../../contexts/StateContext"; +import { ReorderPlayersDialogState } from "../../state/ReorderPlayersDialogState"; + +const content: IDialogContentProps = { + type: DialogType.normal, + title: "Reorder Players", + closeButtonAriaLabel: "Close", + showCloseButton: true, + styles: { + innerContent: { + display: "flex", + flexDirection: "column", + }, + }, +}; + +const modalProps: IModalProps = { + isBlocking: false, + dragOptions: { + moveMenuItemText: "Move", + closeMenuItemText: "Close", + menu: ContextualMenu, + }, + styles: { + main: { + top: "25vh", + }, + }, + topOffsetFixed: true, +}; + +const buttonTokens: IStackTokens = { childrenGap: 10 }; + +const dialogBodyTokens: IStackTokens = { childrenGap: 10 }; + +const columns: IColumn[] = [ + { + key: "name", + fieldName: "name", + name: "name", + minWidth: 30, + }, +]; + +const downButtonProps: IIconProps = { + iconName: "ChevronDown", +}; + +const upButtonProps: IIconProps = { + iconName: "ChevronUp", +}; + +const rowStyle: React.CSSProperties = { + display: "flex", + justifyContent: "space-between", + alignItems: "center", +}; + +const moveDownClassName = "moveDownButton"; +const moveUpClassName = "moveUpButton"; + +// TODO: Look into making a DefaultDialog, which handles the footers and default props +export const ReorderPlayerDialog = observer(function ReorderPlayerDialog(): JSX.Element { + const appState: AppState = React.useContext(StateContext); + + return ( + + ); +}); + +const ReorderPlayerDialogBody = observer(function ReorderPlayerDialogBody(): JSX.Element { + const appState: AppState = React.useContext(StateContext); + + const [draggedItem, setDraggedItem] = React.useState(undefined); + const [autoFocusIndex, setAutoFocusIndex] = React.useState(undefined); + const [autoFocusIsUp, setAutoFocusIsUp] = React.useState(false); + + const teamChangeHandler = React.useCallback((ev: React.FormEvent, option?: IDropdownOption) => { + if (option?.text != undefined) { + ReorderPlayersDialogController.changeTeamName(option.text); + setAutoFocusIndex(undefined); + } + }, []); + + const reorderPlayersDialog: ReorderPlayersDialogState | undefined = + appState.uiState.dialogState.reorderPlayersDialog; + if (reorderPlayersDialog === undefined) { + return <>; + } + + // This makes sure that the focus stays on the button belonging to the player + if (autoFocusIndex != undefined) { + const className: string = autoFocusIsUp ? moveUpClassName : moveDownClassName; + const element = document.getElementsByClassName(className)[autoFocusIndex] as HTMLButtonElement; + if (element) { + element.focus(); + } + + setAutoFocusIndex(undefined); + } + + const teamOptions: IDropdownOption[] = appState.game.teamNames.map((teamName, index) => { + return { + key: index, + text: teamName, + selected: reorderPlayersDialog.teamName === teamName, + }; + }); + + const players: Player[] = reorderPlayersDialog.players.filter((p) => p.teamName === reorderPlayersDialog.teamName); + + function renderPlayerRow(player: Player | undefined, index: number | undefined) { + if (player == undefined || index == undefined || reorderPlayersDialog == undefined) { + return <>; + } + + return ( +
+ + + + + + { + ReorderPlayersDialogController.moveForward(player); + if (index > 1) { + setAutoFocusIsUp(true); + setAutoFocusIndex(index - 1); + } + }} + /> + + + = players.length - 1} + onClick={() => { + ReorderPlayersDialogController.moveBackward(player); + if (index < players.length - 1) { + setAutoFocusIsUp(false); + setAutoFocusIndex(index + 1); + } + }} + /> + + +
+ ); + } + + function insertBeforeItem(item: Player) { + if (draggedItem == undefined) { + return; + } + + const locationIndex = players.indexOf(item); + const itemIndex = players.indexOf(draggedItem); + if (locationIndex !== itemIndex) { + ReorderPlayersDialogController.movePlayerToIndex(draggedItem, locationIndex); + } + } + + const dragDropEvents: IDragDropEvents = { + canDrag: () => true, + canDrop: () => true, + onDragStart: (item?: Player) => { + setDraggedItem(item); + }, + onDragEnd: () => { + setDraggedItem(undefined); + }, + onDrop: (item?: Player) => { + if (draggedItem && item) { + insertBeforeItem(item); + setDraggedItem(undefined); + } + }, + }; + + return ( + + + + + + + + + + + + ); +}); diff --git a/src/components/dialogs/ReorderPlayersDialogController.ts b/src/components/dialogs/ReorderPlayersDialogController.ts new file mode 100644 index 0000000..e819668 --- /dev/null +++ b/src/components/dialogs/ReorderPlayersDialogController.ts @@ -0,0 +1,46 @@ +import { Player } from "../../state/TeamState"; +import { AppState } from "../../state/AppState"; +import { GameState } from "../../state/GameState"; +import { ReorderPlayersDialogState } from "../../state/ReorderPlayersDialogState"; + +export function changeTeamName(newName: string): void { + const appState: AppState = AppState.instance; + const reorderPlayersDialog: ReorderPlayersDialogState | undefined = + appState.uiState.dialogState.reorderPlayersDialog; + if (reorderPlayersDialog == undefined) { + return; + } + + reorderPlayersDialog.setTeamName(newName); +} + +export function hideDialog(): void { + const appState: AppState = AppState.instance; + appState.uiState.dialogState.hideReorderPlayersDialog(); +} + +export function moveBackward(player: Player): void { + AppState.instance.uiState.dialogState.reorderPlayersDialog?.movePlayerBackward(player); +} + +export function moveForward(player: Player): void { + AppState.instance.uiState.dialogState.reorderPlayersDialog?.movePlayerForward(player); +} + +export function movePlayerToIndex(player: Player, index: number): void { + AppState.instance.uiState.dialogState.reorderPlayersDialog?.movePlayerToIndex(player, index); +} + +export function submit(): void { + const appState: AppState = AppState.instance; + const game: GameState = appState.game; + const reorderPlayersDialogState: ReorderPlayersDialogState | undefined = + appState.uiState.dialogState.reorderPlayersDialog; + if (reorderPlayersDialogState == undefined) { + return; + } + + game.setPlayers(reorderPlayersDialogState.players); + + hideDialog(); +} diff --git a/src/state/DialogState.ts b/src/state/DialogState.ts index 6548eea..8ca0fa0 100644 --- a/src/state/DialogState.ts +++ b/src/state/DialogState.ts @@ -6,6 +6,7 @@ import { IGameFormat } from "./IGameFormat"; import { IMessageDialogState, MessageDialogType } from "./IMessageDialogState"; import { RenamePlayerDialogState } from "./RenamePlayerDialogState"; import { Player } from "./TeamState"; +import { ReorderPlayersDialogState } from "./ReorderPlayersDialogState"; export class DialogState { @ignore @@ -32,6 +33,9 @@ export class DialogState { @ignore public renamePlayerDialog: RenamePlayerDialogState | undefined; + @ignore + public reorderPlayersDialog: ReorderPlayersDialogState | undefined; + constructor() { makeAutoObservable(this); @@ -43,6 +47,7 @@ export class DialogState { this.messageDialog = undefined; this.newGameDialogVisible = false; this.renamePlayerDialog = undefined; + this.reorderPlayersDialog = undefined; } public hideAddQuestionsDialog(): void { @@ -77,6 +82,10 @@ export class DialogState { this.renamePlayerDialog = undefined; } + public hideReorderPlayersDialog(): void { + this.reorderPlayersDialog = undefined; + } + public showAddQuestionsDialog(): void { this.addQuestions = new AddQuestionDialogState(); } @@ -101,6 +110,10 @@ export class DialogState { this.renamePlayerDialog = new RenamePlayerDialogState(player); } + public showReorderPlayersDialog(players: Player[]): void { + this.reorderPlayersDialog = new ReorderPlayersDialogState(players); + } + public showOKMessageDialog(title: string, message: string, onOK?: () => void): void { this.messageDialog = { title, diff --git a/src/state/ReorderPlayersDialogState.ts b/src/state/ReorderPlayersDialogState.ts new file mode 100644 index 0000000..a706fde --- /dev/null +++ b/src/state/ReorderPlayersDialogState.ts @@ -0,0 +1,85 @@ +import { makeAutoObservable } from "mobx"; +import { Player } from "./TeamState"; + +export class ReorderPlayersDialogState { + public teamName: string; + + public players: Player[]; + + constructor(players: Player[]) { + makeAutoObservable(this); + + this.players = players; + this.teamName = players.length > 0 ? players[0].teamName : ""; + } + + public setTeamName(newTeam: string): void { + this.teamName = newTeam; + } + + public movePlayerBackward(player: Player): void { + let nextTeammateIndex = -1; + for (let i = this.players.length - 1; i >= 0; i--) { + const currentPlayer: Player = this.players[i]; + if (player === currentPlayer) { + if (nextTeammateIndex === -1) { + // Current player is in front, we can't move them + return; + } + + this.players[i] = this.players[nextTeammateIndex]; + this.players[nextTeammateIndex] = player; + return; + } + + if (this.players[i].teamName === player.teamName) { + nextTeammateIndex = i; + } + } + } + + public movePlayerForward(player: Player): void { + let previousTeammateIndex = -1; + for (let i = 0; i < this.players.length; i++) { + const currentPlayer: Player = this.players[i]; + if (player === currentPlayer) { + if (previousTeammateIndex === -1) { + // Current player is in front, we can't move them + return; + } + + this.players[i] = this.players[previousTeammateIndex]; + this.players[previousTeammateIndex] = player; + return; + } + + if (this.players[i].teamName === player.teamName) { + previousTeammateIndex = i; + } + } + } + + public movePlayerToIndex(player: Player, index: number): void { + if (index < 0) { + return; + } + + const teamName: string = player.teamName; + let sameTeamCount = -1; + for (let i = 0; i < this.players.length; i++) { + const currentPlayer: Player = this.players[i]; + if (currentPlayer.teamName === teamName) { + sameTeamCount++; + } + + if (sameTeamCount === index) { + let newPlayers = this.players.filter((p) => p !== player); + newPlayers = newPlayers.slice(0, i).concat(player).concat(newPlayers.slice(i)); + this.players = newPlayers; + return; + } + } + + // Index is beyond the number of players in the team. Treat it as a no-op + } +} diff --git a/tests/ReorderPlayersDialogControllerTests.ts b/tests/ReorderPlayersDialogControllerTests.ts new file mode 100644 index 0000000..284f85b --- /dev/null +++ b/tests/ReorderPlayersDialogControllerTests.ts @@ -0,0 +1,244 @@ +import { assert, expect } from "chai"; + +import * as ReorderPlayersDialogController from "src/components/dialogs/ReorderPlayersDialogController"; +import { AppState } from "src/state/AppState"; +import { GameState } from "src/state/GameState"; +import { PacketState, Tossup } from "src/state/PacketState"; +import { Player } from "src/state/TeamState"; +import { ReorderPlayersDialogState } from "src/state/ReorderPlayersDialogState"; + +const defaultPacket: PacketState = new PacketState(); +defaultPacket.setTossups([ + new Tossup("first q", "first a"), + new Tossup("second q", "second a"), + new Tossup("third q", "third a"), +]); + +const defaultTeamNames: string[] = ["First Team", "Team2"]; + +const firstTeamPlayers: Player[] = [ + new Player("Alex", defaultTeamNames[0], true), + new Player("Anna", defaultTeamNames[0], true), + new Player("Ashok", defaultTeamNames[0], false), +]; + +const secondTeamPlayers: Player[] = [ + new Player("Bob", defaultTeamNames[1], true), + new Player("Anna", defaultTeamNames[1], true), +]; + +const defaultExistingPlayers: Player[] = [ + firstTeamPlayers[0], + firstTeamPlayers[1], + secondTeamPlayers[0], + secondTeamPlayers[1], + firstTeamPlayers[2], +]; + +function initializeApp(players?: Player[]): { appState: AppState; players: Player[] } { + const gameState: GameState = new GameState(); + gameState.loadPacket(defaultPacket); + + players = players ?? defaultExistingPlayers; + gameState.addPlayers(players); + + AppState.resetInstance(); + const appState: AppState = AppState.instance; + appState.game = gameState; + appState.uiState.dialogState.showReorderPlayersDialog(players); + return { appState, players }; +} + +function getReorderPlayersDialogState(appState: AppState): ReorderPlayersDialogState { + if (appState.uiState.dialogState.reorderPlayersDialog == undefined) { + assert.fail("PendingNewPlayer should not be undefined"); + } + + return appState.uiState.dialogState.reorderPlayersDialog; +} + +describe("ReorderPlayersDialogControllerTests", () => { + it("hideDialog", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.hideDialog(); + + expect(appState.uiState.dialogState.reorderPlayersDialog).to.be.undefined; + }); + describe("changeTeamName", () => { + it("change to both teams", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.changeTeamName(defaultTeamNames[1]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.teamName).to.equal(defaultTeamNames[1]); + ReorderPlayersDialogController.changeTeamName(defaultTeamNames[0]); + expect(dialog.teamName).to.equal(defaultTeamNames[0]); + }); + }); + it("submit", () => { + const { appState, players } = initializeApp(); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + dialog.movePlayerBackward(players[0]); + + ReorderPlayersDialogController.submit(); + + expect(appState.uiState.dialogState.reorderPlayersDialog).to.be.undefined; + expect(appState.game.players).to.be.deep.equal([ + firstTeamPlayers[1], + firstTeamPlayers[0], + secondTeamPlayers[0], + secondTeamPlayers[1], + firstTeamPlayers[2], + ]); + }); + describe("moveBackward", () => { + it("Move backwards from the end is no-op", () => { + const { appState, players } = initializeApp(); + ReorderPlayersDialogController.moveBackward(players[players.length - 1]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players[players.length - 1]).to.equal(players[players.length - 1]); + expect(dialog.players).to.be.deep.equal(players); + }); + it("Move backwards from front moves player back", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.moveBackward(firstTeamPlayers[0]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players[0]).to.equal(firstTeamPlayers[1]); + expect(dialog.players[1]).to.equal(firstTeamPlayers[0]); + expect(dialog.players.slice(2)).to.be.deep.equal(defaultExistingPlayers.slice(2)); + }); + it("Move backwards for second team", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.moveBackward(secondTeamPlayers[0]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal([ + firstTeamPlayers[0], + firstTeamPlayers[1], + secondTeamPlayers[1], + secondTeamPlayers[0], + firstTeamPlayers[2], + ]); + }); + it("Move backwards with gap swaps correctly", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.moveBackward(firstTeamPlayers[1]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal([ + firstTeamPlayers[0], + firstTeamPlayers[2], + secondTeamPlayers[0], + secondTeamPlayers[1], + firstTeamPlayers[1], + ]); + }); + }); + describe("moveForward", () => { + it("Move forwards from 0 is no-op", () => { + const { appState, players } = initializeApp(); + ReorderPlayersDialogController.moveForward(players[0]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players[0]).to.equal(players[0]); + expect(dialog.players).to.be.deep.equal(players); + }); + it("Move forwards from 1 swaps player to front", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.moveForward(firstTeamPlayers[1]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players[0]).to.equal(firstTeamPlayers[1]); + expect(dialog.players[1]).to.equal(firstTeamPlayers[0]); + expect(dialog.players.slice(2)).to.be.deep.equal(defaultExistingPlayers.slice(2)); + }); + it("Move forwards for second team", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.moveForward(secondTeamPlayers[1]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal([ + firstTeamPlayers[0], + firstTeamPlayers[1], + secondTeamPlayers[1], + secondTeamPlayers[0], + firstTeamPlayers[2], + ]); + }); + it("Move forwards with gap swaps correctly", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.moveForward(firstTeamPlayers[2]); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal([ + firstTeamPlayers[0], + firstTeamPlayers[2], + secondTeamPlayers[0], + secondTeamPlayers[1], + firstTeamPlayers[1], + ]); + }); + }); + + // move ToIndex tests + describe("movePlayerToIndex", () => { + it("movePlayerToIndex to same index is no-op", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.movePlayerToIndex(firstTeamPlayers[1], 1); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal(defaultExistingPlayers); + }); + it("movePlayerToIndex to negative index is no-op", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.movePlayerToIndex(firstTeamPlayers[1], -1); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal(defaultExistingPlayers); + }); + it("movePlayerToIndex to overly large index is no-op", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.movePlayerToIndex(firstTeamPlayers[1], firstTeamPlayers.length); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal(defaultExistingPlayers); + }); + it("movePlayerToIndex next player swap", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.movePlayerToIndex(firstTeamPlayers[0], 1); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players[0]).to.equal(firstTeamPlayers[1]); + expect(dialog.players[1]).to.equal(firstTeamPlayers[0]); + expect(dialog.players.slice(2)).to.be.deep.equal(defaultExistingPlayers.slice(2)); + }); + it("movePlayerToIndex first to last player swap", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.movePlayerToIndex(firstTeamPlayers[0], 2); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal([ + firstTeamPlayers[1], + secondTeamPlayers[0], + secondTeamPlayers[1], + firstTeamPlayers[2], + firstTeamPlayers[0], + ]); + }); + it("movePlayerToIndex last to first player swap", () => { + const { appState } = initializeApp(); + ReorderPlayersDialogController.movePlayerToIndex(firstTeamPlayers[2], 0); + const dialog: ReorderPlayersDialogState = getReorderPlayersDialogState(appState); + + expect(dialog.players).to.be.deep.equal([ + firstTeamPlayers[2], + firstTeamPlayers[0], + firstTeamPlayers[1], + secondTeamPlayers[0], + secondTeamPlayers[1], + ]); + }); + }); +});