Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WEG-11 TrainerTab Pagination #1962

Merged
merged 12 commits into from
Jun 28, 2024
23 changes: 13 additions & 10 deletions wegas-app/src/main/node/wegas-lobby/src/API/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@ import { getStore, WegasLobbyState } from '../store/store';
import { CHANNEL_PREFIX, getPusherClient, initPusherSocket } from '../websocket/websocket';
import { entityIs, entityIsException } from './entityHelper';
import {
IAccountWithPerm,
IAuthenticationInformation,
IGameAdminWithTeams,
IJpaAuthentication,
IRoleWithPermissions,
IUserPage,
IUserWithAccounts,
PlayerToGameModel,
WegasLobbyRestClient,
IAccountWithPerm,
IAuthenticationInformation,
IGameAdminWithTeams,
IJpaAuthentication, IPage,
IRoleWithPermissions,
IUserWithAccounts,
PlayerToGameModel,
WegasLobbyRestClient,
} from './restClient';

const logger = getLogger('api');
Expand Down Expand Up @@ -363,7 +362,7 @@ export const getAllUsers = createAsyncThunk(

export const getPaginatedUsers = createAsyncThunk(
'user/getPaginated',
async (payload: {page: number, size: number, query: string}): Promise<IUserPage> => {
async (payload: {page: number, size: number, query: string}): Promise<IPage<IUserWithAccounts>> => {
return await restClient.UserController.getPaginatedUsers(payload.page, payload.size, payload.query);
},
);
Expand Down Expand Up @@ -608,6 +607,10 @@ export const getGames = createAsyncThunk('game/getGames', async (status: IGameWi
return await restClient.GameController.getGames(status);
});

export const getGamesPaginated = createAsyncThunk('game/getGamesPaginated', async (payload: {status: IGameWithId['status'], page: number, size: number, query: string, mine: boolean}) => {
return await restClient.GameController.getGamesPaginated(payload.status, payload.page, payload.size, payload.query, payload.mine);
});

export const changeGameStatus = createAsyncThunk(
'game/changeStatus',
async ({ gameId, status }: { gameId: number; status: IGameWithId['status'] }) => {
Expand Down
33 changes: 25 additions & 8 deletions wegas-app/src/main/node/wegas-lobby/src/API/restClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export interface WegasErrorMessage {

export type WegasRuntimeException = WegasErrorMessage;

export type IPage<T> = {
total: number;
page: number;
pageSize: number;
pageContent: T[];
}

export interface OnlineUser {
fullname: string;
email: string;
Expand All @@ -57,18 +64,12 @@ export type IRoleWithPermissions = IRoleWithId & {
permissions?: IPermissionWithId[];
};

export type IUserPage = {
total: number;
page: number;
pageSize: number;
pageContent:IUserWithAccounts[];
};

export type IUserWithAccounts = IUserWithId & {
accounts?: IAccountWithPerm[];
permissions?: IPermissionWithId[];
roles?: IRoleWithPermissions[];
};

export interface IJpaAuthentication {
'@class': 'JpaAuthentication';
mandatoryMethod: HashMethod;
Expand Down Expand Up @@ -182,6 +183,7 @@ export interface IGameAdminTeam {
declaredSize: number;
players?: IGameAdminPlayer[];
}

export interface IGameAdminWithTeams extends IGameAdminWithId {
teams?: IGameAdminTeam[];
effectiveCount: number;
Expand Down Expand Up @@ -558,7 +560,7 @@ export const WegasLobbyRestClient = function (
},
getPaginatedUsers: (page: number, size: number, query: string) => {
const path = `${baseUrl}/Shadow/User/Paginated?page=${page}&size=${size}&query=${query}`;
return sendJsonRequest<IUserPage>('GET', path, undefined, errorHandler);
return sendJsonRequest<IPage<IUserWithAccounts>>('GET', path, undefined, errorHandler);
},
getUser: (userId: number) => {
const path = `${baseUrl}/User/${userId}`;
Expand Down Expand Up @@ -726,6 +728,21 @@ export const WegasLobbyRestClient = function (
errorHandler,
);
},
getGamesPaginated: (
status: IGameWithId['status'],
page: number,
size: number,
query: string,
mine: boolean,
) => {
const path = `${baseUrl}/Lobby/GameModel/Game/status/${status}/Paginated?page=${page}&size=${size}&query=${query}&mine=${mine}`;
return sendJsonRequest<IPage<(IGameWithId & { gameModel?: IGameModelWithId })>>(
'GET',
path,
undefined,
errorHandler
)
},
changeStatus: (gameId: number, status: IGameWithId['status']) => {
const path = `${baseUrl}/Lobby/GameModel/Game/${gameId}/status/${status}`;
return sendJsonRequest<IGameWithId>('PUT', path, undefined, errorHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ import { defaultSelectStyles, mainButtonStyle } from '../styling/style';

interface CreateGameProps {
close: () => void;
callback: () => void;
}

export default function CreateGame({ close }: CreateGameProps): JSX.Element {
export default function CreateGame({ close, callback }: CreateGameProps): JSX.Element {
const dispatch = useAppDispatch();
const i18n = useTranslations();
const { currentUser } = useCurrentUser();
Expand All @@ -48,6 +49,7 @@ export default function CreateGame({ close }: CreateGameProps): JSX.Element {
if (a.meta.requestStatus === 'fulfilled') {
close();
}
callback();
});
}
}, [dispatch, gameModelId, name, close]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import { css } from '@emotion/css';
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { uniq } from 'lodash';
import * as React from 'react';
import {useMatch, useResolvedPath} from 'react-router-dom';
import { IAbstractAccount, IGameModelWithId, IGameWithId } from 'wegas-ts-api';
import { getGames, getShadowUserByIds } from '../../API/api';
import { getDisplayName, mapByKey, match } from '../../helper';
import { useMatch, useResolvedPath } from 'react-router-dom';
import { IGameModelWithId, IGameWithId } from 'wegas-ts-api';
import { getGamesPaginated, getShadowUserByIds } from '../../API/api';
import useTranslations from '../../i18n/I18nContext';
import { useLocalStorageState } from '../../preferences';
import { useAccountsByUserIds, useCurrentUser } from '../../selectors/userSelector';
Expand All @@ -27,31 +26,11 @@ import FitSpace from '../common/FitSpace';
import Flex from '../common/Flex';
import IconButton from '../common/IconButton';
import InlineLoading from '../common/InlineLoading';
import SortBy, { SortByOption } from '../common/SortBy';
import { successColor } from '../styling/color';
import { panelPadding } from '../styling/style';
import CreateGame from './CreateGame';
import GameCard from './GameCard';

interface SortBy {
createdByName: string;
name: string;
createdTime: number;
}

const matchSearch =
(accountMap: Record<number, IAbstractAccount>, search: string) =>
({ game, gameModel }: { game: IGameWithId; gameModel: IGameModelWithId }) => {
return match(search, regex => {
const username = game.createdById != null ? getDisplayName(accountMap[game.createdById]) : '';
return (
(gameModel.name && gameModel.name.match(regex) != null) ||
(game.name && game.name.match(regex) != null) ||
(game.token && game.token.match(regex) != null) ||
username.match(regex) != null
);
});
};
import Checkbox from '../common/Checkbox';

export default function TrainerTab(): JSX.Element {
const i18n = useTranslations();
Expand All @@ -74,47 +53,50 @@ export default function TrainerTab(): JSX.Element {
}, [isAdmin, statusFilter, setStatusFilter]);

const games = useGames(
!isAdmin && statusFilter === 'DELETE' ? 'BIN' : statusFilter, //non-admin should never sees deleteds
!isAdmin && statusFilter === 'DELETE' ? 'BIN' : statusFilter, //non-admin should never see deleted
currentUser != null ? currentUser.id : undefined,
isAdmin ? mineFilter : 'MINE', // non-admin only see theirs
);

const [viewMode, setViewMode] = React.useState<'EXPANDED' | 'COLLAPSED'>('COLLAPSED');

const [sortBy, setSortBy] = useLocalStorageState<{ key: keyof SortBy; asc: boolean }>(
'trainer-sortby',
{
key: 'createdTime',
asc: false,
},
);

// const onSortChange = React.useCallback(({ key, asc }: { key: keyof SortBy; asc: boolean }) => {
// setSortBy({ key, asc });
// }, []);

const [filter, setFilter] = React.useState('');
const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(20);

const onNextPage = () => setPage(page < games.totalResults / pageSize ? page + 1 : page);
const onPreviousPage = () => setPage(page > 1 ? page - 1 : 1);

const onFilterChange = React.useCallback((filter: string) => {
setFilter(filter);
}, []);

const sortOptions: SortByOption<SortBy>[] = [
{ key: 'createdTime', label: i18n.date },
{ key: 'name', label: i18n.name },
];
if (isAdmin) {
sortOptions.push({ key: 'createdByName', label: i18n.createdBy });
}

const status = games.status[statusFilter];

React.useEffect(() => {
if (status === 'NOT_INITIALIZED') {
dispatch(getGames(statusFilter));
dispatch(
getGamesPaginated({ status: statusFilter, page: page, size: pageSize, query: filter, mine: mineFilter === 'MINE' }),
);
}
}, [status, dispatch, statusFilter]);

React.useEffect(() => {
if (page !== 1) {
setPage(1);
} else {
dispatch(
getGamesPaginated({ status: statusFilter, page: page, size: pageSize, query: filter, mine: mineFilter === 'MINE' }),
);
}
}, [filter, pageSize, statusFilter, mineFilter]);

React.useEffect(() => {
dispatch(
getGamesPaginated({ status: statusFilter, page: page, size: pageSize, query: filter, mine: mineFilter === 'MINE' }),
);
}, [page]);

const userIds = uniq(
games.gamesAndGameModels.flatMap(data =>
data.game.createdById != null ? [data.game.createdById] : [],
Expand All @@ -133,7 +115,6 @@ export default function TrainerTab(): JSX.Element {
);

const accountsState = useAccountsByUserIds(userIds);
const accounts = mapByKey(accountsState.accounts, 'parentId');

React.useEffect(() => {
if (isAdmin && accountsState.unknownUsers.length > 0) {
Expand All @@ -142,7 +123,7 @@ export default function TrainerTab(): JSX.Element {
}, [isAdmin, accountsState, dispatch]);

// Detect any gameModel id in URL
const resolvedPath = useResolvedPath("./");
const resolvedPath = useResolvedPath('./');

const match = useMatch<'id', string>(`${resolvedPath.pathname}:id/*`);
const selectedId = Number(match?.params.id) || undefined;
Expand All @@ -152,24 +133,6 @@ export default function TrainerTab(): JSX.Element {
} else {
const selected = games.gamesAndGameModels.find(ggm => ggm.gameModel.id === selectedId);

const filtered = filter
? games.gamesAndGameModels.filter(matchSearch(accounts, filter))
: games.gamesAndGameModels;
const sorted = filtered.sort((a, b) => {
const reverse = sortBy.asc ? 1 : -1;
if (sortBy.key === 'createdTime') {
return reverse * (a.game.createdTime! - b.game.createdTime!);
} else if (sortBy.key === 'name') {
const aName =
a.game.createdById != null ? getDisplayName(accounts[a.game.createdById]) : '';
const bName =
b.game.createdById != null ? getDisplayName(accounts[b.game.createdById]) : '';

return reverse * aName.localeCompare(bName);
}
return 0;
});

const statusFilterEntries: { value: IGameWithId['status']; label: React.ReactNode }[] = [
{
value: 'LIVE',
Expand Down Expand Up @@ -229,6 +192,17 @@ export default function TrainerTab(): JSX.Element {
close={() => {
setViewMode('COLLAPSED');
}}
callback={() =>
dispatch(
getGamesPaginated({
status: statusFilter,
page: page,
size: pageSize,
query: filter,
mine: mineFilter === 'MINE',
}),
)
}
/>
) : null}
</DropDownPanel>
Expand All @@ -251,7 +225,6 @@ export default function TrainerTab(): JSX.Element {
>
{i18n.createGame}
</IconButton>
<SortBy options={sortOptions} current={sortBy} onChange={setSortBy} />

{dropDownStatus}
{dropDownMine}
Expand All @@ -264,10 +237,58 @@ export default function TrainerTab(): JSX.Element {
/>
</Flex>

<Flex
justify="space-between"
align="center"
className={css({
flexShrink: 0,
height: '20px',
})}
>
<div
className={css({
display: 'flex',
alignContent: 'flex-start',
})}
>
<h3>{`${games.totalResults} ${i18n.games}`}</h3>
</div>
<div>
<h3>
<IconButton onClick={onPreviousPage} icon={'caret-left'}></IconButton>
{page}/{games.totalResults > 0 ? Math.ceil(games.totalResults / pageSize) : 1}
<IconButton onClick={onNextPage} icon={'caret-right'}></IconButton>
</h3>
</div>
<div
className={css({
display: 'flex',
alignContent: 'flex-end',
flexDirection: 'row',
})}
>
<Checkbox
label="20"
value={pageSize === 20}
onChange={(newValue: boolean) => setPageSize(newValue ? 20 : pageSize)}
/>
<Checkbox
label="50"
value={pageSize === 50}
onChange={(newValue: boolean) => setPageSize(newValue ? 50 : pageSize)}
/>
<Checkbox
label="100"
value={pageSize === 100}
onChange={(newValue: boolean) => setPageSize(newValue ? 100 : pageSize)}
/>
</div>
</Flex>

{status === 'READY' ? (
<>
<WindowedContainer
items={sorted}
items={games.gamesAndGameModels}
scrollTo={selected}
emptyMessage={<i>{filter ? i18n.noGamesFound : i18n.noGames}</i>}
>
Expand Down
2 changes: 1 addition & 1 deletion wegas-app/src/main/node/wegas-lobby/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const fr: WegasTranslations = {
gameNotFound: "clé d'accès invalide",
//
Game: 'Partie',
games: 'parites',
games: 'parties',
allGames: 'Toutes les parties',
archive: 'achiver',
restore: 'restaurer',
Expand Down
Loading