diff --git a/packages/server/graphql/mutations/endRetrospective.ts b/packages/server/graphql/mutations/endRetrospective.ts index 43b05fdd166..82136fa8f9d 100644 --- a/packages/server/graphql/mutations/endRetrospective.ts +++ b/packages/server/graphql/mutations/endRetrospective.ts @@ -1,116 +1,11 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {DISCUSS, PARABOL_AI_USER_ID} from 'parabol-client/utils/constants' -import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' -import findStageById from 'parabol-client/utils/meetings/findStageById' -import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' import getRethink from '../../database/rethinkDriver' -import {RDatum} from '../../database/stricterR' import MeetingRetrospective from '../../database/types/MeetingRetrospective' -import TimelineEventRetroComplete from '../../database/types/TimelineEventRetroComplete' -import getKysely from '../../postgres/getKysely' -import removeSuggestedAction from '../../safeMutations/removeSuggestedAction' -import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' -import getPhase from '../../utils/getPhase' -import publish from '../../utils/publish' -import RecallAIServerManager from '../../utils/RecallAIServerManager' -import sendToSentry from '../../utils/sendToSentry' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' import EndRetrospectivePayload from '../types/EndRetrospectivePayload' -import updateTeamInsights from './helpers/updateTeamInsights' -import sendNewMeetingSummary from './helpers/endMeeting/sendNewMeetingSummary' -import generateWholeMeetingSentimentScore from './helpers/generateWholeMeetingSentimentScore' -import generateWholeMeetingSummary from './helpers/generateWholeMeetingSummary' -import handleCompletedStage from './helpers/handleCompletedStage' -import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier' -import removeEmptyTasks from './helpers/removeEmptyTasks' -import updateQualAIMeetingsCount from './helpers/updateQualAIMeetingsCount' -import gatherInsights from './helpers/gatherInsights' - -const getTranscription = async (recallBotId?: string | null) => { - if (!recallBotId) return - const manager = new RecallAIServerManager() - return await manager.getBotTranscript(recallBotId) -} - -const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: GQLContext) => { - const {dataLoader, authToken} = context - const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting - const r = await getRethink() - const [reflectionGroups, reflections, sentimentScore] = await Promise.all([ - dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId), - dataLoader.get('retroReflectionsByMeetingId').load(meetingId), - generateWholeMeetingSentimentScore(meetingId, facilitatorUserId, dataLoader) - ]) - const discussPhase = getPhase(phases, 'discuss') - const {stages} = discussPhase - const discussionIds = stages.map((stage) => stage.discussionId) - - const reflectionGroupIds = reflectionGroups.map(({id}) => id) - const hasTopicSummary = reflectionGroups.some((group) => group.summary) - if (hasTopicSummary) { - const groupsWithMissingTopicSummaries = reflectionGroups.filter((group) => { - const reflectionsInGroup = reflections.filter( - (reflection) => reflection.reflectionGroupId === group.id - ) - return reflectionsInGroup.length > 1 && !group.summary - }) - if (groupsWithMissingTopicSummaries.length > 0) { - const missingGroupIds = groupsWithMissingTopicSummaries.map(({id}) => id).join(', ') - const error = new Error('Missing AI topic summary') - const viewerId = getUserId(authToken) - sendToSentry(error, { - userId: viewerId, - tags: {missingGroupIds, meetingId} - }) - } - } - const [summary, transcription] = await Promise.all([ - generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId, dataLoader), - getTranscription(recallBotId) - ]) - - await r - .table('NewMeeting') - .get(meetingId) - .update( - { - commentCount: r - .table('Comment') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .filter((row: RDatum) => - row('isActive').eq(true).and(row('createdBy').ne(PARABOL_AI_USER_ID)) - ) - .count() - .default(0) as unknown as number, - taskCount: r - .table('Task') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .count() - .default(0) as unknown as number, - topicCount: reflectionGroupIds.length, - reflectionCount: reflections.length, - sentimentScore, - summary, - transcription - }, - {nonAtomic: true} - ) - .run() - - dataLoader.get('newMeetings').clear(meetingId) - // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount - sendNewMeetingSummary(meeting, context).catch(console.log) - updateQualAIMeetingsCount(meetingId, teamId, dataLoader) - // wait for meeting stats to be generated before sending Slack notification - IntegrationNotifier.endMeeting(dataLoader, meetingId, teamId) - const data = {meetingId} - const operationId = dataLoader.share() - const subOptions = {operationId} - publish(SubscriptionChannel.MEETING, meetingId, 'EndRetrospectiveSuccess', data, subOptions) -} +import safeEndRetrospective from './helpers/safeEndRetrospective' export default { type: new GraphQLNonNull(EndRetrospectivePayload), @@ -122,11 +17,8 @@ export default { } }, async resolve(_source: unknown, {meetingId}: {meetingId: string}, context: GQLContext) { - const {authToken, socketId: mutatorId, dataLoader} = context + const {authToken} = context const r = await getRethink() - const pg = getKysely() - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} const now = new Date() const viewerId = getUserId(authToken) @@ -137,7 +29,7 @@ export default { .default(null) .run()) as MeetingRetrospective | null if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) - const {endedAt, facilitatorStageId, phases, teamId} = meeting + const {endedAt, teamId} = meeting // VALIDATION if (!isTeamMember(authToken, teamId) && authToken.rol !== 'su') { @@ -146,106 +38,6 @@ export default { if (endedAt) return standardError(new Error('Meeting already ended'), {userId: viewerId}) // RESOLUTION - const currentStageRes = findStageById(phases, facilitatorStageId) - if (currentStageRes) { - const {stage} = currentStageRes - await handleCompletedStage(stage, meeting, dataLoader) - stage.isComplete = true - stage.endAt = now - } - const phase = getMeetingPhase(phases) - - const insights = await gatherInsights(meeting, dataLoader) - const completedRetrospective = (await r - .table('NewMeeting') - .get(meetingId) - .update( - { - endedAt: now, - phases, - ...insights - }, - {returnChanges: true} - )('changes')(0)('new_val') - .default(null) - .run()) as unknown as MeetingRetrospective - - if (!completedRetrospective) { - return standardError(new Error('Completed retrospective meeting does not exist'), { - userId: viewerId - }) - } - - // remove any empty tasks - const {templateId} = completedRetrospective - const [meetingMembers, team, teamMembers, removedTaskIds, template] = await Promise.all([ - dataLoader.get('meetingMembersByMeetingId').load(meetingId), - dataLoader.get('teams').loadNonNull(teamId), - dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId), - dataLoader.get('meetingTemplates').loadNonNull(templateId), - pg - .deleteFrom('RetroReflectionGroup') - .where('meetingId', '=', meetingId) - .where('isActive', '=', false) - .execute(), - r - .table('RetroReflectionGroup') - .getAll(meetingId, {index: 'meetingId'}) - .filter({isActive: false}) - .delete() - .run(), - updateTeamInsights(teamId, dataLoader) - ]) - // wait for removeEmptyTasks before summarizeRetroMeeting - // don't await for the OpenAI response or it'll hang for a while when ending the retro - summarizeRetroMeeting(completedRetrospective, context) - analytics.retrospectiveEnd(completedRetrospective, meetingMembers, template, dataLoader) - checkTeamsLimit(team.orgId, dataLoader) - const events = teamMembers.map( - (teamMember) => - new TimelineEventRetroComplete({ - userId: teamMember.userId, - teamId, - orgId: team.orgId, - meetingId - }) - ) - const timelineEventId = events[0]!.id - await r.table('TimelineEvent').insert(events).run() - - if (team.isOnboardTeam) { - const teamLeadUserId = await r - .table('TeamMember') - .getAll(teamId, {index: 'teamId'}) - .filter({isLead: true}) - .nth(0)('userId') - .run() - - const removedSuggestedActionId = await removeSuggestedAction( - teamLeadUserId, - 'tryRetroMeeting' - ) - if (removedSuggestedActionId) { - publish( - SubscriptionChannel.NOTIFICATION, - teamLeadUserId, - 'EndRetrospectiveSuccess', - {removedSuggestedActionId}, - subOptions - ) - } - } - - const data = { - meetingId, - teamId, - isKill: !!(phase && phase.phaseType !== DISCUSS), - removedTaskIds, - timelineEventId - } - publish(SubscriptionChannel.TEAM, teamId, 'EndRetrospectiveSuccess', data, subOptions) - - return data + return safeEndRetrospective({meeting, now, context}) } } diff --git a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts new file mode 100644 index 00000000000..1010b8c5551 --- /dev/null +++ b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts @@ -0,0 +1,58 @@ +import getRethink from '../../../database/rethinkDriver' +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import generateUID from '../../../generateUID' +import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {DataLoaderWorker} from '../../graphql' +import createNewMeetingPhases from './createNewMeetingPhases' + +export const DEFAULT_PROMPT = 'What are you working on today? Stuck on anything?' + +const safeCreateRetrospective = async ( + meetingSettings: { + teamId: string + facilitatorUserId: string + totalVotes: number + maxVotesPerGroup: number + disableAnonymity: boolean + templateId: string + videoMeetingURL?: string + }, + dataLoader: DataLoaderWorker +) => { + const r = await getRethink() + const {teamId, facilitatorUserId} = meetingSettings + const meetingType: MeetingTypeEnum = 'retrospective' + const [meetingCount, team] = await Promise.all([ + r + .table('NewMeeting') + .getAll(teamId, {index: 'teamId'}) + .filter({meetingType}) + .count() + .default(0) + .run(), + dataLoader.get('teams').loadNonNull(teamId) + ]) + + const organization = await r.table('Organization').get(team.orgId).run() + const {showConversionModal} = organization + + const meetingId = generateUID() + const phases = await createNewMeetingPhases( + facilitatorUserId, + teamId, + meetingId, + meetingCount, + meetingType, + dataLoader + ) + + return new MeetingRetrospective({ + id: meetingId, + meetingCount, + phases, + showConversionModal, + ...meetingSettings + }) +} + +export default safeCreateRetrospective diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts new file mode 100644 index 00000000000..4d9f4f12c7d --- /dev/null +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -0,0 +1,231 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import {DISCUSS, PARABOL_AI_USER_ID} from 'parabol-client/utils/constants' +import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' +import findStageById from 'parabol-client/utils/meetings/findStageById' +import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' +import getRethink from '../../../database/rethinkDriver' +import {RDatum} from '../../../database/stricterR' +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import TimelineEventRetroComplete from '../../../database/types/TimelineEventRetroComplete' +import getKysely from '../../../postgres/getKysely' +import removeSuggestedAction from '../../../safeMutations/removeSuggestedAction' +import {analytics} from '../../../utils/analytics/analytics' +import {getUserId} from '../../../utils/authorization' +import getPhase from '../../../utils/getPhase' +import publish from '../../../utils/publish' +import RecallAIServerManager from '../../../utils/RecallAIServerManager' +import sendToSentry from '../../../utils/sendToSentry' +import standardError from '../../../utils/standardError' +import {InternalContext} from '../../graphql' +import updateTeamInsights from './updateTeamInsights' +import sendNewMeetingSummary from './endMeeting/sendNewMeetingSummary' +import generateWholeMeetingSentimentScore from './generateWholeMeetingSentimentScore' +import generateWholeMeetingSummary from './generateWholeMeetingSummary' +import handleCompletedStage from './handleCompletedStage' +import {IntegrationNotifier} from './notifications/IntegrationNotifier' +import removeEmptyTasks from './removeEmptyTasks' +import updateQualAIMeetingsCount from './updateQualAIMeetingsCount' +import gatherInsights from './gatherInsights' + +const getTranscription = async (recallBotId?: string | null) => { + if (!recallBotId) return + const manager = new RecallAIServerManager() + return await manager.getBotTranscript(recallBotId) +} + +const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: InternalContext) => { + const {dataLoader, authToken} = context + const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting + const r = await getRethink() + const [reflectionGroups, reflections, sentimentScore] = await Promise.all([ + dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId), + dataLoader.get('retroReflectionsByMeetingId').load(meetingId), + generateWholeMeetingSentimentScore(meetingId, facilitatorUserId, dataLoader) + ]) + const discussPhase = getPhase(phases, 'discuss') + const {stages} = discussPhase + const discussionIds = stages.map((stage) => stage.discussionId) + + const reflectionGroupIds = reflectionGroups.map(({id}) => id) + const hasTopicSummary = reflectionGroups.some((group) => group.summary) + if (hasTopicSummary) { + const groupsWithMissingTopicSummaries = reflectionGroups.filter((group) => { + const reflectionsInGroup = reflections.filter( + (reflection) => reflection.reflectionGroupId === group.id + ) + return reflectionsInGroup.length > 1 && !group.summary + }) + if (groupsWithMissingTopicSummaries.length > 0) { + const missingGroupIds = groupsWithMissingTopicSummaries.map(({id}) => id).join(', ') + const error = new Error('Missing AI topic summary') + const viewerId = getUserId(authToken) + sendToSentry(error, { + userId: viewerId, + tags: {missingGroupIds, meetingId} + }) + } + } + const [summary, transcription] = await Promise.all([ + generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId, dataLoader), + getTranscription(recallBotId) + ]) + + await r + .table('NewMeeting') + .get(meetingId) + .update( + { + commentCount: r + .table('Comment') + .getAll(r.args(discussionIds), {index: 'discussionId'}) + .filter((row: RDatum) => + row('isActive').eq(true).and(row('createdBy').ne(PARABOL_AI_USER_ID)) + ) + .count() + .default(0) as unknown as number, + taskCount: r + .table('Task') + .getAll(r.args(discussionIds), {index: 'discussionId'}) + .count() + .default(0) as unknown as number, + topicCount: reflectionGroupIds.length, + reflectionCount: reflections.length, + sentimentScore, + summary, + transcription + }, + {nonAtomic: true} + ) + .run() + + dataLoader.get('newMeetings').clear(meetingId) + // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount + sendNewMeetingSummary(meeting, context).catch(console.log) + updateQualAIMeetingsCount(meetingId, teamId, dataLoader) + // wait for meeting stats to be generated before sending Slack notification + IntegrationNotifier.endMeeting(dataLoader, meetingId, teamId) + const data = {meetingId} + const operationId = dataLoader.share() + const subOptions = {operationId} + publish(SubscriptionChannel.MEETING, meetingId, 'EndRetrospectiveSuccess', data, subOptions) +} + +const safeEndRetrospective = async ({ + meeting, + context, + now +}: { + meeting: MeetingRetrospective + context: InternalContext + now: Date +}) => { + const {authToken, socketId: mutatorId, dataLoader} = context + const {id: meetingId, phases, facilitatorStageId, teamId} = meeting + const r = await getRethink() + const pg = getKysely() + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const viewerId = getUserId(authToken) + + // RESOLUTION + const currentStageRes = findStageById(phases, facilitatorStageId) + if (currentStageRes) { + const {stage} = currentStageRes + await handleCompletedStage(stage, meeting, dataLoader) + stage.isComplete = true + stage.endAt = now + } + const phase = getMeetingPhase(phases) + + const insights = await gatherInsights(meeting, dataLoader) + const completedRetrospective = (await r + .table('NewMeeting') + .get(meetingId) + .update( + { + endedAt: now, + phases, + ...insights + }, + {returnChanges: true} + )('changes')(0)('new_val') + .default(null) + .run()) as unknown as MeetingRetrospective + + if (!completedRetrospective) { + return standardError(new Error('Completed retrospective meeting does not exist'), { + userId: viewerId + }) + } + + // remove any empty tasks + const {templateId} = completedRetrospective + const [meetingMembers, team, teamMembers, removedTaskIds, template] = await Promise.all([ + dataLoader.get('meetingMembersByMeetingId').load(meetingId), + dataLoader.get('teams').loadNonNull(teamId), + dataLoader.get('teamMembersByTeamId').load(teamId), + removeEmptyTasks(meetingId), + dataLoader.get('meetingTemplates').loadNonNull(templateId), + pg + .deleteFrom('RetroReflectionGroup') + .where('meetingId', '=', meetingId) + .where('isActive', '=', false) + .execute(), + r + .table('RetroReflectionGroup') + .getAll(meetingId, {index: 'meetingId'}) + .filter({isActive: false}) + .delete() + .run(), + updateTeamInsights(teamId, dataLoader) + ]) + // wait for removeEmptyTasks before summarizeRetroMeeting + // don't await for the OpenAI response or it'll hang for a while when ending the retro + summarizeRetroMeeting(completedRetrospective, context) + analytics.retrospectiveEnd(completedRetrospective, meetingMembers, template, dataLoader) + checkTeamsLimit(team.orgId, dataLoader) + const events = teamMembers.map( + (teamMember) => + new TimelineEventRetroComplete({ + userId: teamMember.userId, + teamId, + orgId: team.orgId, + meetingId + }) + ) + const timelineEventId = events[0]!.id + await r.table('TimelineEvent').insert(events).run() + + if (team.isOnboardTeam) { + const teamLeadUserId = await r + .table('TeamMember') + .getAll(teamId, {index: 'teamId'}) + .filter({isLead: true}) + .nth(0)('userId') + .run() + + const removedSuggestedActionId = await removeSuggestedAction(teamLeadUserId, 'tryRetroMeeting') + if (removedSuggestedActionId) { + publish( + SubscriptionChannel.NOTIFICATION, + teamLeadUserId, + 'EndRetrospectiveSuccess', + {removedSuggestedActionId}, + subOptions + ) + } + } + + const data = { + meetingId, + teamId, + isKill: !!(phase && phase.phaseType !== DISCUSS), + removedTaskIds, + timelineEventId + } + publish(SubscriptionChannel.TEAM, teamId, 'EndRetrospectiveSuccess', data, subOptions) + + return data +} + +export default safeEndRetrospective diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index edc5426da1a..93c41f1375c 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -1,9 +1,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import RetroMeetingMember from '../../../database/types/RetroMeetingMember' -import generateUID from '../../../generateUID' import updateMeetingTemplateLastUsedAt from '../../../postgres/queries/updateMeetingTemplateLastUsedAt' import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' @@ -13,9 +11,9 @@ import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' import createGcalEvent from '../../mutations/helpers/createGcalEvent' -import createNewMeetingPhases from '../../mutations/helpers/createNewMeetingPhases' import isStartMeetingLocked from '../../mutations/helpers/isStartMeetingLocked' import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' +import safeCreateRetrospective from '../../mutations/helpers/safeCreateRetrospective' const startRetrospective: MutationResolvers['startRetrospective'] = async ( _source, @@ -34,37 +32,14 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( const unpaidError = await isStartMeetingLocked(teamId, dataLoader) if (unpaidError) return standardError(new Error(unpaidError), {userId: viewerId}) - const meetingType: MeetingTypeEnum = 'retrospective' - // RESOLUTION - const meetingCount = await r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType}) - .count() - .default(0) - .run() - - const meetingId = generateUID() - const phases = await createNewMeetingPhases( - viewerId, - teamId, - meetingId, - meetingCount, - meetingType, - dataLoader - ) - const [team, viewer] = await Promise.all([ - dataLoader.get('teams').loadNonNull(teamId), - dataLoader.get('users').loadNonNull(viewerId) - ]) - - const organization = await r.table('Organization').get(team.orgId).run() - const {showConversionModal} = organization + const viewer = await dataLoader.get('users').loadNonNull(viewerId) + const meetingType: MeetingTypeEnum = 'retrospective' const meetingSettings = (await dataLoader .get('meetingSettingsByType') .load({teamId, meetingType})) as MeetingSettingsRetrospective + const { id: meetingSettingsId, totalVotes, @@ -73,19 +48,20 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( disableAnonymity, videoMeetingURL } = meetingSettings - const meeting = new MeetingRetrospective({ - id: meetingId, - teamId, - meetingCount, - phases, - showConversionModal, - facilitatorUserId: viewerId, - totalVotes, - maxVotesPerGroup, - disableAnonymity, - templateId: selectedTemplateId, - videoMeetingURL: videoMeetingURL ?? undefined - }) + + const meeting = await safeCreateRetrospective( + { + teamId, + facilitatorUserId: viewerId, + totalVotes, + maxVotesPerGroup, + disableAnonymity, + templateId: selectedTemplateId, + videoMeetingURL: videoMeetingURL ?? undefined + }, + dataLoader + ) + const meetingId = meeting.id const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) await Promise.all([ diff --git a/packages/server/graphql/public/typeDefs/_legacy.graphql b/packages/server/graphql/public/typeDefs/_legacy.graphql index b236b7a7af8..07fd1e5d07a 100644 --- a/packages/server/graphql/public/typeDefs/_legacy.graphql +++ b/packages/server/graphql/public/typeDefs/_legacy.graphql @@ -5956,20 +5956,6 @@ type Mutation { isSpotlight: Boolean ): StartDraggingReflectionPayload - """ - Start a new meeting - """ - startRetrospective( - """ - The team starting the meeting - """ - teamId: ID! - """ - The gcal input if creating a gcal event - """ - gcalInput: CreateGcalEventInput - ): StartRetrospectivePayload! - """ Start a new sprint poker meeting """ @@ -7861,18 +7847,6 @@ type StartDraggingReflectionPayload { teamId: ID } -""" -Return object for StartRetrospectivePayload -""" -union StartRetrospectivePayload = ErrorPayload | StartRetrospectiveSuccess - -type StartRetrospectiveSuccess { - meeting: RetrospectiveMeeting! - meetingId: ID! - team: Team! - hasGcalError: Boolean! -} - """ Return object for StartSprintPokerPayload """ diff --git a/packages/server/graphql/public/typeDefs/startRetrospective.graphql b/packages/server/graphql/public/typeDefs/startRetrospective.graphql new file mode 100644 index 00000000000..0bcb2bc0789 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/startRetrospective.graphql @@ -0,0 +1,54 @@ +extend type Mutation { + """ + Start a new meeting + """ + startRetrospective( + """ + The team starting the meeting + """ + teamId: ID! + """ + The gcal input if creating a gcal event + """ + gcalInput: CreateGcalEventInput + ): StartRetrospectivePayload! +} + +""" +Return object for StartRetrospectivePayload +""" +union StartRetrospectivePayload = ErrorPayload | StartRetrospectiveSuccess + +type StartRetrospectiveSuccess { + meeting: RetrospectiveMeeting! + meetingId: ID! + team: Team! + hasGcalError: Boolean! +} + +input CreateGcalEventInput { + """ + The title of the event + """ + title: String! + """ + The start timestamp of the event + """ + startTimestamp: Int! + """ + The end timestamp of the event + """ + endTimestamp: Int! + """ + The timezone of the event + """ + timeZone: String! + """ + The type of video call to added to the gcal event. If not provided, no video call will be added + """ + videoType: GcalVideoTypeEnum + """ + The emails that will be invited to the gcal event. If not provided, the no one will be invited + """ + invitees: [Email!] +}