From 36bc19c96dc86e7d9bd5dd61f753b5ec3d96efe3 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sun, 8 Dec 2024 12:06:38 -0800 Subject: [PATCH] feat: Add consume invite dialog --- availabili.tf/src/assets/main.css | 16 +++++ availabili.tf/src/client/index.ts | 1 + .../client/models/ConsumeInviteResponse.ts | 8 +++ .../src/client/services/DefaultService.ts | 46 +++++++------ .../src/components/InviteKeyDialog.vue | 65 +++++++++++++++++++ .../src/components/TeamsListSidebar.vue | 14 ++-- availabili.tf/src/stores/teams/invites.ts | 10 ++- availabili.tf/src/views/TeamDetailsView.vue | 6 +- backend-flask/team.py | 12 ++-- backend-flask/team_invite.py | 18 +++-- 10 files changed, 148 insertions(+), 48 deletions(-) create mode 100644 availabili.tf/src/client/models/ConsumeInviteResponse.ts create mode 100644 availabili.tf/src/components/InviteKeyDialog.vue diff --git a/availabili.tf/src/assets/main.css b/availabili.tf/src/assets/main.css index 15e0196..df4efc1 100644 --- a/availabili.tf/src/assets/main.css +++ b/availabili.tf/src/assets/main.css @@ -307,6 +307,22 @@ hr { width: 100%; } +.dialog-overlay { + background-color: #00000055; + z-index: 1; + position: fixed; + inset: 0; +} + +[role="dialog"] { + position: fixed; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + background-color: var(--base); + z-index: 10; +} + div.banner { padding: 0.5rem 1rem; font-size: 10pt; diff --git a/availabili.tf/src/client/index.ts b/availabili.tf/src/client/index.ts index 53f0cf1..b486b79 100644 --- a/availabili.tf/src/client/index.ts +++ b/availabili.tf/src/client/index.ts @@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { AddPlayerJson } from './models/AddPlayerJson'; export type { AttendanceJson } from './models/AttendanceJson'; export type { AvailabilitySchema } from './models/AvailabilitySchema'; +export type { ConsumeInviteResponse } from './models/ConsumeInviteResponse'; export type { CreateEventJson } from './models/CreateEventJson'; export type { CreateTeamJson } from './models/CreateTeamJson'; export type { EditMemberRolesJson } from './models/EditMemberRolesJson'; diff --git a/availabili.tf/src/client/models/ConsumeInviteResponse.ts b/availabili.tf/src/client/models/ConsumeInviteResponse.ts new file mode 100644 index 0000000..e72356d --- /dev/null +++ b/availabili.tf/src/client/models/ConsumeInviteResponse.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ConsumeInviteResponse = { + teamId: number; +}; + diff --git a/availabili.tf/src/client/services/DefaultService.ts b/availabili.tf/src/client/services/DefaultService.ts index 0f19739..a3c00d2 100644 --- a/availabili.tf/src/client/services/DefaultService.ts +++ b/availabili.tf/src/client/services/DefaultService.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { AddPlayerJson } from '../models/AddPlayerJson'; import type { AttendanceJson } from '../models/AttendanceJson'; +import type { ConsumeInviteResponse } from '../models/ConsumeInviteResponse'; import type { CreateEventJson } from '../models/CreateEventJson'; import type { CreateTeamJson } from '../models/CreateTeamJson'; import type { EditMemberRolesJson } from '../models/EditMemberRolesJson'; @@ -415,6 +416,27 @@ export class DefaultService { }, }); } + /** + * consume_invite + * @param key + * @returns ConsumeInviteResponse OK + * @throws ApiError + */ + public consumeInvite( + key: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/api/team/consume-invite/{key}', + path: { + 'key': key, + }, + errors: { + 404: `Not Found`, + 422: `Unprocessable Content`, + }, + }); + } /** * delete_team * @param teamId @@ -483,30 +505,6 @@ export class DefaultService { }, }); } - /** - * consume_invite - * @param teamId - * @param key - * @returns void - * @throws ApiError - */ - public consumeInvite( - teamId: string, - key: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'POST', - url: '/api/team/id/{team_id}/consume-invite/{key}', - path: { - 'team_id': teamId, - 'key': key, - }, - errors: { - 404: `Not Found`, - 422: `Unprocessable Content`, - }, - }); - } /** * edit_member_roles * @param teamId diff --git a/availabili.tf/src/components/InviteKeyDialog.vue b/availabili.tf/src/components/InviteKeyDialog.vue new file mode 100644 index 0000000..a58744b --- /dev/null +++ b/availabili.tf/src/components/InviteKeyDialog.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/availabili.tf/src/components/TeamsListSidebar.vue b/availabili.tf/src/components/TeamsListSidebar.vue index b9a5f35..7f1c7f5 100644 --- a/availabili.tf/src/components/TeamsListSidebar.vue +++ b/availabili.tf/src/components/TeamsListSidebar.vue @@ -2,9 +2,13 @@ import { onMounted } from "vue"; import { useTeamsStore } from "../stores/teams"; import { RouterLink } from "vue-router"; +import { useAuthStore } from "@/stores/auth"; +import InviteKeyDialog from "./InviteKeyDialog.vue"; const teams = useTeamsStore(); +const authStore = useAuthStore(); + onMounted(() => { teams.fetchTeams(); }); @@ -17,11 +21,8 @@ onMounted(() => { Your Teams -
- +
+
+
+ Log in to view your teams. +
{ return response; } - async function consumeInvite(teamId: number, key: string) { - const response = await client.default.consumeInvite(teamId.toString(), key); - teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key); + async function consumeInvite(key: string) { + const response = await client.default.consumeInvite(key); + const teamId = response.teamId; + if (teamInvites[teamId]) { + teamInvites[teamId] = teamInvites[teamId] + .filter((invite) => invite.key != key); + } return response; } diff --git a/availabili.tf/src/views/TeamDetailsView.vue b/availabili.tf/src/views/TeamDetailsView.vue index e6af331..ec8a67d 100644 --- a/availabili.tf/src/views/TeamDetailsView.vue +++ b/availabili.tf/src/views/TeamDetailsView.vue @@ -36,7 +36,7 @@ onMounted(() => { }; if (key.value) { - invitesStore.consumeInvite(teamId.value, key.value.toString()) + invitesStore.consumeInvite(key.value.toString()) .finally(doFetchTeam); } else { doFetchTeam(); @@ -73,7 +73,7 @@ onMounted(() => {

Upcoming Events

- +

Recent Matches @@ -82,7 +82,7 @@ onMounted(() => {

- No recent matches. + No recent matches.
diff --git a/backend-flask/team.py b/backend-flask/team.py index e93eefd..97aa957 100644 --- a/backend-flask/team.py +++ b/backend-flask/team.py @@ -9,7 +9,7 @@ from models.player import Player, PlayerSchema from models.player_team import PlayerTeam from models.player_team_availability import PlayerTeamAvailability from models.player_team_role import PlayerTeamRole, RoleSchema -from models.team import Team, TeamSchema +from models.team import Team, TeamSchema, TeamWithRoleSchema from middleware import assert_team_authority, requires_authentication, requires_team_membership from spec import spec, BaseModel from team_invite import api_team_invite @@ -51,7 +51,7 @@ class ViewTeamResponse(BaseModel): team: TeamSchema class ViewTeamsResponse(BaseModel): - teams: list[TeamSchema] + teams: list[TeamWithRoleSchema] @api_team.post("/") @spec.validate( @@ -290,7 +290,7 @@ def view_team(team_id: int, **kwargs): def fetch_teams_for_player(player: Player, team_id: int | None): q = db.session.query( - Team + Team, PlayerTeam ).join( PlayerTeam ).join( @@ -303,15 +303,15 @@ def fetch_teams_for_player(player: Player, team_id: int | None): q = q.where(PlayerTeam.team_id == team_id) if team_id is None: - teams = q.all() + players_teams = list(map(lambda x: x.tuple()[1], q.all())) return ViewTeamsResponse( - teams=list(map(TeamSchema.from_model, teams)) + teams=list(map(TeamWithRoleSchema.from_player_team, players_teams)) ) else: team = q.one_or_none() if team: return ViewTeamResponse( - team=TeamSchema.from_model(team) + team=TeamSchema.from_model(team.tuple()[0]) ) class ViewTeamMembersResponse(PlayerSchema): diff --git a/backend-flask/team_invite.py b/backend-flask/team_invite.py index 1521ac3..5cee2a3 100644 --- a/backend-flask/team_invite.py +++ b/backend-flask/team_invite.py @@ -8,7 +8,7 @@ from middleware import requires_authentication, requires_team_membership from models.player import Player from models.player_team import PlayerTeam from models.team_invite import TeamInvite, TeamInviteSchema -from spec import spec +from spec import BaseModel, spec api_team_invite = Blueprint("team_invite", __name__) @@ -75,27 +75,31 @@ def create_invite(team_id: int, **_): return response.dict(by_alias=True), 200 -@api_team_invite.post("/id//consume-invite/") +class ConsumeInviteResponse(BaseModel): + team_id: int + +@api_team_invite.post("/consume-invite/") @spec.validate( resp=Response( - HTTP_204=None, + HTTP_200=ConsumeInviteResponse, HTTP_404=None, ), operation_id="consume_invite" ) @requires_authentication -def consume_invite(player: Player, team_id: int, key: str, **_): +def consume_invite(player: Player, key: str, **_): invite = db.session.query( TeamInvite - ).where( - TeamInvite.team_id == team_id ).where( TeamInvite.key == key ).one_or_none() + if not invite: abort(404) + team_id = invite.team_id + player_team = db.session.query( PlayerTeam ).where( @@ -118,7 +122,7 @@ def consume_invite(player: Player, team_id: int, key: str, **_): db.session.commit() - return make_response({ }, 204) + return ConsumeInviteResponse(team_id=team_id).dict(by_alias=True), 200 @api_team_invite.delete("/id//invite/") @spec.validate(