From c67bf14980bd4f304df4beb241c7250124a011c2 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Mon, 18 Nov 2024 20:25:07 -0800 Subject: [PATCH] Add integration management for teams - Add new models for team integrations - Create IntegrationDetails component for managing integrations - Update teams store with integration actions - Modify IntegrationsView to display and manage integrations --- availabili.tf/src/client/index.ts | 4 + .../models/AbstractTeamIntegrationSchema.ts | 8 ++ .../models/TeamDiscordIntegrationSchema.ts | 11 +++ .../client/models/TeamIntegrationSchema.ts | 10 ++ .../models/TeamIntegrationSchemaList.ts | 6 ++ .../src/client/services/DefaultService.ts | 97 +++++++++++++++++++ .../src/components/AvailabilityGrid.vue | 10 +- .../src/components/IntegrationDetails.vue | 76 +++++++++++++++ availabili.tf/src/stores/teams.ts | 50 +++++++++- .../views/TeamSettings/IntegrationsView.vue | 37 +++++-- backend-flask/models/team_integration.py | 9 ++ backend-flask/team.py | 20 ++-- 12 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts create mode 100644 availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts create mode 100644 availabili.tf/src/client/models/TeamIntegrationSchema.ts create mode 100644 availabili.tf/src/client/models/TeamIntegrationSchemaList.ts create mode 100644 availabili.tf/src/components/IntegrationDetails.vue diff --git a/availabili.tf/src/client/index.ts b/availabili.tf/src/client/index.ts index 14b2bea..6b3f507 100644 --- a/availabili.tf/src/client/index.ts +++ b/availabili.tf/src/client/index.ts @@ -10,6 +10,7 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; +export type { AbstractTeamIntegrationSchema } from './models/AbstractTeamIntegrationSchema'; export type { AddPlayerJson } from './models/AddPlayerJson'; export type { AvailabilitySchema } from './models/AvailabilitySchema'; export type { CreateTeamJson } from './models/CreateTeamJson'; @@ -19,6 +20,9 @@ export type { PlayerTeamAvailabilityRoleSchema } from './models/PlayerTeamAvaila export type { PutScheduleForm } from './models/PutScheduleForm'; export type { RoleSchema } from './models/RoleSchema'; export type { SetUsernameJson } from './models/SetUsernameJson'; +export type { TeamDiscordIntegrationSchema } from './models/TeamDiscordIntegrationSchema'; +export type { TeamIntegrationSchema } from './models/TeamIntegrationSchema'; +export type { TeamIntegrationSchemaList } from './models/TeamIntegrationSchemaList'; export type { TeamInviteSchema } from './models/TeamInviteSchema'; export type { TeamInviteSchemaList } from './models/TeamInviteSchemaList'; export { TeamRole } from './models/TeamRole'; diff --git a/availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts b/availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts new file mode 100644 index 0000000..ef29f3f --- /dev/null +++ b/availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TeamDiscordIntegrationSchema } from './TeamDiscordIntegrationSchema'; +import type { TeamIntegrationSchema } from './TeamIntegrationSchema'; +export type AbstractTeamIntegrationSchema = (TeamDiscordIntegrationSchema | TeamIntegrationSchema); + diff --git a/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts b/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts new file mode 100644 index 0000000..2f44a58 --- /dev/null +++ b/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TeamDiscordIntegrationSchema = { + id: number; + integrationType: string; + teamId: number; + webhookUrl: string; +}; + diff --git a/availabili.tf/src/client/models/TeamIntegrationSchema.ts b/availabili.tf/src/client/models/TeamIntegrationSchema.ts new file mode 100644 index 0000000..9bab4dc --- /dev/null +++ b/availabili.tf/src/client/models/TeamIntegrationSchema.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TeamIntegrationSchema = { + id: number; + integrationType: string; + teamId: number; +}; + diff --git a/availabili.tf/src/client/models/TeamIntegrationSchemaList.ts b/availabili.tf/src/client/models/TeamIntegrationSchemaList.ts new file mode 100644 index 0000000..f48ebff --- /dev/null +++ b/availabili.tf/src/client/models/TeamIntegrationSchemaList.ts @@ -0,0 +1,6 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TeamIntegrationSchema } from './TeamIntegrationSchema'; +export type TeamIntegrationSchemaList = Array; diff --git a/availabili.tf/src/client/services/DefaultService.ts b/availabili.tf/src/client/services/DefaultService.ts index 4b07ef2..3ef0c22 100644 --- a/availabili.tf/src/client/services/DefaultService.ts +++ b/availabili.tf/src/client/services/DefaultService.ts @@ -2,12 +2,15 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { AbstractTeamIntegrationSchema } from '../models/AbstractTeamIntegrationSchema'; import type { AddPlayerJson } from '../models/AddPlayerJson'; import type { CreateTeamJson } from '../models/CreateTeamJson'; import type { EditMemberRolesJson } from '../models/EditMemberRolesJson'; import type { PlayerSchema } from '../models/PlayerSchema'; import type { PutScheduleForm } from '../models/PutScheduleForm'; import type { SetUsernameJson } from '../models/SetUsernameJson'; +import type { TeamIntegrationSchema } from '../models/TeamIntegrationSchema'; +import type { TeamIntegrationSchemaList } from '../models/TeamIntegrationSchemaList'; import type { TeamInviteSchema } from '../models/TeamInviteSchema'; import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList'; import type { ViewAvailablePlayersResponse } from '../models/ViewAvailablePlayersResponse'; @@ -314,6 +317,100 @@ export class DefaultService { }, }); } + /** + * get_integrations + * @param teamId + * @returns TeamIntegrationSchemaList OK + * @throws ApiError + */ + public getIntegrations( + teamId: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/api/team/id/{team_id}/integrations', + path: { + 'team_id': teamId, + }, + errors: { + 404: `Not Found`, + 422: `Unprocessable Entity`, + }, + }); + } + /** + * delete_integration + * @param teamId + * @param integrationId + * @returns void + * @throws ApiError + */ + public deleteIntegration( + teamId: string, + integrationId: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'DELETE', + url: '/api/team/id/{team_id}/integrations/{integration_id}', + path: { + 'team_id': teamId, + 'integration_id': integrationId, + }, + errors: { + 422: `Unprocessable Entity`, + }, + }); + } + /** + * update_integration + * @param teamId + * @param integrationId + * @param requestBody + * @returns TeamIntegrationSchema OK + * @throws ApiError + */ + public updateIntegration( + teamId: string, + integrationId: string, + requestBody?: AbstractTeamIntegrationSchema, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'PATCH', + url: '/api/team/id/{team_id}/integrations/{integration_id}', + path: { + 'team_id': teamId, + 'integration_id': integrationId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Unprocessable Entity`, + }, + }); + } + /** + * create_integration + * @param teamId + * @param integrationType + * @returns TeamIntegrationSchema OK + * @throws ApiError + */ + public createIntegration( + teamId: string, + integrationType: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/api/team/id/{team_id}/integrations/{integration_type}', + path: { + 'team_id': teamId, + 'integration_type': integrationType, + }, + errors: { + 422: `Unprocessable Entity`, + }, + }); + } /** * get_invites * @param teamId diff --git a/availabili.tf/src/components/AvailabilityGrid.vue b/availabili.tf/src/components/AvailabilityGrid.vue index f3331ff..22b2183 100644 --- a/availabili.tf/src/components/AvailabilityGrid.vue +++ b/availabili.tf/src/components/AvailabilityGrid.vue @@ -35,22 +35,22 @@ const isShiftDown = ref(false); const lowerBoundX = computed(() => { return isShiftDown.value ? 0 : - Math.min(selectionStart.x, selectionEnd.x) + Math.min(selectionStart.x ?? NaN, selectionEnd.x ?? NaN) }); const upperBoundX = computed(() => { return isShiftDown.value ? 6 : - Math.max(selectionStart.x, selectionEnd.x) + Math.max(selectionStart.x ?? NaN, selectionEnd.x ?? NaN) }); const lowerBoundY = computed(() => { return isCtrlDown.value ? props.firstHour : - Math.min(selectionStart.y, selectionEnd.y) + Math.min(selectionStart.y ?? NaN, selectionEnd.y ?? NaN) }); const upperBoundY = computed(() => { return isCtrlDown.value ? props.lastHour : - Math.max(selectionStart.y, selectionEnd.y) + Math.max(selectionStart.y ?? NaN, selectionEnd.y ?? NaN) }); -function selectionInside(dayIndex, hour) { +function selectionInside(dayIndex: number, hour: number) { if (selectionStart.x != undefined) { return (dayIndex >= lowerBoundX.value && dayIndex <= upperBoundX.value) && (hour >= lowerBoundY.value && hour <= upperBoundY.value); diff --git a/availabili.tf/src/components/IntegrationDetails.vue b/availabili.tf/src/components/IntegrationDetails.vue new file mode 100644 index 0000000..86f9694 --- /dev/null +++ b/availabili.tf/src/components/IntegrationDetails.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/availabili.tf/src/stores/teams.ts b/availabili.tf/src/stores/teams.ts index 557c1ea..68ac724 100644 --- a/availabili.tf/src/stores/teams.ts +++ b/availabili.tf/src/stores/teams.ts @@ -1,5 +1,5 @@ import Cacheable from "@/cacheable"; -import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSchema, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse } from "@/client"; +import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSchema, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse, type TeamIntegrationSchema, type AbstractTeamIntegrationSchema } from "@/client"; import { defineStore } from "pinia"; import { computed, reactive, ref, type Reactive, type Ref } from "vue"; import { useClientStore } from "./client"; @@ -17,6 +17,7 @@ export const useTeamsStore = defineStore("teams", () => { const teams: Reactive<{ [id: number]: TeamSchema }> = reactive({ }); const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ }); const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ }); + const teamIntegrations = reactive<{ [id: number]: TeamIntegrationSchema[] }>({ }); async function fetchTeams() { return clientStore.call( @@ -118,6 +119,47 @@ export const useTeamsStore = defineStore("teams", () => { }); } + async function getIntegrations(teamId: number) { + return client.default.getIntegrations(teamId.toString()) + .then((response) => { + teamIntegrations[teamId] = response; + return response; + }); + } + + async function createIntegration(teamId: number, integrationType: string) { + return client.default + .createIntegration(teamId.toString(), integrationType) + .then((response) => { + teamIntegrations[teamId].push(response); + return response; + }); + } + + async function deleteIntegration(teamId: number, integrationId: number) { + return client.default + .deleteIntegration(teamId.toString(), integrationId.toString()) + .then((response) => { + teamIntegrations[teamId] = teamIntegrations[teamId] + .filter((integration) => integration.id != integrationId); + return response; + }); + } + + async function updateIntegration( + teamId: number, + integration: AbstractTeamIntegrationSchema, + ) { + return client.default + .updateIntegration(teamId.toString(), integration.id.toString(), integration) + .then((response) => { + const index = teamIntegrations[teamId] + .findIndex((x) => x.id == integration.id); + teamIntegrations[teamId][index] = response; + return response; + }); + } + async function leaveTeam(teamId: number) { return client.default .removePlayerFromTeam(teamId.toString(), authStore.steamId); @@ -137,5 +179,11 @@ export const useTeamsStore = defineStore("teams", () => { consumeInvite, revokeInvite, leaveTeam, + // TODO: move to separate store + teamIntegrations, + getIntegrations, + createIntegration, + deleteIntegration, + updateIntegration, }; }); diff --git a/availabili.tf/src/views/TeamSettings/IntegrationsView.vue b/availabili.tf/src/views/TeamSettings/IntegrationsView.vue index 844332e..6c1477e 100644 --- a/availabili.tf/src/views/TeamSettings/IntegrationsView.vue +++ b/availabili.tf/src/views/TeamSettings/IntegrationsView.vue @@ -1,20 +1,41 @@ diff --git a/backend-flask/models/team_integration.py b/backend-flask/models/team_integration.py index 32c50a7..9ab6764 100644 --- a/backend-flask/models/team_integration.py +++ b/backend-flask/models/team_integration.py @@ -1,4 +1,6 @@ #from typing import cast, override +from typing import TypeAlias, Union +from pydantic_core.core_schema import UnionSchema from sqlalchemy.orm import mapped_column, relationship from sqlalchemy.orm.attributes import Mapped from sqlalchemy.orm.properties import ForeignKey @@ -56,4 +58,11 @@ class TeamDiscordIntegrationSchema(TeamIntegrationSchema): webhook_url=model.webhook_url ) +class ExampleIntegrationSchema(TeamIntegrationSchema): + test: str + +class AbstractTeamIntegrationSchema(spec.BaseModel): + __root__: TeamDiscordIntegrationSchema | TeamIntegrationSchema + + from models.team import Team diff --git a/backend-flask/team.py b/backend-flask/team.py index 1cb05f9..033d155 100644 --- a/backend-flask/team.py +++ b/backend-flask/team.py @@ -2,7 +2,7 @@ from datetime import UTC, datetime, timedelta, timezone from random import randint, random import sys import time -from typing import List +from typing import List, cast from flask import Blueprint, abort, jsonify, make_response, request from pydantic.v1 import validator from spectree import Response @@ -14,7 +14,7 @@ from models.player_team_availability import PlayerTeamAvailability from models.player_team_role import PlayerTeamRole, RoleSchema from models.team import Team, TeamSchema from models.team_invite import TeamInvite, TeamInviteSchema -from models.team_integration import TeamDiscordIntegration, TeamDiscordIntegrationSchema, TeamIntegration, TeamIntegrationSchema +from models.team_integration import AbstractTeamIntegrationSchema, TeamDiscordIntegration, TeamDiscordIntegrationSchema, TeamIntegration, TeamIntegrationSchema from middleware import assert_team_authority, requires_authentication, requires_team_membership import models from spec import spec, BaseModel @@ -630,7 +630,9 @@ def create_integration(player_team: PlayerTeam, integration_type: str, **_): ), operation_id="delete_integration" ) -def delete_integration(player_team: PlayerTeam, integration_id: int): +@requires_authentication +@requires_team_membership +def delete_integration(player_team: PlayerTeam, integration_id: int, **_): assert_team_authority(player_team) integration = db.session.query( @@ -656,10 +658,12 @@ def delete_integration(player_team: PlayerTeam, integration_id: int): ), operation_id="update_integration" ) +@requires_authentication +@requires_team_membership def update_integration( player_team: PlayerTeam, integration_id: int, - json: TeamIntegrationSchema, + json: AbstractTeamIntegrationSchema, **_ ): assert_team_authority(player_team) @@ -676,8 +680,12 @@ def update_integration( abort(404) if isinstance(integration, TeamDiscordIntegration): - if isinstance(json, TeamDiscordIntegrationSchema): - integration.webhook_url = json.webhook_url + print(json.dict(), file=sys.stderr) + if json.__root__.integration_type == "team_discord_integrations": + discord_integration = cast(TeamDiscordIntegration, json.__root__) + integration.webhook_url = discord_integration.webhook_url + #if isinstance(json, TeamDiscordIntegrationSchema): + # integration.webhook_url = json.webhook_url else: abort(400) else: