feat: Implement changing team settings

master
John Montagu, the 4th Earl of Sandvich 2024-12-05 17:20:02 -08:00
parent f0e2e28d65
commit 40641f80a3
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
7 changed files with 151 additions and 8 deletions

View File

@ -38,6 +38,7 @@
--crust: #dce0e8; --crust: #dce0e8;
--red: #d20f39; --red: #d20f39;
--blue: #1e66f5;
--flamingo: #dd7878; --flamingo: #dd7878;
--flamingo-transparent: #f0c6c655; --flamingo-transparent: #f0c6c655;
@ -52,13 +53,11 @@
*/ */
--green-transparent: color-mix(in srgb, var(--green), transparent 80%); --green-transparent: color-mix(in srgb, var(--green), transparent 80%);
--yellow-transparent: color-mix(in srgb, var(--yellow), transparent 80%); --yellow-transparent: color-mix(in srgb, var(--yellow), transparent 80%);
/* --blue-transparent: color-mix(in srgb, var(--blue), transparent 80%);
--green-transparent-50: #a6e3a1;
--yellow-transparent-50: #f9e2af;
*/
--lavender: #7287fd; --lavender: #7287fd;
--accent: var(--lavender); --accent: var(--lavender);
--lavender-transparent: color-mix(in srgb, var(--lavender), transparent 80%);
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%); --accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
--accent-transparent-50: color-mix(in srgb, var(--accent), transparent 50%); --accent-transparent-50: color-mix(in srgb, var(--accent), transparent 50%);
--accent-transparent: var(--accent-transparent-80); --accent-transparent: var(--accent-transparent-80);

View File

@ -313,3 +313,18 @@ div[role="menu"] div[role="menuitem"]:last-child button {
background-color: var(--surface-0); background-color: var(--surface-0);
width: 100%; width: 100%;
} }
div.banner {
padding: 0.5rem 1rem;
font-size: 10pt;
}
div.banner.warning {
background-color: var(--yellow-transparent);
color: var(--yellow);
}
div.banner.info {
background-color: var(--lavender-transparent);
color: var(--lavender);
}

View File

@ -17,6 +17,7 @@ import type { SetUsernameJson } from '../models/SetUsernameJson';
import type { TeamIntegrationSchema } from '../models/TeamIntegrationSchema'; import type { TeamIntegrationSchema } from '../models/TeamIntegrationSchema';
import type { TeamInviteSchema } from '../models/TeamInviteSchema'; import type { TeamInviteSchema } from '../models/TeamInviteSchema';
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList'; import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
import type { TeamSchema } from '../models/TeamSchema';
import type { UpdateEventJson } from '../models/UpdateEventJson'; import type { UpdateEventJson } from '../models/UpdateEventJson';
import type { ViewAvailablePlayersResponse } from '../models/ViewAvailablePlayersResponse'; import type { ViewAvailablePlayersResponse } from '../models/ViewAvailablePlayersResponse';
import type { ViewScheduleResponse } from '../models/ViewScheduleResponse'; import type { ViewScheduleResponse } from '../models/ViewScheduleResponse';
@ -458,6 +459,30 @@ export class DefaultService {
}, },
}); });
} }
/**
* update_team <PATCH>
* @param teamId
* @param requestBody
* @returns TeamSchema OK
* @throws ApiError
*/
public updateTeam(
teamId: number,
requestBody?: CreateTeamJson,
): CancelablePromise<TeamSchema> {
return this.httpRequest.request({
method: 'PATCH',
url: '/api/team/id/{team_id}/',
path: {
'team_id': teamId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Unprocessable Content`,
},
});
}
/** /**
* consume_invite <POST> * consume_invite <POST>
* @param teamId * @param teamId

View File

@ -2,7 +2,7 @@ import { defineStore } from "pinia";
import { reactive, type Reactive } from "vue"; import { reactive, type Reactive } from "vue";
import { useClientStore } from "./client"; import { useClientStore } from "./client";
import { useAuthStore } from "./auth"; import { useAuthStore } from "./auth";
import { type TeamSchema, type RoleSchema, type ViewTeamMembersResponse } from "@/client"; import { type TeamSchema, type RoleSchema, type ViewTeamMembersResponse, type CreateTeamJson } from "@/client";
export type TeamMap = { [id: number]: TeamSchema }; export type TeamMap = { [id: number]: TeamSchema };
@ -55,6 +55,11 @@ export const useTeamsStore = defineStore("teams", () => {
}); });
} }
async function updateTeam(teamId: number, args: CreateTeamJson) {
return await client.default.updateTeam(teamId, args)
.then((response) => teams[teamId] = response);
}
async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) { async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) {
return await client.default.editMemberRoles(teamId.toString(), playerId, { roles }); return await client.default.editMemberRoles(teamId.toString(), playerId, { roles });
} }
@ -70,6 +75,7 @@ export const useTeamsStore = defineStore("teams", () => {
fetchTeam, fetchTeam,
fetchTeamMembers, fetchTeamMembers,
createTeam, createTeam,
updateTeam,
updateRoles, updateRoles,
leaveTeam, leaveTeam,
}; };

View File

@ -75,6 +75,13 @@ function createTeam() {
past the hour. past the hour.
</em> </em>
</div> </div>
<div class="form-group margin">
<div class="banner info">
<i class="bi bi-info-circle" />
Note: changing the timezone or minute offset after team creation
will remove all team members' availability data.
</div>
</div>
<div class="form-group margin"> <div class="form-group margin">
<div class="action-buttons"> <div class="action-buttons">
<button class="accent" @click="createTeam">Create team</button> <button class="accent" @click="createTeam">Create team</button>

View File

@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTeamSettings } from '@/composables/team-settings'; import { useTeamSettings } from "@/composables/team-settings";
import timezones from "@/assets/timezones.json"; import timezones from "@/assets/timezones.json";
import { onMounted, ref } from "vue";
import { useTeamsStore } from "@/stores/teams";
import { useTeamDetails } from "@/composables/team-details";
import { computed } from "@vue/reactivity";
const { const {
teamName, teamName,
@ -8,9 +12,61 @@ const {
minuteOffset, minuteOffset,
} = useTeamSettings(); } = useTeamSettings();
function updateTeamSettings() { const { teamId } = useTeamDetails();
function updateTeamSettings() {
teamsStore.updateTeam(teamId.value, {
teamName: teamName.value,
leagueTimezone: timezone.value,
minuteOffset: minuteOffset.value,
});
} }
function resetChanges() {
teamName.value = team.value.teamName;
timezone.value = team.value.tzTimezone;
minuteOffset.value = team.value.minuteOffset;
}
const hasChangedDetails = computed(() => {
if (!team) {
return false;
}
return (
teamName.value !== team.value.teamName ||
timezone.value !== team.value.tzTimezone ||
minuteOffset.value !== team.value.minuteOffset
);
});
const hasChangedTimeDetails = computed(() => {
if (!team) {
return false;
}
return (
timezone.value !== team.value.tzTimezone ||
minuteOffset.value !== team.value.minuteOffset
);
});
const isLoaded = ref(false);
const teamsStore = useTeamsStore();
const team = computed(() => teamsStore.teams[teamId.value]);
onMounted(() => {
isLoaded.value = true;
teamsStore.fetchTeam(teamId.value)
.then((response) => {
teamName.value = response.team.teamName;
timezone.value = response.team.tzTimezone;
minuteOffset.value = response.team.minuteOffset;
isLoaded.value = false;
});
})
</script> </script>
<template> <template>
@ -53,11 +109,25 @@ function updateTeamSettings() {
past the hour. past the hour.
</em> </em>
</div> </div>
<div class="form-group margin" v-if="hasChangedTimeDetails">
<div class="banner warning">
<i class="bi bi-exclamation-triangle-fill margin"></i>
Warning: changing the timezone or minute offset will remove all
current availability data.
</div>
</div>
<div class="form-group margin"> <div class="form-group margin">
<div class="action-buttons"> <div class="action-buttons">
<button class="transparent" v-if="hasChangedDetails" @click="resetChanges">
<i class="bi bi-arrow-counterclockwise"></i>
Undo changes
</button>
<button class="accent" @click="updateTeamSettings">Save</button> <button class="accent" @click="updateTeamSettings">Save</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped>
</style>

View File

@ -10,7 +10,7 @@ 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
from middleware import requires_authentication 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
from team_integration import api_team_integration from team_integration import api_team_integration
@ -86,6 +86,27 @@ def create_team(json: CreateTeamJson, player: Player, **kwargs):
response = ViewTeamResponse(team=TeamSchema.from_model(team)) response = ViewTeamResponse(team=TeamSchema.from_model(team))
return response.dict(by_alias=True), 200 return response.dict(by_alias=True), 200
@api_team.patch("/id/<int:team_id>/")
@spec.validate(
resp=Response(
HTTP_200=TeamSchema,
),
operation_id="update_team",
)
@requires_authentication
@requires_team_membership()
def update_team(player_team: PlayerTeam, team_id: int, json: CreateTeamJson, **kwargs):
assert_team_authority(player_team)
team = player_team.team
team.team_name = json.team_name
team.tz_timezone = json.league_timezone
team.minute_offset = json.minute_offset
db.session.commit()
return TeamSchema.from_model(team).dict(by_alias=True), 200
@api_team.delete("/id/<team_id>/") @api_team.delete("/id/<team_id>/")
@spec.validate( @spec.validate(
resp=Response( resp=Response(