feat: Add consume invite dialog
parent
242562d662
commit
36bc19c96d
|
@ -307,6 +307,22 @@ hr {
|
||||||
width: 100%;
|
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 {
|
div.banner {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
|
|
|
@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
|
||||||
export type { AddPlayerJson } from './models/AddPlayerJson';
|
export type { AddPlayerJson } from './models/AddPlayerJson';
|
||||||
export type { AttendanceJson } from './models/AttendanceJson';
|
export type { AttendanceJson } from './models/AttendanceJson';
|
||||||
export type { AvailabilitySchema } from './models/AvailabilitySchema';
|
export type { AvailabilitySchema } from './models/AvailabilitySchema';
|
||||||
|
export type { ConsumeInviteResponse } from './models/ConsumeInviteResponse';
|
||||||
export type { CreateEventJson } from './models/CreateEventJson';
|
export type { CreateEventJson } from './models/CreateEventJson';
|
||||||
export type { CreateTeamJson } from './models/CreateTeamJson';
|
export type { CreateTeamJson } from './models/CreateTeamJson';
|
||||||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type ConsumeInviteResponse = {
|
||||||
|
teamId: number;
|
||||||
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
||||||
import type { AttendanceJson } from '../models/AttendanceJson';
|
import type { AttendanceJson } from '../models/AttendanceJson';
|
||||||
|
import type { ConsumeInviteResponse } from '../models/ConsumeInviteResponse';
|
||||||
import type { CreateEventJson } from '../models/CreateEventJson';
|
import type { CreateEventJson } from '../models/CreateEventJson';
|
||||||
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
||||||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
||||||
|
@ -415,6 +416,27 @@ export class DefaultService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* consume_invite <POST>
|
||||||
|
* @param key
|
||||||
|
* @returns ConsumeInviteResponse OK
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public consumeInvite(
|
||||||
|
key: string,
|
||||||
|
): CancelablePromise<ConsumeInviteResponse> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/team/consume-invite/{key}',
|
||||||
|
path: {
|
||||||
|
'key': key,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
404: `Not Found`,
|
||||||
|
422: `Unprocessable Content`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* delete_team <DELETE>
|
* delete_team <DELETE>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
@ -483,30 +505,6 @@ export class DefaultService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* consume_invite <POST>
|
|
||||||
* @param teamId
|
|
||||||
* @param key
|
|
||||||
* @returns void
|
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public consumeInvite(
|
|
||||||
teamId: string,
|
|
||||||
key: string,
|
|
||||||
): CancelablePromise<void> {
|
|
||||||
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 <PATCH>
|
* edit_member_roles <PATCH>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useInvitesStore } from "@/stores/teams/invites";
|
||||||
|
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const invitesStore = useInvitesStore();
|
||||||
|
|
||||||
|
const key = ref("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
invitesStore.consumeInvite(key.value)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
router.push({
|
||||||
|
name: "team-details",
|
||||||
|
params: {
|
||||||
|
id: response.teamId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogRoot>
|
||||||
|
<DialogTrigger>
|
||||||
|
<i class="bi bi-person-plus-fill margin" />
|
||||||
|
Join a team
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay class="dialog-overlay" />
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>Join a team</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<p>
|
||||||
|
Enter the invite key to join a team. Don't have an invite key? Ask
|
||||||
|
your team leader to send you one.
|
||||||
|
</p>
|
||||||
|
</DialogDescription>
|
||||||
|
<div class="form-group margin">
|
||||||
|
<h3>Invite key</h3>
|
||||||
|
<input type="text" placeholder="Invite key or URL" v-model="key" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<DialogClose class="accent" aria-label="Close" @click="submit">
|
||||||
|
<i class="bi bi-check" />
|
||||||
|
Join
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
[role="dialog"] {
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,9 +2,13 @@
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { useTeamsStore } from "../stores/teams";
|
import { useTeamsStore } from "../stores/teams";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import InviteKeyDialog from "./InviteKeyDialog.vue";
|
||||||
|
|
||||||
const teams = useTeamsStore();
|
const teams = useTeamsStore();
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
teams.fetchTeams();
|
teams.fetchTeams();
|
||||||
});
|
});
|
||||||
|
@ -17,11 +21,8 @@ onMounted(() => {
|
||||||
<i class="bi bi-people-fill margin"></i>
|
<i class="bi bi-people-fill margin"></i>
|
||||||
Your Teams
|
Your Teams
|
||||||
</h2>
|
</h2>
|
||||||
<div class="button-group">
|
<div class="button-group" v-if="authStore.isLoggedIn">
|
||||||
<button class="small">
|
<InviteKeyDialog />
|
||||||
<i class="bi bi-person-plus-fill margin" />
|
|
||||||
Join a team
|
|
||||||
</button>
|
|
||||||
<RouterLink class="button" to="/team/register">
|
<RouterLink class="button" to="/team/register">
|
||||||
<button class="small accent">
|
<button class="small accent">
|
||||||
<i class="bi bi-plus-circle-fill margin"></i>
|
<i class="bi bi-plus-circle-fill margin"></i>
|
||||||
|
@ -30,6 +31,9 @@ onMounted(() => {
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!authStore.isLoggedIn">
|
||||||
|
Log in to view your teams.
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="teams.teamsWithRole"
|
v-if="teams.teamsWithRole"
|
||||||
v-for="(team, _, i) in teams.teamsWithRole"
|
v-for="(team, _, i) in teams.teamsWithRole"
|
||||||
|
|
|
@ -26,9 +26,13 @@ export const useInvitesStore = defineStore("invites", () => {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function consumeInvite(teamId: number, key: string) {
|
async function consumeInvite(key: string) {
|
||||||
const response = await client.default.consumeInvite(teamId.toString(), key);
|
const response = await client.default.consumeInvite(key);
|
||||||
teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
|
const teamId = response.teamId;
|
||||||
|
if (teamInvites[teamId]) {
|
||||||
|
teamInvites[teamId] = teamInvites[teamId]
|
||||||
|
.filter((invite) => invite.key != key);
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ onMounted(() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (key.value) {
|
if (key.value) {
|
||||||
invitesStore.consumeInvite(teamId.value, key.value.toString())
|
invitesStore.consumeInvite(key.value.toString())
|
||||||
.finally(doFetchTeam);
|
.finally(doFetchTeam);
|
||||||
} else {
|
} else {
|
||||||
doFetchTeam();
|
doFetchTeam();
|
||||||
|
@ -73,7 +73,7 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<h2>Upcoming Events</h2>
|
<h2>Upcoming Events</h2>
|
||||||
<EventList :events="events" />
|
<EventList :events="events" :team-context="team" />
|
||||||
<h2 id="recent-matches-header">
|
<h2 id="recent-matches-header">
|
||||||
Recent Matches
|
Recent Matches
|
||||||
<RouterLink class="button" to="/">
|
<RouterLink class="button" to="/">
|
||||||
|
@ -82,7 +82,7 @@ onMounted(() => {
|
||||||
</button>
|
</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</h2>
|
</h2>
|
||||||
<em class="subtext" v-if="false">No recent matches.</em>
|
<em class="subtext" v-if="true">No recent matches.</em>
|
||||||
<MatchCard v-else />
|
<MatchCard v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ from models.player import Player, PlayerSchema
|
||||||
from models.player_team import PlayerTeam
|
from models.player_team import PlayerTeam
|
||||||
from models.player_team_availability import PlayerTeamAvailability
|
from models.player_team_availability import PlayerTeamAvailability
|
||||||
from models.player_team_role import PlayerTeamRole, RoleSchema
|
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 middleware import assert_team_authority, requires_authentication, requires_team_membership
|
||||||
from spec import spec, BaseModel
|
from spec import spec, BaseModel
|
||||||
from team_invite import api_team_invite
|
from team_invite import api_team_invite
|
||||||
|
@ -51,7 +51,7 @@ class ViewTeamResponse(BaseModel):
|
||||||
team: TeamSchema
|
team: TeamSchema
|
||||||
|
|
||||||
class ViewTeamsResponse(BaseModel):
|
class ViewTeamsResponse(BaseModel):
|
||||||
teams: list[TeamSchema]
|
teams: list[TeamWithRoleSchema]
|
||||||
|
|
||||||
@api_team.post("/")
|
@api_team.post("/")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
@ -290,7 +290,7 @@ def view_team(team_id: int, **kwargs):
|
||||||
|
|
||||||
def fetch_teams_for_player(player: Player, team_id: int | None):
|
def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||||
q = db.session.query(
|
q = db.session.query(
|
||||||
Team
|
Team, PlayerTeam
|
||||||
).join(
|
).join(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).join(
|
).join(
|
||||||
|
@ -303,15 +303,15 @@ def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||||
q = q.where(PlayerTeam.team_id == team_id)
|
q = q.where(PlayerTeam.team_id == team_id)
|
||||||
|
|
||||||
if team_id is None:
|
if team_id is None:
|
||||||
teams = q.all()
|
players_teams = list(map(lambda x: x.tuple()[1], q.all()))
|
||||||
return ViewTeamsResponse(
|
return ViewTeamsResponse(
|
||||||
teams=list(map(TeamSchema.from_model, teams))
|
teams=list(map(TeamWithRoleSchema.from_player_team, players_teams))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
team = q.one_or_none()
|
team = q.one_or_none()
|
||||||
if team:
|
if team:
|
||||||
return ViewTeamResponse(
|
return ViewTeamResponse(
|
||||||
team=TeamSchema.from_model(team)
|
team=TeamSchema.from_model(team.tuple()[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
class ViewTeamMembersResponse(PlayerSchema):
|
class ViewTeamMembersResponse(PlayerSchema):
|
||||||
|
|
|
@ -8,7 +8,7 @@ from middleware import requires_authentication, requires_team_membership
|
||||||
from models.player import Player
|
from models.player import Player
|
||||||
from models.player_team import PlayerTeam
|
from models.player_team import PlayerTeam
|
||||||
from models.team_invite import TeamInvite, TeamInviteSchema
|
from models.team_invite import TeamInvite, TeamInviteSchema
|
||||||
from spec import spec
|
from spec import BaseModel, spec
|
||||||
|
|
||||||
|
|
||||||
api_team_invite = Blueprint("team_invite", __name__)
|
api_team_invite = Blueprint("team_invite", __name__)
|
||||||
|
@ -75,27 +75,31 @@ def create_invite(team_id: int, **_):
|
||||||
|
|
||||||
return response.dict(by_alias=True), 200
|
return response.dict(by_alias=True), 200
|
||||||
|
|
||||||
@api_team_invite.post("/id/<team_id>/consume-invite/<key>")
|
class ConsumeInviteResponse(BaseModel):
|
||||||
|
team_id: int
|
||||||
|
|
||||||
|
@api_team_invite.post("/consume-invite/<key>")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
resp=Response(
|
resp=Response(
|
||||||
HTTP_204=None,
|
HTTP_200=ConsumeInviteResponse,
|
||||||
HTTP_404=None,
|
HTTP_404=None,
|
||||||
),
|
),
|
||||||
operation_id="consume_invite"
|
operation_id="consume_invite"
|
||||||
)
|
)
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def consume_invite(player: Player, team_id: int, key: str, **_):
|
def consume_invite(player: Player, key: str, **_):
|
||||||
invite = db.session.query(
|
invite = db.session.query(
|
||||||
TeamInvite
|
TeamInvite
|
||||||
).where(
|
|
||||||
TeamInvite.team_id == team_id
|
|
||||||
).where(
|
).where(
|
||||||
TeamInvite.key == key
|
TeamInvite.key == key
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
if not invite:
|
if not invite:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
team_id = invite.team_id
|
||||||
|
|
||||||
player_team = db.session.query(
|
player_team = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).where(
|
).where(
|
||||||
|
@ -118,7 +122,7 @@ def consume_invite(player: Player, team_id: int, key: str, **_):
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return make_response({ }, 204)
|
return ConsumeInviteResponse(team_id=team_id).dict(by_alias=True), 200
|
||||||
|
|
||||||
@api_team_invite.delete("/id/<team_id>/invite/<key>")
|
@api_team_invite.delete("/id/<team_id>/invite/<key>")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
|
Loading…
Reference in New Issue