UX improvements (#722)

This commit is contained in:
Erik Vroon
2024-05-14 19:43:42 +02:00
committed by GitHub
parent 2e9e4343b3
commit fe458771fc
16 changed files with 143 additions and 107 deletions

View File

@@ -61,7 +61,7 @@ async def delete_stage(
detail="Stage contains stage items, please delete those first",
)
if stage.is_active:
if stage.is_active and len(stage.stage_items) > 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Stage is active, please activate another stage first",

View File

@@ -109,7 +109,14 @@ async def update_tournament_by_id(
async def delete_tournament(
tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament)
) -> SuccessResponse:
with check_foreign_key_violation({ForeignKey.stages_tournament_id_fkey}):
with check_foreign_key_violation(
{
ForeignKey.stages_tournament_id_fkey,
ForeignKey.teams_tournament_id_fkey,
ForeignKey.players_tournament_id_fkey,
ForeignKey.courts_tournament_id_fkey,
}
):
await sql_delete_tournament(tournament_id)
return SuccessResponse()

View File

@@ -16,12 +16,16 @@ class UniqueIndex(EnumAutoStr):
class ForeignKey(EnumAutoStr):
stages_tournament_id_fkey = auto()
tournaments_club_id_fkey = auto()
stage_item_inputs_team_id_fkey = auto()
courts_tournament_id_fkey = auto()
matches_team1_id_fkey = auto()
matches_team2_id_fkey = auto()
matches_team1_winner_from_stage_item_id_fkey = auto()
matches_team2_id_fkey = auto()
matches_team2_winner_from_stage_item_id_fkey = auto()
players_tournament_id_fkey = auto()
stage_item_inputs_team_id_fkey = auto()
stages_tournament_id_fkey = auto()
teams_tournament_id_fkey = auto()
tournaments_club_id_fkey = auto()
unique_index_violation_error_lookup = {
@@ -31,13 +35,18 @@ unique_index_violation_error_lookup = {
foreign_key_violation_error_lookup = {
ForeignKey.stages_tournament_id_fkey: "This tournament still has stages, delete those first",
ForeignKey.tournaments_club_id_fkey: "This club still has tournaments, delete those first",
ForeignKey.stage_item_inputs_team_id_fkey: "This team is still used in stage items",
ForeignKey.courts_tournament_id_fkey: "This tournament still has courts, delete those first",
ForeignKey.matches_team1_id_fkey: "This team is still part of matches",
ForeignKey.matches_team2_id_fkey: "This team is still part of matches",
ForeignKey.matches_team1_winner_from_stage_item_id_fkey: "This stage item is referenced by "
"other stage items",
ForeignKey.matches_team2_id_fkey: "This team is still part of matches",
ForeignKey.matches_team2_winner_from_stage_item_id_fkey: "This stage item is referenced by "
"other stage items",
ForeignKey.players_tournament_id_fkey: "This tournament still has players, delete those first",
ForeignKey.stage_item_inputs_team_id_fkey: "This team is still used in stage items",
ForeignKey.stages_tournament_id_fkey: "This tournament still has stages, delete those first",
ForeignKey.teams_tournament_id_fkey: "This tournament still has teams, delete those first",
ForeignKey.tournaments_club_id_fkey: "This club still has tournaments, delete those first",
}

View File

@@ -70,6 +70,7 @@ disable = [
'unspecified-encoding',
'unused-argument', # Gives false positives.
'wrong-import-position',
'contextmanager-generator-missing-cleanup', # Gives false positives.
]
[tool.bandit]

View File

@@ -72,6 +72,5 @@ async def reinit_database(event_loop: AbstractEventLoop, worker_id: str) -> Asyn
@pytest.fixture(scope="session")
async def auth_context(reinit_database: Database) -> AsyncIterator[AuthContext]:
async with reinit_database:
async with inserted_auth_context() as auth_context:
yield auth_context
async with reinit_database, inserted_auth_context() as auth_context:
yield auth_context

View File

@@ -140,9 +140,11 @@
"negative_match_margin_validation": "Match margin cannot be negative",
"negative_score_validation": "Score cannot be negative",
"next_matches_badge": "Next matches",
"next_stage_button": "Go to Next Stage",
"next_stage_button": "Next Stage",
"no_matches_description": "First, add matches by creating stages and stage items. Then, schedule them using the button in the topright corner.",
"no_matches_title": "No matches scheduled yet",
"no_players_title": "No players yet",
"no_teams_title": "No teams yet",
"no_round_description": "There are no rounds in this stage item yet",
"no_round_found_description": "Please wait for the organiser to add them.",
"no_round_found_in_stage_description": "There are no rounds in this stage yet",
@@ -168,7 +170,7 @@
"players_spotlight_description": "View, add or delete players",
"players_title": "players",
"policy_not_accepted": "Please indicate that you have read the policy",
"previous_stage_button": "Go to Previous Stage",
"previous_stage_button": "Previous Stage",
"recommended_badge_title": "Recommended",
"remove_logo": "Remove logo",
"remove_match_button": "Remove Match",

View File

@@ -47,7 +47,7 @@ export function CreateStageButtonLarge({
variant="outline"
color="green"
size="lg"
style={{ marginRight: 10, width: '25%' }}
style={{ marginRight: 10 }}
onClick={async () => {
await createStage(tournament.id);
await swrStagesResponse.mutate();

View File

@@ -1,4 +1,4 @@
import { Button, Group, Modal, TextInput } from '@mantine/core';
import { Button, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { BiEditAlt } from '@react-icons/all-files/bi/BiEditAlt';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
@@ -23,13 +23,13 @@ export default function ClubModal({
const icon = is_create_form ? <GoPlus size={20} /> : <BiEditAlt size={20} />;
const [opened, setOpened] = useState(false);
const modalOpenButton = is_create_form ? (
<Group justify="right">
<SaveButton
onClick={() => setOpened(true)}
leftSection={<GoPlus size={24} />}
title={operation_text}
/>
</Group>
<SaveButton
mx="0px"
fullWidth
onClick={() => setOpened(true)}
leftSection={<GoPlus size={24} />}
title={operation_text}
/>
) : (
<Button
color="green"

View File

@@ -190,6 +190,7 @@ export default function TournamentModal({
/>
</Modal>
<SaveButton
mx="0px"
fullWidth
onClick={() => setOpened(true)}
leftSection={<GoPlus size={24} />}

View File

@@ -1,4 +1,4 @@
import { Badge, Table, Text } from '@mantine/core';
import { Badge, Center, Pagination, Table, Text } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { SWRResponse } from 'swr';
@@ -10,7 +10,7 @@ import DeleteButton from '../buttons/delete';
import { PlayerScore } from '../info/player_score';
import { WinDistribution } from '../info/player_statistics';
import PlayerUpdateModal from '../modals/player_update_modal';
import { EmptyTableInfo } from '../no_content/empty_table_info';
import { NoContent } from '../no_content/empty_table_info';
import { DateTime } from '../utils/datetime';
import RequestErrorAlert from '../utils/error_alert';
import { TableSkeletonSingleColumn } from '../utils/skeletons';
@@ -39,10 +39,12 @@ export default function PlayersTable({
swrPlayersResponse,
tournamentData,
tableState,
playerCount,
}: {
swrPlayersResponse: SWRResponse;
tournamentData: TournamentMinimal;
tableState: TableState;
playerCount: number;
}) {
const { t } = useTranslation();
const players: Player[] =
@@ -111,36 +113,46 @@ export default function PlayersTable({
</Table.Tr>
));
if (rows.length < 1) return <EmptyTableInfo entity_name={t('players_title')} />;
if (rows.length < 1) return <NoContent title={t('no_players_title')} />;
return (
<TableLayout miw={900}>
<Table.Thead>
<Table.Tr>
<ThSortable state={tableState} field="active">
{t('status')}
</ThSortable>
<ThSortable state={tableState} field="name">
{t('title')}
</ThSortable>
<ThSortable state={tableState} field="created">
{t('created')}
</ThSortable>
<ThNotSortable>
<>
<WinDistributionTitle />
</>
</ThNotSortable>
<ThSortable state={tableState} field="elo_score">
{t('elo_score')}
</ThSortable>
<ThSortable state={tableState} field="swiss_score">
{t('swiss_score')}
</ThSortable>
<ThNotSortable>{null}</ThNotSortable>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</TableLayout>
<>
<TableLayout miw={900}>
<Table.Thead>
<Table.Tr>
<ThSortable state={tableState} field="active">
{t('status')}
</ThSortable>
<ThSortable state={tableState} field="name">
{t('title')}
</ThSortable>
<ThSortable state={tableState} field="created">
{t('created')}
</ThSortable>
<ThNotSortable>
<>
<WinDistributionTitle />
</>
</ThNotSortable>
<ThSortable state={tableState} field="elo_score">
{t('elo_score')}
</ThSortable>
<ThSortable state={tableState} field="swiss_score">
{t('swiss_score')}
</ThSortable>
<ThNotSortable>{null}</ThNotSortable>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</TableLayout>
<Center mt="1rem">
<Pagination
value={tableState.page}
onChange={tableState.setPage}
total={1 + playerCount / tableState.pageSize}
size="lg"
/>
</Center>
</>
);
}

View File

@@ -1,4 +1,4 @@
import { Badge, Table } from '@mantine/core';
import { Badge, Center, Pagination, Table } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { SWRResponse } from 'swr';
@@ -9,7 +9,7 @@ import { deleteTeam } from '../../services/team';
import DeleteButton from '../buttons/delete';
import PlayerList from '../info/player_list';
import TeamUpdateModal from '../modals/team_update_modal';
import { EmptyTableInfo } from '../no_content/empty_table_info';
import { NoContent } from '../no_content/empty_table_info';
import { DateTime } from '../utils/datetime';
import RequestErrorAlert from '../utils/error_alert';
import { TableSkeletonSingleColumn } from '../utils/skeletons';
@@ -20,11 +20,13 @@ export default function TeamsTable({
swrTeamsResponse,
teams,
tableState,
teamCount,
}: {
tournamentData: TournamentMinimal;
swrTeamsResponse: SWRResponse;
teams: TeamInterface[];
tableState: TableState;
teamCount: number;
}) {
const { t } = useTranslation();
if (swrTeamsResponse.error) return <RequestErrorAlert error={swrTeamsResponse.error} />;
@@ -70,32 +72,43 @@ export default function TeamsTable({
</Table.Tr>
));
if (rows.length < 1) return <EmptyTableInfo entity_name={t('teams_title')} />;
if (rows.length < 1) return <NoContent title={t('no_teams_title')} />;
return (
<TableLayout miw={850}>
<Table.Thead>
<Table.Tr>
<ThSortable state={tableState} field="active">
{t('status')}
</ThSortable>
<ThSortable state={tableState} field="name">
{t('name_table_header')}
</ThSortable>
<ThNotSortable>{t('members_table_header')}</ThNotSortable>
<ThSortable state={tableState} field="created">
{t('created')}
</ThSortable>
<ThSortable state={tableState} field="swiss_score">
{t('swiss_score')}
</ThSortable>
<ThSortable state={tableState} field="elo_score">
{t('elo_score')}
</ThSortable>
<ThNotSortable>{null}</ThNotSortable>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</TableLayout>
<>
<TableLayout miw={850}>
<Table.Thead>
<Table.Tr>
<ThSortable state={tableState} field="active">
{t('status')}
</ThSortable>
<ThSortable state={tableState} field="name">
{t('name_table_header')}
</ThSortable>
<ThNotSortable>{t('members_table_header')}</ThNotSortable>
<ThSortable state={tableState} field="created">
{t('created')}
</ThSortable>
<ThSortable state={tableState} field="swiss_score">
{t('swiss_score')}
</ThSortable>
<ThSortable state={tableState} field="elo_score">
{t('elo_score')}
</ThSortable>
<ThNotSortable>{null}</ThNotSortable>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</TableLayout>
<Center mt="1rem">
<Pagination
value={tableState.page}
onChange={tableState.setPage}
total={1 + teamCount / tableState.pageSize}
size="lg"
/>
</Center>
</>
);
}

View File

@@ -7,6 +7,7 @@ import ClubsTable from '../components/tables/clubs';
import { capitalize } from '../components/utils/util';
import { checkForAuthError, getClubs } from '../services/adapter';
import Layout from './_layout';
import classes from './index.module.css';
export default function HomePage() {
const swrClubsResponse = getClubs();
@@ -16,11 +17,11 @@ export default function HomePage() {
return (
<Layout>
<Grid grow>
<Grid.Col span={9}>
<Grid justify="space-between">
<Grid.Col span="auto">
<Title>{capitalize(t('clubs_title'))}</Title>
</Grid.Col>
<Grid.Col span={3}>
<Grid.Col span="content" className={classes.fullWithMobile}>
<ClubModal swrClubsResponse={swrClubsResponse} club={null} />
</Grid.Col>
</Grid>

View File

@@ -1,4 +1,4 @@
import { Center, Grid, Pagination, Title } from '@mantine/core';
import { Grid, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
@@ -33,18 +33,11 @@ export default function Players() {
</Grid.Col>
</Grid>
<PlayersTable
playerCount={playerCount}
swrPlayersResponse={swrPlayersResponse}
tournamentData={tournamentData}
tableState={tableState}
/>
<Center mt="1rem">
<Pagination
value={tableState.page}
onChange={tableState.setPage}
total={1 + playerCount / tableState.pageSize}
size="lg"
/>
</Center>
</TournamentLayout>
);
}

View File

@@ -19,6 +19,7 @@ import { IconCalendar, IconCalendarTime } from '@tabler/icons-react';
import assert from 'assert';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import React from 'react';
import { SWRResponse } from 'swr';
@@ -32,6 +33,7 @@ import {
getBaseApiUrl,
getClubs,
getTournamentById,
handleRequestError,
removeTournamentLogo,
} from '../../../services/adapter';
import { deleteTournament, updateTournament } from '../../../services/tournament';
@@ -53,6 +55,7 @@ function GeneralTournamentForm({
swrTournamentResponse: SWRResponse;
clubs: Club[];
}) {
const router = useRouter();
const { t } = useTranslation();
const form = useForm({
@@ -228,7 +231,11 @@ function GeneralTournamentForm({
size="sm"
leftSection={<MdDelete size={20} />}
onClick={async () => {
await deleteTournament(tournament.id);
await deleteTournament(tournament.id)
.then(async () => {
await router.push('/');
})
.catch((response: any) => handleRequestError(response));
}}
>
{t('delete_tournament_button')}

View File

@@ -1,4 +1,4 @@
import { Center, Grid, Pagination, Select, Title } from '@mantine/core';
import { Grid, Select, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React, { useState } from 'react';
@@ -95,15 +95,8 @@ export default function Teams() {
tournamentData={tournamentData}
teams={teams}
tableState={tableState}
teamCount={teamCount}
/>
<Center mt="1rem">
<Pagination
value={tableState.page}
onChange={tableState.setPage}
total={1 + teamCount / tableState.pageSize}
size="lg"
/>
</Center>
</TournamentLayout>
);
}

View File

@@ -28,9 +28,7 @@ export async function createTournament(
}
export async function deleteTournament(tournament_id: number) {
return createAxios()
.delete(`tournaments/${tournament_id}`)
.catch((response: any) => handleRequestError(response));
return createAxios().delete(`tournaments/${tournament_id}`);
}
export async function updateTournament(