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 integrationsmaster
parent
8a00c53479
commit
c67bf14980
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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<TeamIntegrationSchema>;
|
|
@ -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 <GET>
|
||||
* @param teamId
|
||||
* @returns TeamIntegrationSchemaList OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public getIntegrations(
|
||||
teamId: string,
|
||||
): CancelablePromise<TeamIntegrationSchemaList> {
|
||||
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 <DELETE>
|
||||
* @param teamId
|
||||
* @param integrationId
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deleteIntegration(
|
||||
teamId: string,
|
||||
integrationId: string,
|
||||
): CancelablePromise<void> {
|
||||
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 <PATCH>
|
||||
* @param teamId
|
||||
* @param integrationId
|
||||
* @param requestBody
|
||||
* @returns TeamIntegrationSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public updateIntegration(
|
||||
teamId: string,
|
||||
integrationId: string,
|
||||
requestBody?: AbstractTeamIntegrationSchema,
|
||||
): CancelablePromise<TeamIntegrationSchema> {
|
||||
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 <POST>
|
||||
* @param teamId
|
||||
* @param integrationType
|
||||
* @returns TeamIntegrationSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public createIntegration(
|
||||
teamId: string,
|
||||
integrationType: string,
|
||||
): CancelablePromise<TeamIntegrationSchema> {
|
||||
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 <GET>
|
||||
* @param teamId
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<script setup lang="ts">
|
||||
import type { TeamIntegrationSchema, TeamDiscordIntegrationSchema } from "@/client";
|
||||
import { useTeamDetails } from "@/composables/team-details";
|
||||
import { useTeamsStore } from "@/stores/teams";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
integration: TeamIntegrationSchema,
|
||||
}>();
|
||||
|
||||
const teamsStore = useTeamsStore();
|
||||
|
||||
const { teamId } = useTeamDetails();
|
||||
|
||||
/*
|
||||
const isDiscord = (x: TeamIntegrationSchema): x is TeamDiscordIntegrationSchema => x.integrationType === "team_discord_integrations";
|
||||
|
||||
const isDiscordIntegration = computed(() => {
|
||||
return isDiscord(props.integration);
|
||||
});
|
||||
*/
|
||||
|
||||
const discordIntegration = computed(() => props.integration as TeamDiscordIntegrationSchema);
|
||||
|
||||
function deleteIntegration() {
|
||||
teamsStore.deleteIntegration(teamId.value, props.integration.id);
|
||||
}
|
||||
|
||||
function saveIntegration() {
|
||||
teamsStore.updateIntegration(teamId.value, props.integration);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details class="accordion">
|
||||
<summary>
|
||||
<span class="title">
|
||||
<h2 v-if="discordIntegration">
|
||||
Discord Integration
|
||||
</h2>
|
||||
<span class="aside">(id: {{ props.integration.id }})</span>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="form-group margin">
|
||||
<h3>Webhook URL</h3>
|
||||
<input v-model="discordIntegration.webhookUrl" />
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="destructive-on-hover" @click="deleteIntegration">
|
||||
<i class="bi bi-trash margin" />
|
||||
Delete
|
||||
</button>
|
||||
<button @click="saveIntegration">Save</button>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
summary > .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
summary .aside {
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,20 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import IntegrationDetails from "@/components/IntegrationDetails.vue";
|
||||
import { useTeamDetails } from "@/composables/team-details";
|
||||
import { useTeamsStore } from "@/stores/teams";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
const teamsStore = useTeamsStore();
|
||||
const {
|
||||
teamId,
|
||||
} = useTeamDetails();
|
||||
|
||||
const integrations = computed(() => teamsStore.teamIntegrations[teamId.value]);
|
||||
|
||||
function createIntegration() {
|
||||
teamsStore.createIntegration(teamId.value, "discord");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
teamsStore.fetchTeam(teamId.value)
|
||||
.then(() => teamsStore.getIntegrations(teamId.value));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="team-integrations">
|
||||
<h2>Team Integrations</h2>
|
||||
<div v-if="true">
|
||||
<div v-if="integrations?.length == 0">
|
||||
This team currently does not have any integrations.
|
||||
</div>
|
||||
<div v-else>
|
||||
<details class="accordion">
|
||||
<summary>
|
||||
<h2>Discord Webhook</h2>
|
||||
</summary>
|
||||
<h3>Webhook URL</h3>
|
||||
<input hidden />
|
||||
</details>
|
||||
<IntegrationDetails
|
||||
v-for="integration in integrations"
|
||||
:integration="integration"
|
||||
/>
|
||||
</div>
|
||||
<button class="accent" @click="createIntegration">
|
||||
<i class="bi bi-database-fill-add margin" />
|
||||
Create Integration
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue