From 8fe74557315ffb0883373afef3dbd1620b70fc57 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 8 Jan 2024 11:12:10 +0100 Subject: [PATCH 1/3] chore: Convert GraphQL meeting types to use codegen --- codegen.json | 9 + .../server/graphql/mutations/startCheckIn.ts | 152 ---- .../graphql/mutations/startRetrospective.ts | 151 ---- .../graphql/public/mutations/startCheckIn.ts | 122 +++ .../public/mutations/startRetrospective.ts | 136 +++ .../public/typeDefs/ActionMeeting.graphql | 151 ++++ .../public/typeDefs/AgendaItem.graphql | 64 ++ .../public/typeDefs/NewMeeting.graphql | 121 +++ .../public/typeDefs/PokerMeeting.graphql | 142 +++ .../typeDefs/RetrospectiveMeeting.graphql | 238 +++++ .../public/typeDefs/TeamPromptMeeting.graphql | 150 ++++ .../graphql/public/typeDefs/_legacy.graphql | 813 +----------------- .../public/typeDefs/startCheckIn.graphql | 31 + .../graphql/public/types/ActionMeeting.ts | 45 + .../server/graphql/public/types/AgendaItem.ts | 10 + .../server/graphql/public/types/NewMeeting.ts | 69 ++ .../public/types/RetrospectiveMeeting.ts | 86 ++ .../public/types/StartCheckInSuccess.ts | 18 + .../public/types/StartRetrospectiveSuccess.ts | 18 + packages/server/graphql/rootMutation.ts | 4 - packages/server/graphql/rootTypes.ts | 6 - .../server/graphql/types/ActionMeeting.ts | 87 +- packages/server/graphql/types/AgendaItem.ts | 71 +- .../graphql/types/AutogroupReflectionGroup.ts | 19 - packages/server/graphql/types/NewMeeting.ts | 204 +---- packages/server/graphql/types/PokerMeeting.ts | 3 +- .../graphql/types/RetrospectiveMeeting.ts | 203 +---- .../graphql/types/StartCheckInPayload.ts | 35 - .../types/StartRetrospectivePayload.ts | 38 - .../server/graphql/types/TeamPromptMeeting.ts | 3 +- .../server/graphql/types/TranscriptBlock.ts | 19 - 31 files changed, 1450 insertions(+), 1768 deletions(-) delete mode 100644 packages/server/graphql/mutations/startCheckIn.ts delete mode 100644 packages/server/graphql/mutations/startRetrospective.ts create mode 100644 packages/server/graphql/public/mutations/startCheckIn.ts create mode 100644 packages/server/graphql/public/mutations/startRetrospective.ts create mode 100644 packages/server/graphql/public/typeDefs/ActionMeeting.graphql create mode 100644 packages/server/graphql/public/typeDefs/AgendaItem.graphql create mode 100644 packages/server/graphql/public/typeDefs/NewMeeting.graphql create mode 100644 packages/server/graphql/public/typeDefs/PokerMeeting.graphql create mode 100644 packages/server/graphql/public/typeDefs/RetrospectiveMeeting.graphql create mode 100644 packages/server/graphql/public/typeDefs/TeamPromptMeeting.graphql create mode 100644 packages/server/graphql/public/typeDefs/startCheckIn.graphql create mode 100644 packages/server/graphql/public/types/ActionMeeting.ts create mode 100644 packages/server/graphql/public/types/AgendaItem.ts create mode 100644 packages/server/graphql/public/types/NewMeeting.ts create mode 100644 packages/server/graphql/public/types/RetrospectiveMeeting.ts create mode 100644 packages/server/graphql/public/types/StartCheckInSuccess.ts create mode 100644 packages/server/graphql/public/types/StartRetrospectiveSuccess.ts delete mode 100644 packages/server/graphql/types/AutogroupReflectionGroup.ts delete mode 100644 packages/server/graphql/types/StartCheckInPayload.ts delete mode 100644 packages/server/graphql/types/StartRetrospectivePayload.ts delete mode 100644 packages/server/graphql/types/TranscriptBlock.ts diff --git a/codegen.json b/codegen.json index 4448f88b323..fd74ed03705 100644 --- a/codegen.json +++ b/codegen.json @@ -46,9 +46,12 @@ "mappers": { "AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource", "AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource", + "ActionMeeting": "../../database/types/MeetingAction#default", + "ActionMeetingMember": "../../database/types/ActionMeetingMember#default as ActionMeetingMemberDB", "AddApprovedOrganizationDomainsSuccess": "./types/AddApprovedOrganizationDomainsSuccess#AddApprovedOrganizationDomainsSuccessSource", "AddTranscriptionBotSuccess": "./types/AddTranscriptionBotSuccess#AddTranscriptionBotSuccessSource", "AddedNotification": "./types/AddedNotification#AddedNotificationSource", + "AgendaItem": "../../database/types/AgendaItem#default as AgendaItemDB", "ArchiveTeamPayload": "./types/ArchiveTeamPayload#ArchiveTeamPayloadSource", "AuthTokenPayload": "./types/AuthTokenPayload#AuthTokenPayloadSource", "AutogroupSuccess": "./types/AutogroupSuccess#AutogroupSuccessSource", @@ -87,6 +90,7 @@ "Organization": "../../database/types/Organization#default as Organization", "OrganizationUser": "../../database/types/OrganizationUser#default as OrganizationUser", "PokerMeeting": "../../database/types/MeetingPoker#default as MeetingPoker", + "PokerMeetingMember": "../../database/types/MeetingPokerMeetingMember#default as PokerMeetingMemberDB", "RRule": "rrule#RRule", "ReflectPrompt": "../../database/types/RetrospectivePrompt#default", "ReflectTemplate": "../../database/types/ReflectTemplate#default", @@ -95,7 +99,9 @@ "RemoveTeamMemberPayload": "./types/RemoveTeamMemberPayload#RemoveTeamMemberPayloadSource", "RequestToJoinDomainSuccess": "./types/RequestToJoinDomainSuccess#RequestToJoinDomainSuccessSource", "ResetReflectionGroupsSuccess": "./types/ResetReflectionGroupsSuccess#ResetReflectionGroupsSuccessSource", + "RetroReflectionGroup": "../../database/types/RetroReflectionGroup#default as RetroReflectionGroupDB", "RetrospectiveMeeting": "../../database/types/MeetingRetrospective#default", + "RetrospectiveMeetingMember": "../../database/types/RetroMeetingMember#default", "Reactable": "../../database/types/Reactable#Reactable", "RetrospectiveMeetingSettings": "../../database/types/MeetingSettingsRetrospective#default", "SAML": "./types/SAML#SAMLSource", @@ -103,6 +109,8 @@ "SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource", "SetOrgUserRoleSuccess": "./types/SetOrgUserRoleSuccess#SetOrgUserRoleSuccessSource", "ShareTopicSuccess": "./types/ShareTopicSuccess#ShareTopicSuccessSource", + "StartCheckInSuccess": "./types/StartCheckInSuccess#StartCheckInSuccessSource", + "StartRetrospectiveSuccess": "./types/StartRetrospectiveSuccess#StartRetrospectiveSuccessSource", "StartTeamPromptSuccess": "./types/StartTeamPromptSuccess#StartTeamPromptSuccessSource", "StripeFailPaymentPayload": "./types/StripeFailPaymentPayload#StripeFailPaymentPayloadSource", "Task": "../../database/types/Task#default", @@ -114,6 +122,7 @@ "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrations": "./types/TeamMemberIntegrations#TeamMemberIntegrationsSource", "TeamPromptMeeting": "../../database/types/MeetingTeamPrompt#default as MeetingTeamPromptDB", + "TeamPromptMeetingMember": "../../database/types/TeamPromptMeetingMember#default as TeamPromptMeetingMemberDB", "TeamPromptResponse": "../../postgres/queries/getTeamPromptResponsesByIds#TeamPromptResponse", "TemplateDimension": "../../database/types/TemplateDimension#default", "TimelineEventTeamPromptComplete": "./types/TimelineEventTeamPromptComplete#TimelineEventTeamPromptCompleteSource", diff --git a/packages/server/graphql/mutations/startCheckIn.ts b/packages/server/graphql/mutations/startCheckIn.ts deleted file mode 100644 index 2197d635531..00000000000 --- a/packages/server/graphql/mutations/startCheckIn.ts +++ /dev/null @@ -1,152 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import ActionMeetingMember from '../../database/types/ActionMeetingMember' -import MeetingAction from '../../database/types/MeetingAction' -import generateUID from '../../generateUID' -import updateTeamByTeamId from '../../postgres/queries/updateTeamByTeamId' -import {MeetingTypeEnum} from '../../postgres/types/Meeting' -import {analytics} from '../../utils/analytics/analytics' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import CreateGcalEventInput, {CreateGcalEventInputType} from '../public/types/CreateGcalEventInput' -import StartCheckInPayload from '../types/StartCheckInPayload' -import createGcalEvent from './helpers/createGcalEvent' -import createNewMeetingPhases from './helpers/createNewMeetingPhases' -import isStartMeetingLocked from './helpers/isStartMeetingLocked' -import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier' -import maybeCreateOneOnOneTeam from './helpers/maybeCreateOneOnOneTeam' -import CreateOneOnOneTeamInput, { - CreateOneOnOneTeamInputType -} from '../public/types/CreateOneOnOneTeamInput' - -export default { - type: new GraphQLNonNull(StartCheckInPayload), - description: 'Start a new meeting', - args: { - teamId: { - type: GraphQLID, - description: 'The team starting the meeting. Can be null if oneOnOneTeamInput is provided' - }, - gcalInput: { - type: CreateGcalEventInput, - description: 'The gcal event to create. If not provided, no event will be created' - }, - oneOnOneTeamInput: { - type: CreateOneOnOneTeamInput, - description: 'One-on-One ad-hoc team to create. If provided, teamId ignored' - } - }, - async resolve( - _source: unknown, - { - teamId: existingTeamId, - gcalInput, - oneOnOneTeamInput - }: { - teamId?: string - gcalInput?: CreateGcalEventInputType - oneOnOneTeamInput?: CreateOneOnOneTeamInputType - }, - context: GQLContext - ) { - const r = await getRethink() - const {authToken, socketId: mutatorId, dataLoader} = context - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - // AUTH - const viewerId = getUserId(authToken) - - if (existingTeamId && oneOnOneTeamInput) { - return standardError( - new Error('Please provide either "teamId" or "oneOnOneTeamInput", but not both'), - { - userId: viewerId - } - ) - } - - if (existingTeamId) { - if (!isTeamMember(authToken, existingTeamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - const unpaidError = await isStartMeetingLocked(existingTeamId, dataLoader) - if (unpaidError) return standardError(new Error(unpaidError), {userId: viewerId}) - } - const viewer = await dataLoader.get('users').loadNonNull(viewerId) - const teamId = oneOnOneTeamInput - ? await maybeCreateOneOnOneTeam(viewer, oneOnOneTeamInput, context) - : existingTeamId - if (!teamId) { - return standardError(new Error('Must provide teamId or oneOnOneTeamInput'), { - userId: viewerId - }) - } - - const meetingType: MeetingTypeEnum = 'action' - - // 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 meeting = new MeetingAction({ - id: meetingId, - teamId, - meetingCount, - name: oneOnOneTeamInput ? `One on One #${meetingCount + 1}` : undefined, - phases, - facilitatorUserId: viewerId - }) - await r.table('NewMeeting').insert(meeting).run() - - // Disallow 2 active check-in meetings - const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) - const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { - const {id} = activeMeeting - if (id === meetingId || activeMeeting.meetingType !== meetingType) return false - return true - }) - if (otherActiveMeeting) { - await r.table('NewMeeting').get(meetingId).delete().run() - return {error: {message: 'Meeting already started'}} - } - const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) - const agendaItemIds = agendaItems.map(({id}) => id) - - const updates = { - lastMeetingType: meetingType - } - await Promise.all([ - r - .table('MeetingMember') - .insert(new ActionMeetingMember({meetingId, userId: viewerId, teamId})) - .run(), - updateTeamByTeamId(updates, teamId), - r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run() - ]) - IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) - const team = await dataLoader.get('teams').loadNonNull(teamId) - analytics.meetingStarted(viewer, meeting, undefined, team) - const {error} = await createGcalEvent({gcalInput, teamId, meetingId, viewerId, dataLoader}) - const data = {teamId, meetingId, hasGcalError: !!error?.message} - publish(SubscriptionChannel.TEAM, teamId, 'StartCheckInSuccess', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/mutations/startRetrospective.ts b/packages/server/graphql/mutations/startRetrospective.ts deleted file mode 100644 index 2738da699ae..00000000000 --- a/packages/server/graphql/mutations/startRetrospective.ts +++ /dev/null @@ -1,151 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -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' -import {analytics} from '../../utils/analytics/analytics' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import CreateGcalEventInput, {CreateGcalEventInputType} from '../public/types/CreateGcalEventInput' -import StartRetrospectivePayload from '../types/StartRetrospectivePayload' -import createGcalEvent from './helpers/createGcalEvent' -import createNewMeetingPhases from './helpers/createNewMeetingPhases' -import isStartMeetingLocked from './helpers/isStartMeetingLocked' -import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier' - -export default { - type: new GraphQLNonNull(StartRetrospectivePayload), - description: 'Start a new meeting', - args: { - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The team starting the meeting' - }, - gcalInput: { - type: CreateGcalEventInput, - description: 'The gcal event to create. If not provided, no event will be created' - } - }, - async resolve( - _source: unknown, - {teamId, gcalInput}: {teamId: string; gcalInput?: CreateGcalEventInputType}, - {authToken, socketId: mutatorId, dataLoader}: GQLContext - ) { - const r = await getRethink() - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const DUPLICATE_THRESHOLD = 3000 - // AUTH - const viewerId = getUserId(authToken) - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('User not on team'), {userId: viewerId}) - } - 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 meetingSettings = (await dataLoader - .get('meetingSettingsByType') - .load({teamId, meetingType})) as MeetingSettingsRetrospective - const { - id: meetingSettingsId, - totalVotes, - maxVotesPerGroup, - selectedTemplateId, - disableAnonymity, - videoMeetingURL - } = meetingSettings - const meeting = new MeetingRetrospective({ - id: meetingId, - teamId, - meetingCount, - phases, - showConversionModal, - facilitatorUserId: viewerId, - totalVotes, - maxVotesPerGroup, - disableAnonymity, - templateId: selectedTemplateId, - videoMeetingURL: videoMeetingURL ?? undefined - }) - - const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - await Promise.all([ - r.table('NewMeeting').insert(meeting).run(), - updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) - ]) - - // Disallow accidental starts (2 meetings within 2 seconds) - const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) - const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { - const {createdAt, id} = activeMeeting - if (id === meetingId || activeMeeting.meetingType !== meetingType) return false - return createdAt.getTime() > Date.now() - DUPLICATE_THRESHOLD - }) - if (otherActiveMeeting) { - await r.table('NewMeeting').get(meetingId).delete().run() - return {error: {message: 'Meeting already started'}} - } - - const updates = { - lastMeetingType: meetingType - } - await Promise.all([ - r - .table('MeetingMember') - .insert( - new RetroMeetingMember({meetingId, userId: viewerId, teamId, votesRemaining: totalVotes}) - ) - .run(), - updateTeamByTeamId(updates, teamId), - videoMeetingURL && - r - .table('MeetingSettings') - .get(meetingSettingsId) - .update({ - videoMeetingURL: null - }) - .run() - ]) - IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) - analytics.meetingStarted(viewer, meeting, template) - const {error} = await createGcalEvent({gcalInput, meetingId, teamId, viewerId, dataLoader}) - const data = {teamId, meetingId, hasGcalError: !!error?.message} - publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts new file mode 100644 index 00000000000..b3997933509 --- /dev/null +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -0,0 +1,122 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import ActionMeetingMember from '../../../database/types/ActionMeetingMember' +import MeetingAction from '../../../database/types/MeetingAction' +import generateUID from '../../../generateUID' +import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' +import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {analytics} from '../../../utils/analytics/analytics' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import createGcalEvent from '../../mutations/helpers/createGcalEvent' +import createNewMeetingPhases from '../../mutations/helpers/createNewMeetingPhases' +import isStartMeetingLocked from '../../mutations/helpers/isStartMeetingLocked' +import maybeCreateOneOnOneTeam from '../../mutations/helpers/maybeCreateOneOnOneTeam' +import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' +import {MutationResolvers} from '../resolverTypes' + +const startCheckIn: MutationResolvers['startCheckIn'] = async ( + _source, + {teamId: existingTeamId, gcalInput, oneOnOneTeamInput}, + context +) => { + const r = await getRethink() + const {authToken, socketId: mutatorId, dataLoader} = context + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + // AUTH + const viewerId = getUserId(authToken) + + if (existingTeamId && oneOnOneTeamInput) { + return standardError( + new Error('Please provide either "teamId" or "oneOnOneTeamInput", but not both'), + { + userId: viewerId + } + ) + } + + if (existingTeamId) { + if (!isTeamMember(authToken, existingTeamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + const unpaidError = await isStartMeetingLocked(existingTeamId, dataLoader) + if (unpaidError) return standardError(new Error(unpaidError), {userId: viewerId}) + } + const viewer = await dataLoader.get('users').loadNonNull(viewerId) + const teamId = oneOnOneTeamInput + ? await maybeCreateOneOnOneTeam(viewer, oneOnOneTeamInput, context) + : existingTeamId + if (!teamId) { + return standardError(new Error('Must provide teamId or oneOnOneTeamInput'), { + userId: viewerId + }) + } + + const meetingType: MeetingTypeEnum = 'action' + + // 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 meeting = new MeetingAction({ + id: meetingId, + teamId, + meetingCount, + name: oneOnOneTeamInput ? `One on One #${meetingCount + 1}` : undefined, + phases, + facilitatorUserId: viewerId + }) + await r.table('NewMeeting').insert(meeting).run() + + // Disallow 2 active check-in meetings + const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) + const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { + const {id} = activeMeeting + if (id === meetingId || activeMeeting.meetingType !== meetingType) return false + return true + }) + if (otherActiveMeeting) { + await r.table('NewMeeting').get(meetingId).delete().run() + return {error: {message: 'Meeting already started'}} + } + const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const agendaItemIds = agendaItems.map(({id}) => id) + + const updates = { + lastMeetingType: meetingType + } + await Promise.all([ + r + .table('MeetingMember') + .insert(new ActionMeetingMember({meetingId, userId: viewerId, teamId})) + .run(), + updateTeamByTeamId(updates, teamId), + r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run() + ]) + IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) + const team = await dataLoader.get('teams').loadNonNull(teamId) + analytics.meetingStarted(viewer, meeting, undefined, team) + const {error} = await createGcalEvent({gcalInput, teamId, meetingId, viewerId, dataLoader}) + const data = {teamId, meetingId, hasGcalError: !!error?.message} + publish(SubscriptionChannel.TEAM, teamId, 'StartCheckInSuccess', data, subOptions) + return data +} + +export default startCheckIn diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts new file mode 100644 index 00000000000..edc5426da1a --- /dev/null +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -0,0 +1,136 @@ +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' +import {analytics} from '../../../utils/analytics/analytics' +import {getUserId, isTeamMember} from '../../../utils/authorization' +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' + +const startRetrospective: MutationResolvers['startRetrospective'] = async ( + _source, + {teamId, gcalInput}, + {authToken, socketId: mutatorId, dataLoader} +) => { + const r = await getRethink() + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const DUPLICATE_THRESHOLD = 3000 + // AUTH + const viewerId = getUserId(authToken) + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('User not on team'), {userId: viewerId}) + } + 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 meetingSettings = (await dataLoader + .get('meetingSettingsByType') + .load({teamId, meetingType})) as MeetingSettingsRetrospective + const { + id: meetingSettingsId, + totalVotes, + maxVotesPerGroup, + selectedTemplateId, + disableAnonymity, + videoMeetingURL + } = meetingSettings + const meeting = new MeetingRetrospective({ + id: meetingId, + teamId, + meetingCount, + phases, + showConversionModal, + facilitatorUserId: viewerId, + totalVotes, + maxVotesPerGroup, + disableAnonymity, + templateId: selectedTemplateId, + videoMeetingURL: videoMeetingURL ?? undefined + }) + + const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) + await Promise.all([ + r.table('NewMeeting').insert(meeting).run(), + updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) + ]) + + // Disallow accidental starts (2 meetings within 2 seconds) + const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) + const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { + const {createdAt, id} = activeMeeting + if (id === meetingId || activeMeeting.meetingType !== meetingType) return false + return createdAt.getTime() > Date.now() - DUPLICATE_THRESHOLD + }) + if (otherActiveMeeting) { + await r.table('NewMeeting').get(meetingId).delete().run() + return {error: {message: 'Meeting already started'}} + } + + const updates = { + lastMeetingType: meetingType + } + await Promise.all([ + r + .table('MeetingMember') + .insert( + new RetroMeetingMember({meetingId, userId: viewerId, teamId, votesRemaining: totalVotes}) + ) + .run(), + updateTeamByTeamId(updates, teamId), + videoMeetingURL && + r + .table('MeetingSettings') + .get(meetingSettingsId) + .update({ + videoMeetingURL: null + }) + .run() + ]) + IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) + analytics.meetingStarted(viewer, meeting, template) + const {error} = await createGcalEvent({gcalInput, meetingId, teamId, viewerId, dataLoader}) + const data = {teamId, meetingId, hasGcalError: !!error?.message} + publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions) + return data +} + +export default startRetrospective diff --git a/packages/server/graphql/public/typeDefs/ActionMeeting.graphql b/packages/server/graphql/public/typeDefs/ActionMeeting.graphql new file mode 100644 index 00000000000..28cc23a00a0 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/ActionMeeting.graphql @@ -0,0 +1,151 @@ +""" +An action meeting +""" +type ActionMeeting implements NewMeeting { + """ + The unique meeting id. shortid. + """ + id: ID! + + """ + The timestamp the meeting was created + """ + createdAt: DateTime! + + """ + The id of the user that created the meeting + """ + createdBy: ID! + + """ + The user that created the meeting + """ + createdByUser: User! + + """ + The timestamp the meeting officially ended + """ + endedAt: DateTime + + """ + The location of the facilitator in the meeting + """ + facilitatorStageId: ID! + + """ + The userId (or anonymousId) of the most recent facilitator + """ + facilitatorUserId: ID! + + """ + The facilitator team member + """ + facilitator: TeamMember! + + """ + Is this locked for starter plans? + """ + locked: Boolean! + + """ + The team members that were active during the time of the meeting + """ + meetingMembers: [ActionMeetingMember!]! + + """ + The auto-incrementing meeting number for the team + """ + meetingNumber: Int! + + """ + The id of the meeting series this meeting belongs to + """ + meetingSeriesId: ID + + meetingType: MeetingTypeEnum! + + """ + The name of the meeting + """ + name: String! + + """ + The organization this meeting belongs to + """ + organization: Organization! + + """ + The phases the meeting will go through, including all phase-specific state + """ + phases: [NewMeetingPhase!]! + + """ + If meeting has a meeting series associated, this is the time the meeting will end + """ + scheduledEndTime: DateTime + + """ + true if should show the org the conversion modal, else false + """ + showConversionModal: Boolean! + + """ + The OpenAI generated summary of all the content in the meeting, such as reflections, tasks, and comments. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` + """ + summary: String + + """ + The time the meeting summary was emailed to the team + """ + summarySentAt: DateTime + + """ + foreign key for team + """ + teamId: ID! + + """ + The team that ran the meeting + """ + team: Team! + + """ + The last time a meeting was updated (stage completed, finished, etc) + """ + updatedAt: DateTime + + """ + The action meeting member of the viewer + """ + viewerMeetingMember: ActionMeetingMember + + """ + A single agenda item + """ + agendaItem(agendaItemId: ID!): AgendaItem + + """ + The number of agenda items generated in the meeting + """ + agendaItemCount: Int! + + """ + All of the agenda items for the meeting + """ + agendaItems: [AgendaItem!]! + + """ + The number of comments generated in the meeting + """ + commentCount: Int! + + """ + The number of tasks generated in the meeting + """ + taskCount: Int! + + """ + The tasks created within the meeting + """ + tasks: [Task!]! +} diff --git a/packages/server/graphql/public/typeDefs/AgendaItem.graphql b/packages/server/graphql/public/typeDefs/AgendaItem.graphql new file mode 100644 index 00000000000..ec96cfa59ba --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AgendaItem.graphql @@ -0,0 +1,64 @@ +""" +A request placeholder that will likely turn into 1 or more tasks +""" +type AgendaItem { + """ + The unique agenda item id teamId::shortid + """ + id: ID! + + """ + The body of the agenda item + """ + content: String! + + """ + The timestamp the agenda item was created + """ + createdAt: DateTime + + """ + true if the agenda item has not been processed or deleted + """ + isActive: Boolean! + + """ + True if the agenda item has been pinned + """ + pinned: Boolean + + """ + If pinned, this is the unique id of the original agenda item + """ + pinnedParentId: ID + + """ + The sort order of the agenda item in the list + """ + sortOrder: Float! + + """ + *The team for this agenda item + """ + teamId: ID! + + """ + The teamMemberId that created this agenda item + """ + teamMemberId: ID! + + """ + The meetingId of the agenda item + """ + meetingId: ID + + """ + The timestamp the agenda item was updated + """ + updatedAt: DateTime + + """ + The team member that created the agenda item + """ + teamMember: TeamMember! +} diff --git a/packages/server/graphql/public/typeDefs/NewMeeting.graphql b/packages/server/graphql/public/typeDefs/NewMeeting.graphql new file mode 100644 index 00000000000..c38dda89f62 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/NewMeeting.graphql @@ -0,0 +1,121 @@ +""" +A team meeting history for all previous meetings +""" +interface NewMeeting { + """ + The unique meeting id. shortid. + """ + id: ID! + + """ + The timestamp the meeting was created + """ + createdAt: DateTime! + + """ + The id of the user that created the meeting + """ + createdBy: ID! + + """ + The user that created the meeting + """ + createdByUser: User! + + """ + The timestamp the meeting officially ended + """ + endedAt: DateTime + + """ + The location of the facilitator in the meeting + """ + facilitatorStageId: ID! + + """ + The userId (or anonymousId) of the most recent facilitator + """ + facilitatorUserId: ID! + + """ + The facilitator team member + """ + facilitator: TeamMember! + + """ + Is this locked for starter plans? + """ + locked: Boolean! + + """ + The team members that were active during the time of the meeting + """ + meetingMembers: [MeetingMember!]! + + """ + The auto-incrementing meeting number for the team + """ + meetingNumber: Int! + + """ + The id of the meeting series this meeting belongs to + """ + meetingSeriesId: ID + + meetingType: MeetingTypeEnum! + + """ + The name of the meeting + """ + name: String! + + """ + The organization this meeting belongs to + """ + organization: Organization! + + """ + The phases the meeting will go through, including all phase-specific state + """ + phases: [NewMeetingPhase!]! + + """ + If meeting has a meeting series associated, this is the time the meeting will end + """ + scheduledEndTime: DateTime + + """ + true if should show the org the conversion modal, else false + """ + showConversionModal: Boolean! + + """ + The OpenAI generated summary of all the content in the meeting, such as reflections, tasks, and comments. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` + """ + summary: String + + """ + The time the meeting summary was emailed to the team + """ + summarySentAt: DateTime + + """ + foreign key for team + """ + teamId: ID! + + """ + The team that ran the meeting + """ + team: Team! + + """ + The last time a meeting was updated (stage completed, finished, etc) + """ + updatedAt: DateTime + + """ + The meeting member of the viewer + """ + viewerMeetingMember: MeetingMember +} diff --git a/packages/server/graphql/public/typeDefs/PokerMeeting.graphql b/packages/server/graphql/public/typeDefs/PokerMeeting.graphql new file mode 100644 index 00000000000..19cbc21f971 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/PokerMeeting.graphql @@ -0,0 +1,142 @@ +""" +A Poker meeting +""" +type PokerMeeting implements NewMeeting { + """ + The unique meeting id. shortid. + """ + id: ID! + + """ + The timestamp the meeting was created + """ + createdAt: DateTime! + + """ + The id of the user that created the meeting + """ + createdBy: ID! + + """ + The user that created the meeting + """ + createdByUser: User! + + """ + The timestamp the meeting officially ended + """ + endedAt: DateTime + + """ + The location of the facilitator in the meeting + """ + facilitatorStageId: ID! + + """ + The userId (or anonymousId) of the most recent facilitator + """ + facilitatorUserId: ID! + + """ + The facilitator team member + """ + facilitator: TeamMember! + + """ + Is this locked for starter plans? + """ + locked: Boolean! + + """ + The team members that were active during the time of the meeting + """ + meetingMembers: [PokerMeetingMember!]! + + """ + The auto-incrementing meeting number for the team + """ + meetingNumber: Int! + + """ + The id of the meeting series this meeting belongs to + """ + meetingSeriesId: ID + + meetingType: MeetingTypeEnum! + + """ + The name of the meeting + """ + name: String! + + """ + The organization this meeting belongs to + """ + organization: Organization! + + """ + The phases the meeting will go through, including all phase-specific state + """ + phases: [NewMeetingPhase!]! + + """ + If meeting has a meeting series associated, this is the time the meeting will end + """ + scheduledEndTime: DateTime + + """ + true if should show the org the conversion modal, else false + """ + showConversionModal: Boolean! + + """ + The OpenAI generated summary of all the content in the meeting, such as reflections, tasks, and comments. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` + """ + summary: String + + """ + The time the meeting summary was emailed to the team + """ + summarySentAt: DateTime + teamId: ID! + + """ + The team that ran the meeting + """ + team: Team! + + """ + The last time a meeting was updated (stage completed, finished, etc) + """ + updatedAt: DateTime + + """ + The Poker meeting member of the viewer + """ + viewerMeetingMember: PokerMeetingMember + + """ + The number of comments generated in the meeting + """ + commentCount: Int! + + """ + The number of stories scored during a meeting + """ + storyCount: Int! + + """ + A single story created in a Sprint Poker meeting + """ + story(storyId: ID!): Task + + """ + The ID of the template used for the meeting. Note the underlying template could have changed! + """ + templateId: ID! @deprecated(reason: "The underlying template could be mutated. Use templateRefId") + + """ + The ID of the immutable templateRef used for the meeting + """ + templateRefId: ID! +} diff --git a/packages/server/graphql/public/typeDefs/RetrospectiveMeeting.graphql b/packages/server/graphql/public/typeDefs/RetrospectiveMeeting.graphql new file mode 100644 index 00000000000..f81308825d6 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/RetrospectiveMeeting.graphql @@ -0,0 +1,238 @@ +""" +A suggested reflection group created by OpenAI' +""" +type AutogroupReflectionGroup { + """ + The smart title for the reflection group created by OpenAI + """ + groupTitle: String! + + """ + The ids of the reflections in the group + """ + reflectionIds: [ID!]! +} + +""" +A block of the meeting transcription with a speaker and text +""" +type TranscriptBlock { + """ + The speaker who said the words + """ + speaker: String! + + """ + The words that the speaker said + """ + words: String! +} + +""" +A retrospective meeting +""" +type RetrospectiveMeeting implements NewMeeting { + """ + The unique meeting id. shortid. + """ + id: ID! + + """ + The suggested reflection groups created by OpenAI + """ + autogroupReflectionGroups: [AutogroupReflectionGroup!] + + """ + The groups that existed before the autogrouping + """ + resetReflectionGroups: [AutogroupReflectionGroup!] + + """ + The Zoom meeting URL for the meeting + """ + videoMeetingURL: String + + """ + The transcription of the meeting + """ + transcription: [TranscriptBlock!] + + """ + The timestamp the meeting was created + """ + createdAt: DateTime! + + """ + The id of the user that created the meeting + """ + createdBy: ID! + + """ + The user that created the meeting + """ + createdByUser: User! + + """ + Disables anonymity of reflections + """ + disableAnonymity: Boolean! + + """ + The timestamp the meeting officially ended + """ + endedAt: DateTime + + """ + The location of the facilitator in the meeting + """ + facilitatorStageId: ID! + + """ + The userId (or anonymousId) of the most recent facilitator + """ + facilitatorUserId: ID! + + """ + The facilitator team member + """ + facilitator: TeamMember! + + """ + The team members that were active during the time of the meeting + """ + meetingMembers: [RetrospectiveMeetingMember!]! + + """ + The auto-incrementing meeting number for the team + """ + meetingNumber: Int! + + """ + The id of the meeting series this meeting belongs to + """ + meetingSeriesId: ID + + meetingType: MeetingTypeEnum! + + """ + The name of the meeting + """ + name: String! + + """ + The organization this meeting belongs to + """ + organization: Organization! + + """ + The phases the meeting will go through, including all phase-specific state + """ + phases: [NewMeetingPhase!]! + + """ + If meeting has a meeting series associated, this is the time the meeting will end + """ + scheduledEndTime: DateTime + + """ + true if should show the org the conversion modal, else false + """ + showConversionModal: Boolean! + + """ + The OpenAI generated summary of all the content in the meeting, such as reflections, tasks, and comments. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` + """ + summary: String + + """ + The time the meeting summary was emailed to the team + """ + summarySentAt: DateTime + + teamId: ID! + + """ + The team that ran the meeting + """ + team: Team! + + """ + The last time a meeting was updated (stage completed, finished, etc) + """ + updatedAt: DateTime + + """ + The retrospective meeting member of the viewer + """ + viewerMeetingMember: RetrospectiveMeetingMember + + """ + the threshold used to achieve the autogroup. Useful for model tuning. Serves as a flag if autogroup was used. + """ + autoGroupThreshold: Float + + """ + The number of comments generated in the meeting + """ + commentCount: Int! + + """ + Is this locked for starter plans? + """ + locked: Boolean! + + """ + the number of votes allowed for each participant to cast on a single group + """ + maxVotesPerGroup: Int! + + """ + the next smallest distance threshold to guarantee at least 1 more grouping will be achieved + """ + nextAutoGroupThreshold: Float + + """ + The number of reflections generated in the meeting + """ + reflectionCount: Int! + + """ + a single reflection group + """ + reflectionGroup(reflectionGroupId: ID!): RetroReflectionGroup + + """ + The grouped reflections + """ + reflectionGroups(sortBy: ReflectionGroupSortEnum): [RetroReflectionGroup!]! + + """ + The number of tasks generated in the meeting + """ + taskCount: Int! + + """ + The tasks created within the meeting + """ + tasks: [Task!]! + + """ + The ID of the template used for the meeting + """ + templateId: ID! + + """ + The number of topics generated in the meeting + """ + topicCount: Int! + + """ + the total number of votes allowed for each participant + """ + totalVotes: Int! + + """ + The sum total of the votes remaining for the meeting members that are present in the meeting + """ + votesRemaining: Int! +} diff --git a/packages/server/graphql/public/typeDefs/TeamPromptMeeting.graphql b/packages/server/graphql/public/typeDefs/TeamPromptMeeting.graphql new file mode 100644 index 00000000000..24035c7b32f --- /dev/null +++ b/packages/server/graphql/public/typeDefs/TeamPromptMeeting.graphql @@ -0,0 +1,150 @@ +""" +A team prompt meeting +""" +type TeamPromptMeeting implements NewMeeting { + """ + The unique meeting id. shortid. + """ + id: ID! + + """ + The meeting series id this meeting is associated with if the meeting is recurring + """ + meetingSeriesId: ID + + """ + The meeting series this meeting is associated with if the meeting is recurring + """ + meetingSeries: MeetingSeries + + """ + The timestamp the meeting was created + """ + createdAt: DateTime! + + """ + The id of the user that created the meeting + """ + createdBy: ID! + + """ + The user that created the meeting + """ + createdByUser: User! + + """ + The timestamp the meeting officially ended + """ + endedAt: DateTime + + """ + The location of the facilitator in the meeting + """ + facilitatorStageId: ID! + + """ + The userId (or anonymousId) of the most recent facilitator + """ + facilitatorUserId: ID! + + """ + The facilitator team member + """ + facilitator: TeamMember! + + """ + Is this locked for starter plans? + """ + locked: Boolean! + + """ + The team members that were active during the time of the meeting + """ + meetingMembers: [MeetingMember!]! + + """ + The auto-incrementing meeting number for the team + """ + meetingNumber: Int! + meetingType: MeetingTypeEnum! + + """ + The name of the meeting + """ + name: String! + + """ + The organization this meeting belongs to + """ + organization: Organization! + + """ + The phases the meeting will go through, including all phase-specific state + """ + phases: [NewMeetingPhase!]! + + """ + If meeting has a meeting series associated, this is the time the meeting will end + """ + scheduledEndTime: DateTime + + """ + true if should show the org the conversion modal, else false + """ + showConversionModal: Boolean! + + """ + The OpenAI generated summary of all the content in the meeting, such as reflections, tasks, and comments. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` + """ + summary: String + + """ + The time the meeting summary was emailed to the team + """ + summarySentAt: DateTime + + """ + The tasks created within the meeting + """ + tasks: [Task!]! + + """ + foreign key for team + """ + teamId: ID! + + """ + The team that ran the meeting + """ + team: Team! + + """ + The last time a meeting was updated (stage completed, finished, etc) + """ + updatedAt: DateTime + + """ + The team prompt meeting member of the viewer + """ + viewerMeetingMember: TeamPromptMeetingMember + + """ + The settings that govern the team prompt meeting + """ + settings: TeamPromptMeetingSettings! + + """ + The tasks created within the meeting + """ + responses: [TeamPromptResponse!]! + + """ + The previous meeting in the series if this meeting is recurring + """ + prevMeeting: TeamPromptMeeting + + """ + The next meeting in the series if this meeting is recurring + """ + nextMeeting: TeamPromptMeeting +} diff --git a/packages/server/graphql/public/typeDefs/_legacy.graphql b/packages/server/graphql/public/typeDefs/_legacy.graphql index 5892fcd74f7..b236b7a7af8 100644 --- a/packages/server/graphql/public/typeDefs/_legacy.graphql +++ b/packages/server/graphql/public/typeDefs/_legacy.graphql @@ -439,112 +439,6 @@ interface NewMeetingStage { timeRemaining: Float } -""" -A team meeting history for all previous meetings -""" -interface NewMeeting { - """ - The unique meeting id. shortid. - """ - id: ID! - - """ - The timestamp the meeting was created - """ - createdAt: DateTime! - - """ - The id of the user that created the meeting - """ - createdBy: ID! - - """ - The user that created the meeting - """ - createdByUser: User! - - """ - The timestamp the meeting officially ended - """ - endedAt: DateTime - - """ - The location of the facilitator in the meeting - """ - facilitatorStageId: ID! - - """ - The userId (or anonymousId) of the most recent facilitator - """ - facilitatorUserId: ID! - - """ - The facilitator team member - """ - facilitator: TeamMember! - - """ - The team members that were active during the time of the meeting - """ - meetingMembers: [MeetingMember!]! - - """ - The auto-incrementing meeting number for the team - """ - meetingNumber: Int! - meetingType: MeetingTypeEnum! - - """ - The name of the meeting - """ - name: String! - - """ - The organization this meeting belongs to - """ - organization: Organization! - - """ - The phases the meeting will go through, including all phase-specific state - """ - phases: [NewMeetingPhase!]! - - """ - true if should show the org the conversion modal, else false - """ - showConversionModal: Boolean! - - """ - The time the meeting summary was emailed to the team - """ - summarySentAt: DateTime - - """ - foreign key for team - """ - teamId: ID! - - """ - The team that ran the meeting - """ - team: Team! - - """ - The last time a meeting was updated (stage completed, finished, etc) - """ - updatedAt: DateTime - - """ - The meeting member of the viewer - """ - viewerMeetingMember: MeetingMember - - """ - Is this locked for starter plans? - """ - locked: Boolean! -} - """ A connection to a list of items. """ @@ -801,77 +695,6 @@ interface Threadable { updatedAt: DateTime! } -""" -A request placeholder that will likely turn into 1 or more tasks -""" -type AgendaItem { - """ - The unique agenda item id teamId::shortid - """ - id: ID! - - """ - A list of users currently commenting - """ - commentors: [CommentorDetails!] - @deprecated(reason: "Moved to ThreadConnection. Can remove Jun-01-2021") - - """ - The body of the agenda item - """ - content: String! - - """ - The timestamp the agenda item was created - """ - createdAt: DateTime - - """ - true if the agenda item has not been processed or deleted - """ - isActive: Boolean! - - """ - True if the agenda item has been pinned - """ - pinned: Boolean - - """ - If pinned, this is the unique id of the original agenda item - """ - pinnedParentId: ID - - """ - The sort order of the agenda item in the list - """ - sortOrder: Float! - - """ - *The team for this agenda item - """ - teamId: ID! - - """ - The teamMemberId that created this agenda item - """ - teamMemberId: ID! - - """ - The meetingId of the agenda item - """ - meetingId: ID - - """ - The timestamp the agenda item was updated - """ - updatedAt: DateTime - - """ - The team member that created the agenda item - """ - teamMember: TeamMember! -} - """ The user that is commenting """ @@ -4245,137 +4068,6 @@ type NotifyPromoteToOrgLeader implements Notification { userId: ID! } -""" -An action meeting -""" -type ActionMeeting implements NewMeeting { - """ - The unique meeting id. shortid. - """ - id: ID! - - """ - The timestamp the meeting was created - """ - createdAt: DateTime! - - """ - The id of the user that created the meeting - """ - createdBy: ID! - - """ - The user that created the meeting - """ - createdByUser: User! - - """ - The timestamp the meeting officially ended - """ - endedAt: DateTime - - """ - The location of the facilitator in the meeting - """ - facilitatorStageId: ID! - - """ - The userId (or anonymousId) of the most recent facilitator - """ - facilitatorUserId: ID! - - """ - The facilitator team member - """ - facilitator: TeamMember! - - """ - The team members that were active during the time of the meeting - """ - meetingMembers: [ActionMeetingMember!]! - - """ - The auto-incrementing meeting number for the team - """ - meetingNumber: Int! - meetingType: MeetingTypeEnum! - - """ - The name of the meeting - """ - name: String! - - """ - The organization this meeting belongs to - """ - organization: Organization! - - """ - The phases the meeting will go through, including all phase-specific state - """ - phases: [NewMeetingPhase!]! - - """ - true if should show the org the conversion modal, else false - """ - showConversionModal: Boolean! - - """ - The time the meeting summary was emailed to the team - """ - summarySentAt: DateTime - - """ - foreign key for team - """ - teamId: ID! - - """ - The team that ran the meeting - """ - team: Team! - - """ - The last time a meeting was updated (stage completed, finished, etc) - """ - updatedAt: DateTime - - """ - The action meeting member of the viewer - """ - viewerMeetingMember: ActionMeetingMember - - """ - A single agenda item - """ - agendaItem(agendaItemId: ID!): AgendaItem - - """ - The number of agenda items generated in the meeting - """ - agendaItemCount: Int! - - """ - All of the agenda items for the meeting - """ - agendaItems: [AgendaItem!]! - - """ - The number of comments generated in the meeting - """ - commentCount: Int! - - """ - The number of tasks generated in the meeting - """ - taskCount: Int! - - """ - The tasks created within the meeting - """ - tasks: [Task!]! -} - """ All the meeting specifics for a user in a action meeting """ @@ -4482,205 +4174,43 @@ type PokerMeetingSettings implements TeamMeetingSettings { } """ -A retrospective meeting +sorts for the reflection group. default is sortOrder. sorting by voteCount filters out items without votes. +""" +enum ReflectionGroupSortEnum { + voteCount + stageOrder +} + +""" +All the meeting specifics for a user in a retro meeting """ -type RetrospectiveMeeting implements NewMeeting { +type RetrospectiveMeetingMember implements MeetingMember { """ - The unique meeting id. shortid. + A composite of userId::meetingId """ id: ID! """ - The timestamp the meeting was created + true if present, false if absent, else null """ - createdAt: DateTime! + isCheckedIn: Boolean + @deprecated( + reason: "Members are checked in when they enter the meeting now & not created beforehand" + ) + meetingId: ID! + meetingType: MeetingTypeEnum! + teamId: ID! + teamMember: TeamMember! + user: User! + userId: ID! """ - The id of the user that created the meeting + The last time a meeting was updated (stage completed, finished, etc) """ - createdBy: ID! + updatedAt: DateTime! """ - The user that created the meeting - """ - createdByUser: User! - - """ - The timestamp the meeting officially ended - """ - endedAt: DateTime - - """ - The location of the facilitator in the meeting - """ - facilitatorStageId: ID! - - """ - The userId (or anonymousId) of the most recent facilitator - """ - facilitatorUserId: ID! - - """ - The facilitator team member - """ - facilitator: TeamMember! - - """ - The team members that were active during the time of the meeting - """ - meetingMembers: [RetrospectiveMeetingMember!]! - - """ - The auto-incrementing meeting number for the team - """ - meetingNumber: Int! - meetingType: MeetingTypeEnum! - - """ - The name of the meeting - """ - name: String! - - """ - The organization this meeting belongs to - """ - organization: Organization! - - """ - The phases the meeting will go through, including all phase-specific state - """ - phases: [NewMeetingPhase!]! - - """ - true if should show the org the conversion modal, else false - """ - showConversionModal: Boolean! - - """ - The time the meeting summary was emailed to the team - """ - summarySentAt: DateTime - teamId: ID! - - """ - The team that ran the meeting - """ - team: Team! - - """ - The last time a meeting was updated (stage completed, finished, etc) - """ - updatedAt: DateTime - - """ - The retrospective meeting member of the viewer - """ - viewerMeetingMember: RetrospectiveMeetingMember - - """ - the threshold used to achieve the autogroup. Useful for model tuning. Serves as a flag if autogroup was used. - """ - autoGroupThreshold: Float - - """ - The number of comments generated in the meeting - """ - commentCount: Int! - - """ - the number of votes allowed for each participant to cast on a single group - """ - maxVotesPerGroup: Int! - - """ - the next smallest distance threshold to guarantee at least 1 more grouping will be achieved - """ - nextAutoGroupThreshold: Float - - """ - The number of reflections generated in the meeting - """ - reflectionCount: Int! - - """ - a single reflection group - """ - reflectionGroup(reflectionGroupId: ID!): RetroReflectionGroup - - """ - The grouped reflections - """ - reflectionGroups(sortBy: ReflectionGroupSortEnum): [RetroReflectionGroup!]! - - """ - The number of tasks generated in the meeting - """ - taskCount: Int! - - """ - The tasks created within the meeting - """ - tasks: [Task!]! - - """ - The ID of the template used for the meeting - """ - templateId: ID! - - """ - The number of topics generated in the meeting - """ - topicCount: Int! - - """ - the total number of votes allowed for each participant - """ - totalVotes: Int! - - """ - The sum total of the votes remaining for the meeting members that are present in the meeting - """ - votesRemaining: Int! -} - -""" -sorts for the reflection group. default is sortOrder. sorting by voteCount filters out items without votes. -""" -enum ReflectionGroupSortEnum { - voteCount - stageOrder -} - -""" -All the meeting specifics for a user in a retro meeting -""" -type RetrospectiveMeetingMember implements MeetingMember { - """ - A composite of userId::meetingId - """ - id: ID! - - """ - true if present, false if absent, else null - """ - isCheckedIn: Boolean - @deprecated( - reason: "Members are checked in when they enter the meeting now & not created beforehand" - ) - meetingId: ID! - meetingType: MeetingTypeEnum! - teamId: ID! - teamMember: TeamMember! - user: User! - userId: ID! - - """ - The last time a meeting was updated (stage completed, finished, etc) - """ - updatedAt: DateTime! - - """ - The tasks assigned to members during the meeting + The tasks assigned to members during the meeting """ tasks: [Task!]! votesRemaining: Int! @@ -5352,128 +4882,6 @@ type TimelineEventPokerComplete implements TimelineEvent { meetingId: ID! } -""" -A Poker meeting -""" -type PokerMeeting implements NewMeeting { - """ - The unique meeting id. shortid. - """ - id: ID! - - """ - The timestamp the meeting was created - """ - createdAt: DateTime! - - """ - The id of the user that created the meeting - """ - createdBy: ID! - - """ - The user that created the meeting - """ - createdByUser: User! - - """ - The timestamp the meeting officially ended - """ - endedAt: DateTime - - """ - The location of the facilitator in the meeting - """ - facilitatorStageId: ID! - - """ - The userId (or anonymousId) of the most recent facilitator - """ - facilitatorUserId: ID! - - """ - The facilitator team member - """ - facilitator: TeamMember! - - """ - The team members that were active during the time of the meeting - """ - meetingMembers: [PokerMeetingMember!]! - - """ - The auto-incrementing meeting number for the team - """ - meetingNumber: Int! - meetingType: MeetingTypeEnum! - - """ - The name of the meeting - """ - name: String! - - """ - The organization this meeting belongs to - """ - organization: Organization! - - """ - The phases the meeting will go through, including all phase-specific state - """ - phases: [NewMeetingPhase!]! - - """ - true if should show the org the conversion modal, else false - """ - showConversionModal: Boolean! - - """ - The time the meeting summary was emailed to the team - """ - summarySentAt: DateTime - teamId: ID! - - """ - The team that ran the meeting - """ - team: Team! - - """ - The last time a meeting was updated (stage completed, finished, etc) - """ - updatedAt: DateTime - - """ - The Poker meeting member of the viewer - """ - viewerMeetingMember: PokerMeetingMember - - """ - The number of comments generated in the meeting - """ - commentCount: Int! - - """ - The number of stories scored during a meeting - """ - storyCount: Int! - - """ - A single story created in a Sprint Poker meeting - """ - story(storyId: ID!): Task - - """ - The ID of the template used for the meeting. Note the underlying template could have changed! - """ - templateId: ID! @deprecated(reason: "The underlying template could be mutated. Use templateRefId") - - """ - The ID of the immutable templateRef used for the meeting - """ - templateRefId: ID! -} - """ All the meeting specifics for a user in a poker meeting """ @@ -5535,147 +4943,6 @@ type ActionMeetingSettings implements TeamMeetingSettings { team: Team! } -""" -A team prompt meeting -""" -type TeamPromptMeeting implements NewMeeting { - """ - The unique meeting id. shortid. - """ - id: ID! - - """ - The meeting series id this meeting is associated with if the meeting is recurring - """ - meetingSeriesId: ID - - """ - The meeting series this meeting is associated with if the meeting is recurring - """ - meetingSeries: MeetingSeries - - """ - The timestamp the meeting is scheduled to end - """ - scheduledEndTime: DateTime - - """ - The timestamp the meeting was created - """ - createdAt: DateTime! - - """ - The id of the user that created the meeting - """ - createdBy: ID! - - """ - The user that created the meeting - """ - createdByUser: User! - - """ - The timestamp the meeting officially ended - """ - endedAt: DateTime - - """ - The location of the facilitator in the meeting - """ - facilitatorStageId: ID! - - """ - The userId (or anonymousId) of the most recent facilitator - """ - facilitatorUserId: ID! - - """ - The facilitator team member - """ - facilitator: TeamMember! - - """ - The team members that were active during the time of the meeting - """ - meetingMembers: [MeetingMember!]! - - """ - The auto-incrementing meeting number for the team - """ - meetingNumber: Int! - meetingType: MeetingTypeEnum! - - """ - The name of the meeting - """ - name: String! - - """ - The organization this meeting belongs to - """ - organization: Organization! - - """ - The phases the meeting will go through, including all phase-specific state - """ - phases: [NewMeetingPhase!]! - - """ - true if should show the org the conversion modal, else false - """ - showConversionModal: Boolean! - - """ - The time the meeting summary was emailed to the team - """ - summarySentAt: DateTime - - """ - The tasks created within the meeting - """ - tasks: [Task!]! - - """ - foreign key for team - """ - teamId: ID! - - """ - The team that ran the meeting - """ - team: Team! - - """ - The last time a meeting was updated (stage completed, finished, etc) - """ - updatedAt: DateTime - - """ - The team prompt meeting member of the viewer - """ - viewerMeetingMember: TeamPromptMeetingMember - - """ - The settings that govern the team prompt meeting - """ - settings: TeamPromptMeetingSettings! - - """ - The tasks created within the meeting - """ - responses: [TeamPromptResponse!]! - - """ - The previous meeting in the series if this meeting is recurring - """ - prevMeeting: TeamPromptMeeting - - """ - The next meeting in the series if this meeting is recurring - """ - nextMeeting: TeamPromptMeeting -} - """ All the meeting specifics for a user in a team prompt meeting """ @@ -6689,24 +5956,6 @@ type Mutation { isSpotlight: Boolean ): StartDraggingReflectionPayload - """ - Start a new meeting - """ - startCheckIn( - """ - The team starting the meeting - """ - teamId: ID - """ - The gcal input if creating a gcal event - """ - gcalInput: CreateGcalEventInput - """ - One-on-one ad-hoc team input - """ - oneOnOneTeamInput: CreateOneOnOneTeamInput - ): StartCheckInPayload! - """ Start a new meeting """ @@ -8612,18 +7861,6 @@ type StartDraggingReflectionPayload { teamId: ID } -""" -Return object for StartCheckInPayload -""" -union StartCheckInPayload = ErrorPayload | StartCheckInSuccess - -type StartCheckInSuccess { - meeting: ActionMeeting! - meetingId: ID! - team: Team! - hasGcalError: Boolean! -} - """ Return object for StartRetrospectivePayload """ diff --git a/packages/server/graphql/public/typeDefs/startCheckIn.graphql b/packages/server/graphql/public/typeDefs/startCheckIn.graphql new file mode 100644 index 00000000000..b21e52d3eb4 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/startCheckIn.graphql @@ -0,0 +1,31 @@ +""" +Return object for StartCheckInPayload +""" +union StartCheckInPayload = StartCheckInSuccess | ErrorPayload + +type StartCheckInSuccess { + meeting: ActionMeeting! + meetingId: ID! + team: Team! + hasGcalError: Boolean! +} + +extend type Mutation { + """ + Start a new meeting + """ + startCheckIn( + """ + The team starting the meeting + """ + teamId: ID + """ + The gcal input if creating a gcal event + """ + gcalInput: CreateGcalEventInput + """ + One-on-one ad-hoc team input + """ + oneOnOneTeamInput: CreateOneOnOneTeamInput + ): StartCheckInPayload! +} diff --git a/packages/server/graphql/public/types/ActionMeeting.ts b/packages/server/graphql/public/types/ActionMeeting.ts new file mode 100644 index 00000000000..daed42c627f --- /dev/null +++ b/packages/server/graphql/public/types/ActionMeeting.ts @@ -0,0 +1,45 @@ +import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' +import {getUserId} from '../../../utils/authorization' +import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' +import {ActionMeetingResolvers} from '../resolverTypes' + +const ActionMeeting: ActionMeetingResolvers = { + agendaItem: async ({id: meetingId}, {agendaItemId}, {dataLoader}) => { + const agendaItem = await dataLoader.get('agendaItems').load(agendaItemId) + if (agendaItem.meetingId !== meetingId) return null + return agendaItem + }, + agendaItemCount: async ({agendaItemCount}) => { + // only populated after the meeting has been completed (not killed) + return agendaItemCount || 0 + }, + agendaItems: async ({id: meetingId}, _args: unknown, {dataLoader}) => { + return await dataLoader.get('agendaItemsByMeetingId').load(meetingId) + }, + commentCount: async ({commentCount}) => { + // only populated after the meeting has been completed (not killed) + return commentCount || 0 + }, + meetingMembers: ({id: meetingId}, _args, {dataLoader}) => { + return dataLoader.get('meetingMembersByMeetingId').load(meetingId) + }, + taskCount: async ({taskCount}) => { + // only populated after the meeting has been completed (not killed) + return taskCount || 0 + }, + tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + const meeting = await dataLoader.get('newMeetings').load(meetingId) + const {teamId} = meeting + const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) + return filterTasksByMeeting(teamTasks, meetingId, viewerId) + }, + viewerMeetingMember: async ({id: meetingId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + const meetingMemberId = toTeamMemberId(meetingId, viewerId) + const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) + return meetingMember || null + } +} + +export default ActionMeeting diff --git a/packages/server/graphql/public/types/AgendaItem.ts b/packages/server/graphql/public/types/AgendaItem.ts new file mode 100644 index 00000000000..685ad8f290b --- /dev/null +++ b/packages/server/graphql/public/types/AgendaItem.ts @@ -0,0 +1,10 @@ +import {AgendaItemResolvers} from '../resolverTypes' + +const AgendaItem: AgendaItemResolvers = { + isActive: ({isActive}) => !!isActive, + teamMember: async ({teamMemberId}, _args: unknown, {dataLoader}) => { + return dataLoader.get('teamMembers').load(teamMemberId) + } +} + +export default AgendaItem diff --git a/packages/server/graphql/public/types/NewMeeting.ts b/packages/server/graphql/public/types/NewMeeting.ts new file mode 100644 index 00000000000..51af97fa768 --- /dev/null +++ b/packages/server/graphql/public/types/NewMeeting.ts @@ -0,0 +1,69 @@ +import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' +import {getUserId} from '../../../utils/authorization' +import isMeetingLocked from '../../types/helpers/isMeetingLocked' +import {NewMeetingResolvers} from '../resolverTypes' + +const NewMeeting: NewMeetingResolvers = { + __resolveType: ({meetingType}) => { + const resolveTypeLookup = { + retrospective: 'RetrospectiveMeeting', + action: 'ActionMeeting', + poker: 'PokerMeeting', + teamPrompt: 'TeamPromptMeeting' + } as const + return resolveTypeLookup[meetingType as keyof typeof resolveTypeLookup] + }, + createdByUser: ({createdBy}, _args, {dataLoader}) => { + return dataLoader.get('users').loadNonNull(createdBy) + }, + facilitator: ({facilitatorUserId, teamId}, _args, {dataLoader}) => { + const teamMemberId = toTeamMemberId(teamId, facilitatorUserId) + return dataLoader.get('teamMembers').load(teamMemberId) + }, + locked: async ({endedAt, teamId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return isMeetingLocked(viewerId, teamId, endedAt, dataLoader) + }, + + meetingMembers: ({id: meetingId}, _args, {dataLoader}) => { + return dataLoader.get('meetingMembersByMeetingId').load(meetingId) + }, + organization: async ({teamId}, _args, {dataLoader}) => { + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {orgId} = team + return dataLoader.get('organizations').load(orgId) + }, + phases: async ({phases, id: meetingId, teamId, endedAt}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + const locked = await isMeetingLocked(viewerId, teamId, endedAt, dataLoader) + + const resolvedPhases = phases.map((phase: any) => ({ + ...phase, + meetingId, + teamId + })) + + if (locked) { + // make all stages non-navigable so even if the user removes the overlay they cannot see all meeting data + return resolvedPhases.map((phase: any) => ({ + ...phase, + stages: phase.stages.map((stage: any) => ({ + ...stage, + isNavigable: false, + isNavigableByFacilitator: false + })) + })) + } + return resolvedPhases + }, + showConversionModal: ({showConversionModal}) => !!showConversionModal, + team: ({teamId}, _args, {dataLoader}) => dataLoader.get('teams').loadNonNull(teamId), + viewerMeetingMember: async ({id: meetingId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + const meetingMemberId = toTeamMemberId(meetingId, viewerId) + const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) + return meetingMember || null + } +} + +export default NewMeeting diff --git a/packages/server/graphql/public/types/RetrospectiveMeeting.ts b/packages/server/graphql/public/types/RetrospectiveMeeting.ts new file mode 100644 index 00000000000..b259c84409d --- /dev/null +++ b/packages/server/graphql/public/types/RetrospectiveMeeting.ts @@ -0,0 +1,86 @@ +import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' +import RetroMeetingMember from '../../../database/types/RetroMeetingMember' +import {getUserId} from '../../../utils/authorization' +import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' +import getPhase from '../../../utils/getPhase' +import {GQLContext} from '../../graphql' +import {resolveForSU} from '../../resolvers' +import {RetrospectiveMeetingResolvers} from '../resolverTypes' +import ReflectionGroupType from '../../../database/types/ReflectionGroup' + +const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { + autoGroupThreshold: resolveForSU('autoGroupThreshold'), + commentCount: ({commentCount}) => commentCount || 0, + disableAnonymity: ({disableAnonymity}) => disableAnonymity ?? false, + meetingMembers: ({id: meetingId}, _args: unknown, {dataLoader}) => { + return dataLoader.get('meetingMembersByMeetingId').load(meetingId) as Promise< + RetroMeetingMember[] + > + }, + reflectionCount: ({reflectionCount}) => reflectionCount || 0, + reflectionGroup: async ({id: meetingId}, {reflectionGroupId}, {dataLoader}) => { + const reflectionGroup = await dataLoader.get('retroReflectionGroups').load(reflectionGroupId) + if (reflectionGroup.meetingId !== meetingId) return null + return reflectionGroup + }, + reflectionGroups: async ({id: meetingId}, {sortBy}, {dataLoader}) => { + const reflectionGroups = await dataLoader + .get('retroReflectionGroupsByMeetingId') + .load(meetingId) + if (sortBy === 'voteCount') { + reflectionGroups.sort((a: ReflectionGroupType, b: ReflectionGroupType) => + a.voterIds.length < b.voterIds.length ? 1 : -1 + ) + return reflectionGroups + } else if (sortBy === 'stageOrder') { + const meeting = await dataLoader.get('newMeetings').load(meetingId) + const {phases} = meeting + const discussPhase = getPhase(phases, 'discuss') + if (!discussPhase) return reflectionGroups + const {stages} = discussPhase + // for early terminations the stages may not exist + const sortLookup = {} as {[reflectionGroupId: string]: number} + reflectionGroups.forEach((group: ReflectionGroupType) => { + const idx = stages.findIndex((stage) => stage.reflectionGroupId === group.id) + sortLookup[group.id] = idx + }) + reflectionGroups.sort((a: ReflectionGroupType, b: ReflectionGroupType) => { + return sortLookup[a.id]! < sortLookup[b.id]! ? -1 : 1 + }) + return reflectionGroups + } + reflectionGroups.sort((a: ReflectionGroupType, b: ReflectionGroupType) => + a.sortOrder < b.sortOrder ? -1 : 1 + ) + return reflectionGroups + }, + taskCount: ({taskCount}) => taskCount || 0, + tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + const meeting = await dataLoader.get('newMeetings').load(meetingId) + const {teamId} = meeting + const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) + return filterTasksByMeeting(teamTasks, meetingId, viewerId) + }, + topicCount: ({topicCount}) => topicCount || 0, + votesRemaining: async ({id: meetingId}, _args: unknown, {dataLoader}) => { + const meetingMembers = (await dataLoader + .get('meetingMembersByMeetingId') + .load(meetingId)) as RetroMeetingMember[] + return meetingMembers.reduce((sum, member) => sum + member.votesRemaining, 0) + }, + viewerMeetingMember: async ( + {id: meetingId}, + _args: unknown, + {authToken, dataLoader}: GQLContext + ) => { + const viewerId = getUserId(authToken) + const meetingMemberId = toTeamMemberId(meetingId, viewerId) + const meetingMember = (await dataLoader + .get('meetingMembers') + .load(meetingMemberId)) as RetroMeetingMember + return meetingMember || null + } +} + +export default RetrospectiveMeeting diff --git a/packages/server/graphql/public/types/StartCheckInSuccess.ts b/packages/server/graphql/public/types/StartCheckInSuccess.ts new file mode 100644 index 00000000000..0dedaf9905c --- /dev/null +++ b/packages/server/graphql/public/types/StartCheckInSuccess.ts @@ -0,0 +1,18 @@ +import MeetingAction from '../../../database/types/MeetingAction' +import {StartCheckInSuccessResolvers} from '../resolverTypes' + +export type StartCheckInSuccessSource = { + meetingId: string + teamId: string +} + +const StartCheckInSuccess: StartCheckInSuccessResolvers = { + meeting: ({meetingId}, _args, {dataLoader}) => { + return dataLoader.get('newMeetings').load(meetingId) as Promise + }, + team: ({teamId}, _args, {dataLoader}) => { + return dataLoader.get('teams').loadNonNull(teamId) + } +} + +export default StartCheckInSuccess diff --git a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts new file mode 100644 index 00000000000..ecd1b772f89 --- /dev/null +++ b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts @@ -0,0 +1,18 @@ +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {StartRetrospectiveSuccessResolvers} from '../resolverTypes' + +export type StartRetrospectiveSuccessSource = { + meetingId: string + teamId: string +} + +const StartRetrospectiveSuccess: StartRetrospectiveSuccessResolvers = { + meeting: ({meetingId}, _args: unknown, {dataLoader}) => { + return dataLoader.get('newMeetings').load(meetingId) as Promise + }, + team: ({teamId}, _args: unknown, {dataLoader}) => { + return dataLoader.get('teams').loadNonNull(teamId) + } +} + +export default StartRetrospectiveSuccess diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 31c6ac81af5..e82930a04ca 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -99,9 +99,7 @@ import setSlackNotification from './mutations/setSlackNotification' import setStageTimer from './mutations/setStageTimer' import setTaskEstimate from './mutations/setTaskEstimate' import setTaskHighlight from './mutations/setTaskHighlight' -import startCheckIn from './mutations/startCheckIn' import startDraggingReflection from './mutations/startDraggingReflection' -import startRetrospective from './mutations/startRetrospective' import startSprintPoker from './mutations/startSprintPoker' import toggleTeamDrawer from './mutations/toggleTeamDrawer' import updateAgendaItem from './mutations/updateAgendaItem' @@ -211,8 +209,6 @@ export default new GraphQLObjectType({ setStageTimer, setSlackNotification, startDraggingReflection, - startCheckIn, - startRetrospective, startSprintPoker, setTaskHighlight, updateAgendaItem, diff --git a/packages/server/graphql/rootTypes.ts b/packages/server/graphql/rootTypes.ts index 4138d846d9d..e0bfb77c349 100644 --- a/packages/server/graphql/rootTypes.ts +++ b/packages/server/graphql/rootTypes.ts @@ -1,4 +1,3 @@ -import ActionMeeting from './types/ActionMeeting' import ActionMeetingMember from './types/ActionMeetingMember' import ActionMeetingSettings from './types/ActionMeetingSettings' import AgendaItemsPhase from './types/AgendaItemsPhase' @@ -17,7 +16,6 @@ import JiraDimensionField from './types/JiraDimensionField' import PokerMeetingSettings from './types/PokerMeetingSettings' import ReflectPhase from './types/ReflectPhase' import RenamePokerTemplatePayload from './types/RenamePokerTemplatePayload' -import RetrospectiveMeeting from './types/RetrospectiveMeeting' import RetrospectiveMeetingMember from './types/RetrospectiveMeetingMember' import RetrospectiveMeetingSettings from './types/RetrospectiveMeetingSettings' import SetMeetingSettingsPayload from './types/SetMeetingSettingsPayload' @@ -26,7 +24,6 @@ import SuggestedActionInviteYourTeam from './types/SuggestedActionInviteYourTeam import SuggestedActionTryActionMeeting from './types/SuggestedActionTryActionMeeting' import SuggestedActionTryRetroMeeting from './types/SuggestedActionTryRetroMeeting' import SuggestedActionTryTheDemo from './types/SuggestedActionTryTheDemo' -import TeamPromptMeeting from './types/TeamPromptMeeting' import TeamPromptMeetingMember from './types/TeamPromptMeetingMember' import TeamPromptMeetingSettings from './types/TeamPromptMeetingSettings' import TeamPromptResponsesPhase from './types/TeamPromptResponsesPhase' @@ -53,10 +50,8 @@ const rootTypes = [ TeamPromptResponsesPhase, GenericMeetingPhase, EstimatePhase, - ActionMeeting, ActionMeetingMember, PokerMeetingSettings, - RetrospectiveMeeting, RetrospectiveMeetingMember, RetrospectiveMeetingSettings, SetMeetingSettingsPayload, @@ -71,7 +66,6 @@ const rootTypes = [ TimelineEventCompletedActionMeeting, TimelineEventPokerComplete, ActionMeetingSettings, - TeamPromptMeeting, TeamPromptMeetingMember, TeamPromptMeetingSettings, Comment, diff --git a/packages/server/graphql/types/ActionMeeting.ts b/packages/server/graphql/types/ActionMeeting.ts index 55f396caa4f..c47be79950b 100644 --- a/packages/server/graphql/types/ActionMeeting.ts +++ b/packages/server/graphql/types/ActionMeeting.ts @@ -1,93 +1,12 @@ -import {GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import {getUserId} from '../../utils/authorization' -import filterTasksByMeeting from '../../utils/filterTasksByMeeting' +import {GraphQLObjectType} from 'graphql' import {GQLContext} from '../graphql' -import ActionMeetingMember from './ActionMeetingMember' -import AgendaItem from './AgendaItem' -import NewMeeting, {newMeetingFields} from './NewMeeting' -import Task from './Task' +import NewMeeting from './NewMeeting' const ActionMeeting = new GraphQLObjectType({ name: 'ActionMeeting', interfaces: () => [NewMeeting], description: 'An action meeting', - fields: () => ({ - ...newMeetingFields(), - agendaItem: { - type: AgendaItem, - description: 'A single agenda item', - args: { - agendaItemId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve: async ({id: meetingId}, {agendaItemId}, {dataLoader}) => { - const agendaItem = await dataLoader.get('agendaItems').load(agendaItemId) - if (agendaItem.meetingId !== meetingId) return null - return agendaItem - } - }, - agendaItemCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of agenda items generated in the meeting', - resolve: async ({agendaItemCount}) => { - // only populated after the meeting has been completed (not killed) - return agendaItemCount || 0 - } - }, - agendaItems: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AgendaItem))), - description: 'All of the agenda items for the meeting', - resolve: async ({id: meetingId}, _args: unknown, {dataLoader}) => { - return await dataLoader.get('agendaItemsByMeetingId').load(meetingId) - } - }, - commentCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of comments generated in the meeting', - resolve: async ({commentCount}) => { - // only populated after the meeting has been completed (not killed) - return commentCount || 0 - } - }, - meetingMembers: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ActionMeetingMember))), - description: 'The team members that were active during the time of the meeting', - resolve: ({id: meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('meetingMembersByMeetingId').load(meetingId) - } - }, - taskCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of tasks generated in the meeting', - resolve: async ({taskCount}) => { - // only populated after the meeting has been completed (not killed) - return taskCount || 0 - } - }, - tasks: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Task))), - description: 'The tasks created within the meeting', - resolve: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { - const viewerId = getUserId(authToken) - const meeting = await dataLoader.get('newMeetings').load(meetingId) - const {teamId} = meeting - const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) - return filterTasksByMeeting(teamTasks, meetingId, viewerId) - } - }, - viewerMeetingMember: { - type: ActionMeetingMember, - description: 'The action meeting member of the viewer', - resolve: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}: GQLContext) => { - const viewerId = getUserId(authToken) - const meetingMemberId = toTeamMemberId(meetingId, viewerId) - const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) - return meetingMember || null - } - } - }) + fields: () => ({}) }) export default ActionMeeting diff --git a/packages/server/graphql/types/AgendaItem.ts b/packages/server/graphql/types/AgendaItem.ts index 5cfc5151fdf..7946d50c26f 100644 --- a/packages/server/graphql/types/AgendaItem.ts +++ b/packages/server/graphql/types/AgendaItem.ts @@ -1,80 +1,15 @@ -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' +import {GraphQLNonNull, GraphQLObjectType} from 'graphql' import {GQLContext} from '../graphql' -import CommentorDetails from './CommentorDetails' -import GraphQLISO8601Type from './GraphQLISO8601Type' import TeamMember from './TeamMember' const AgendaItem = new GraphQLObjectType({ name: 'AgendaItem', description: 'A request placeholder that will likely turn into 1 or more tasks', fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'The unique agenda item id teamId::shortid' - }, - commentors: { - type: new GraphQLList(new GraphQLNonNull(CommentorDetails)), - deprecationReason: 'Moved to ThreadConnection. Can remove Jun-01-2021', - description: 'A list of users currently commenting', - resolve: ({commentors = []}) => { - return commentors - } - }, - content: { - type: new GraphQLNonNull(GraphQLString), - description: 'The body of the agenda item' - }, - createdAt: { - type: GraphQLISO8601Type, - description: 'The timestamp the agenda item was created' - }, - isActive: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the agenda item has not been processed or deleted', - resolve: ({isActive}) => !!isActive - }, - pinned: { - type: GraphQLBoolean, - description: 'True if the agenda item has been pinned' - }, - pinnedParentId: { - type: GraphQLID, - description: 'If pinned, this is the unique id of the original agenda item' - }, - sortOrder: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'The sort order of the agenda item in the list' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: '*The team for this agenda item' - }, - teamMemberId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The teamMemberId that created this agenda item' - }, - meetingId: { - type: GraphQLID, - description: 'The meetingId of the agenda item' - }, - updatedAt: { - type: GraphQLISO8601Type, - description: 'The timestamp the agenda item was updated' - }, + // ideally all fields would be moved to codegen, but something brakes in merging schemes if this particular field is removed. No need to investigate further, we can get rid of of this whole object once it's no longer referenced by other legacy graphql definitions teamMember: { type: new GraphQLNonNull(TeamMember), - description: 'The team member that created the agenda item', - resolve: async ({teamMemberId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('teamMembers').load(teamMemberId) - } + description: 'The team member that created the agenda item' } }) }) diff --git a/packages/server/graphql/types/AutogroupReflectionGroup.ts b/packages/server/graphql/types/AutogroupReflectionGroup.ts deleted file mode 100644 index 5d630f45a24..00000000000 --- a/packages/server/graphql/types/AutogroupReflectionGroup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' - -const AutogroupReflectionGroup = new GraphQLObjectType({ - name: 'AutogroupReflectionGroup', - description: 'A suggested reflection group created by OpenAI', - fields: () => ({ - groupTitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'The smart title for the reflection group created by OpenAI' - }, - reflectionIds: { - type: new GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLID))), - description: 'The ids of the reflections in the group' - } - }) -}) - -export default AutogroupReflectionGroup diff --git a/packages/server/graphql/types/NewMeeting.ts b/packages/server/graphql/types/NewMeeting.ts index af7a677b960..d17613fe501 100644 --- a/packages/server/graphql/types/NewMeeting.ts +++ b/packages/server/graphql/types/NewMeeting.ts @@ -1,213 +1,15 @@ -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInt, - GraphQLInterfaceType, - GraphQLList, - GraphQLNonNull, - GraphQLString -} from 'graphql' -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' +import {GraphQLInterfaceType} from 'graphql' import {MeetingTypeEnum as MeetingTypeEnumType} from '../../postgres/types/Meeting' -import {getUserId} from '../../utils/authorization' -import {GQLContext} from '../graphql' -import {resolveTeam} from '../resolvers' import ActionMeeting from './ActionMeeting' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import isMeetingLocked from './helpers/isMeetingLocked' -import MeetingMember from './MeetingMember' -import MeetingTypeEnum from './MeetingTypeEnum' -import NewMeetingPhase from './NewMeetingPhase' -import Organization from './Organization' import PokerMeeting from './PokerMeeting' import RetrospectiveMeeting from './RetrospectiveMeeting' -import Team from './Team' -import TeamMember from './TeamMember' import TeamPromptMeeting from './TeamPromptMeeting' -export const newMeetingFields = () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'The unique meeting id. shortid.' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the meeting was created' - }, - createdBy: { - type: new GraphQLNonNull(GraphQLID), - description: 'The id of the user that created the meeting' - }, - createdByUser: { - type: new GraphQLNonNull(require('./User').default), - description: 'The user that created the meeting', - resolve: ({createdBy}: {createdBy: string}, _args: any, {dataLoader}: GQLContext) => { - return dataLoader.get('users').load(createdBy) - } - }, - endedAt: { - type: GraphQLISO8601Type, - description: 'The timestamp the meeting officially ended' - }, - meetingSeriesId: { - type: GraphQLID, - description: 'The id of the meeting series this meeting belongs to' - }, - scheduledEndTime: { - type: GraphQLISO8601Type, - description: 'If meeting has a meeting series associated, this is the time the meeting will end' - }, - facilitatorStageId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The location of the facilitator in the meeting' - }, - facilitatorUserId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The userId (or anonymousId) of the most recent facilitator' - }, - facilitator: { - type: new GraphQLNonNull(TeamMember), - description: 'The facilitator team member', - resolve: ( - {facilitatorUserId, teamId}: {facilitatorUserId: string; teamId: string}, - _args: any, - {dataLoader}: GQLContext - ) => { - const teamMemberId = toTeamMemberId(teamId, facilitatorUserId) - return dataLoader.get('teamMembers').load(teamMemberId) - } - }, - meetingMembers: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(MeetingMember))), - description: 'The team members that were active during the time of the meeting', - resolve: ({id: meetingId}: {id: string}, _args: any, {dataLoader}: GQLContext) => { - return dataLoader.get('meetingMembersByMeetingId').load(meetingId) - } - }, - meetingNumber: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The auto-incrementing meeting number for the team' - }, - meetingType: { - type: new GraphQLNonNull(MeetingTypeEnum) - }, - name: { - type: new GraphQLNonNull(GraphQLString), - description: 'The name of the meeting' - }, - organization: { - type: new GraphQLNonNull(Organization), - description: 'The organization this meeting belongs to', - resolve: async ({teamId}: {teamId: string}, _args: any, {dataLoader}: GQLContext) => { - const team = await dataLoader.get('teams').loadNonNull(teamId) - const {orgId} = team - return dataLoader.get('organizations').load(orgId) - } - }, - phases: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(NewMeetingPhase))), - description: 'The phases the meeting will go through, including all phase-specific state', - resolve: async ( - { - phases, - id: meetingId, - teamId, - endedAt - }: { - phases: any - id: string - teamId: string - endedAt?: Date | null - }, - _args: unknown, - {authToken, dataLoader}: GQLContext - ) => { - const viewerId = getUserId(authToken) - const locked = await isMeetingLocked(viewerId, teamId, endedAt, dataLoader) - - const resolvedPhases = phases.map((phase: any) => ({ - ...phase, - meetingId, - teamId - })) - - if (locked) { - // make all stages non-navigable so even if the user removes the overlay they cannot see all meeting data - return resolvedPhases.map((phase: any) => ({ - ...phase, - stages: phase.stages.map((stage: any) => ({ - ...stage, - isNavigable: false, - isNavigableByFacilitator: false - })) - })) - } - return resolvedPhases - } - }, - showConversionModal: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if should show the org the conversion modal, else false', - resolve: ({showConversionModal}: {showConversionModal: boolean}) => !!showConversionModal - }, - summary: { - type: GraphQLString, - description: `The OpenAI generated summary of all the content in the meeting, such as reflections, tasks, and comments. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` - }, - sentimentScore: { - type: GraphQLFloat, - description: `The overall sentiment score (range from -1.0 to 1.0) for the meeting. Undefined if the user doesnt have access to the feature or it's unavailable in this meeting type` - }, - summarySentAt: { - type: GraphQLISO8601Type, - description: 'The time the meeting summary was emailed to the team' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'foreign key for team' - }, - team: { - type: new GraphQLNonNull(Team), - description: 'The team that ran the meeting', - resolve: resolveTeam - }, - updatedAt: { - type: GraphQLISO8601Type, - description: 'The last time a meeting was updated (stage completed, finished, etc)' - }, - viewerMeetingMember: { - type: MeetingMember, - description: 'The meeting member of the viewer', - resolve: async ( - {id: meetingId}: {id: string}, - _args: any, - {authToken, dataLoader}: GQLContext - ) => { - const viewerId = getUserId(authToken) - const meetingMemberId = toTeamMemberId(meetingId, viewerId) - const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) - return meetingMember || null - } - }, - locked: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'Is this locked for starter plans?', - resolve: async ( - {endedAt, teamId}: {endedAt?: Date | null; teamId: string}, - _args: any, - {authToken, dataLoader}: GQLContext - ) => { - const viewerId = getUserId(authToken) - return isMeetingLocked(viewerId, teamId, endedAt, dataLoader) - } - } -}) - +// scaffolding until all types using this are migrated to codegen const NewMeeting: GraphQLInterfaceType = new GraphQLInterfaceType({ name: 'NewMeeting', description: 'A team meeting history for all previous meetings', - fields: newMeetingFields, + fields: () => ({}), resolveType: ({meetingType}: {meetingType: MeetingTypeEnumType}) => { const resolveTypeLookup = { retrospective: RetrospectiveMeeting, diff --git a/packages/server/graphql/types/PokerMeeting.ts b/packages/server/graphql/types/PokerMeeting.ts index f43b24c5c2c..f9262f8a919 100644 --- a/packages/server/graphql/types/PokerMeeting.ts +++ b/packages/server/graphql/types/PokerMeeting.ts @@ -2,7 +2,7 @@ import {GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType} f import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {getUserId} from '../../utils/authorization' import {GQLContext} from '../graphql' -import NewMeeting, {newMeetingFields} from './NewMeeting' +import NewMeeting from './NewMeeting' import PokerMeetingMember from './PokerMeetingMember' import Task from './Task' @@ -11,7 +11,6 @@ const PokerMeeting = new GraphQLObjectType({ interfaces: () => [NewMeeting], description: 'A Poker meeting', fields: () => ({ - ...newMeetingFields(), commentCount: { type: new GraphQLNonNull(GraphQLInt), description: 'The number of comments generated in the meeting', diff --git a/packages/server/graphql/types/RetrospectiveMeeting.ts b/packages/server/graphql/types/RetrospectiveMeeting.ts index a4d41fb6648..eced643d85b 100644 --- a/packages/server/graphql/types/RetrospectiveMeeting.ts +++ b/packages/server/graphql/types/RetrospectiveMeeting.ts @@ -1,39 +1,8 @@ -import { - GraphQLBoolean, - GraphQLEnumType, - GraphQLFloat, - GraphQLID, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import ReflectionGroupType from '../../database/types/ReflectionGroup' -import RetroMeetingMember from '../../database/types/RetroMeetingMember' -import {getUserId} from '../../utils/authorization' -import filterTasksByMeeting from '../../utils/filterTasksByMeeting' -import getPhase from '../../utils/getPhase' +import {GraphQLObjectType} from 'graphql' import {GQLContext} from '../graphql' -import {resolveForSU} from '../resolvers' -import NewMeeting, {newMeetingFields} from './NewMeeting' -import RetroReflectionGroup from './RetroReflectionGroup' -import RetrospectiveMeetingMember from './RetrospectiveMeetingMember' -import Task from './Task' -import AutogroupReflectionGroup from './AutogroupReflectionGroup' -import TranscriptBlock from './TranscriptBlock' - -const ReflectionGroupSortEnum = new GraphQLEnumType({ - name: 'ReflectionGroupSortEnum', - description: - 'sorts for the reflection group. default is sortOrder. sorting by voteCount filters out items without votes.', - values: { - voteCount: {}, - stageOrder: {} - } -}) +import NewMeeting from './NewMeeting' +// scaffolding until all types using this are migrated to codegen const RetrospectiveMeeting: GraphQLObjectType = new GraphQLObjectType< any, GQLContext @@ -41,171 +10,7 @@ const RetrospectiveMeeting: GraphQLObjectType = new GraphQLObje name: 'RetrospectiveMeeting', interfaces: () => [NewMeeting], description: 'A retrospective meeting', - fields: () => ({ - ...newMeetingFields(), - autoGroupThreshold: { - type: GraphQLFloat, - description: - 'the threshold used to achieve the autogroup. Useful for model tuning. Serves as a flag if autogroup was used.', - resolve: resolveForSU('autoGroupThreshold') - }, - commentCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of comments generated in the meeting', - resolve: ({commentCount}) => commentCount || 0 - }, - autogroupReflectionGroups: { - type: new GraphQLList(new GraphQLNonNull(AutogroupReflectionGroup)), - description: 'The suggested reflection groups created by OpenAI' - }, - resetReflectionGroups: { - type: new GraphQLList(new GraphQLNonNull(AutogroupReflectionGroup)), - description: 'The groups that existed before the autogrouping' - }, - maxVotesPerGroup: { - type: new GraphQLNonNull(GraphQLInt), - description: 'the number of votes allowed for each participant to cast on a single group' - }, - disableAnonymity: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'Disables anonymity of reflections', - resolve: ({disableAnonymity}) => disableAnonymity ?? false - }, - meetingMembers: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(RetrospectiveMeetingMember))), - description: 'The team members that were active during the time of the meeting', - resolve: ({id: meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('meetingMembersByMeetingId').load(meetingId) - } - }, - nextAutoGroupThreshold: { - type: GraphQLFloat, - description: - 'the next smallest distance threshold to guarantee at least 1 more grouping will be achieved' - }, - reflectionCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of reflections generated in the meeting', - resolve: ({reflectionCount}) => reflectionCount || 0 - }, - reflectionGroup: { - type: RetroReflectionGroup, - description: 'a single reflection group', - args: { - reflectionGroupId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve: async ({id: meetingId}, {reflectionGroupId}, {dataLoader}) => { - const reflectionGroup = await dataLoader - .get('retroReflectionGroups') - .load(reflectionGroupId) - if (reflectionGroup.meetingId !== meetingId) return null - return reflectionGroup - } - }, - reflectionGroups: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(RetroReflectionGroup))), - description: 'The grouped reflections', - args: { - sortBy: { - type: ReflectionGroupSortEnum - } - }, - resolve: async ({id: meetingId}, {sortBy}, {dataLoader}) => { - const reflectionGroups = await dataLoader - .get('retroReflectionGroupsByMeetingId') - .load(meetingId) - if (sortBy === 'voteCount') { - reflectionGroups.sort((a: ReflectionGroupType, b: ReflectionGroupType) => - a.voterIds.length < b.voterIds.length ? 1 : -1 - ) - return reflectionGroups - } else if (sortBy === 'stageOrder') { - const meeting = await dataLoader.get('newMeetings').load(meetingId) - const {phases} = meeting - const discussPhase = getPhase(phases, 'discuss') - if (!discussPhase) return reflectionGroups - const {stages} = discussPhase - // for early terminations the stages may not exist - const sortLookup = {} as {[reflectionGroupId: string]: number} - reflectionGroups.forEach((group: ReflectionGroupType) => { - const idx = stages.findIndex((stage) => stage.reflectionGroupId === group.id) - sortLookup[group.id] = idx - }) - reflectionGroups.sort((a: ReflectionGroupType, b: ReflectionGroupType) => { - return sortLookup[a.id]! < sortLookup[b.id]! ? -1 : 1 - }) - return reflectionGroups - } - reflectionGroups.sort((a: ReflectionGroupType, b: ReflectionGroupType) => - a.sortOrder < b.sortOrder ? -1 : 1 - ) - return reflectionGroups - } - }, - taskCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of tasks generated in the meeting', - resolve: ({taskCount}) => taskCount || 0 - }, - tasks: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Task))), - description: 'The tasks created within the meeting', - resolve: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { - const viewerId = getUserId(authToken) - const meeting = await dataLoader.get('newMeetings').load(meetingId) - const {teamId} = meeting - const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) - return filterTasksByMeeting(teamTasks, meetingId, viewerId) - } - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - }, - templateId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The ID of the template used for the meeting' - }, - topicCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of topics generated in the meeting', - resolve: ({topicCount}) => topicCount || 0 - }, - totalVotes: { - type: new GraphQLNonNull(GraphQLInt), - description: 'the total number of votes allowed for each participant' - }, - transcription: { - type: GraphQLList(TranscriptBlock), - description: 'The transcription of the meeting' - }, - votesRemaining: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The sum total of the votes remaining for the meeting members that are present in the meeting', - resolve: async ({id: meetingId}, _args: unknown, {dataLoader}) => { - const meetingMembers = (await dataLoader - .get('meetingMembersByMeetingId') - .load(meetingId)) as RetroMeetingMember[] - return meetingMembers.reduce((sum, member) => sum + member.votesRemaining, 0) - } - }, - viewerMeetingMember: { - type: RetrospectiveMeetingMember, - description: 'The retrospective meeting member of the viewer', - resolve: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}: GQLContext) => { - const viewerId = getUserId(authToken) - const meetingMemberId = toTeamMemberId(meetingId, viewerId) - const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) - return meetingMember || null - } - }, - videoMeetingURL: { - type: GraphQLString, - description: 'The Zoom meeting URL for the meeting' - } - }) + fields: {} }) export default RetrospectiveMeeting diff --git a/packages/server/graphql/types/StartCheckInPayload.ts b/packages/server/graphql/types/StartCheckInPayload.ts deleted file mode 100644 index 17b3d3656fc..00000000000 --- a/packages/server/graphql/types/StartCheckInPayload.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {GraphQLBoolean, GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ActionMeeting from './ActionMeeting' -import makeMutationPayload from './makeMutationPayload' -import Team from './Team' - -export const StartCheckInSuccess = new GraphQLObjectType({ - name: 'StartCheckInSuccess', - fields: () => ({ - meeting: { - type: new GraphQLNonNull(ActionMeeting), - resolve: ({meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) - } - }, - meetingId: { - type: new GraphQLNonNull(GraphQLID) - }, - team: { - type: new GraphQLNonNull(Team), - resolve: ({teamId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('teams').load(teamId) - } - }, - hasGcalError: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'True if there was an error creating the Google Calendar event. False if there was no error or no gcalInput was provided.' - } - }) -}) - -const StartCheckInPayload = makeMutationPayload('StartCheckInPayload', StartCheckInSuccess) - -export default StartCheckInPayload diff --git a/packages/server/graphql/types/StartRetrospectivePayload.ts b/packages/server/graphql/types/StartRetrospectivePayload.ts deleted file mode 100644 index 833479dc755..00000000000 --- a/packages/server/graphql/types/StartRetrospectivePayload.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {GraphQLBoolean, GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import makeMutationPayload from './makeMutationPayload' -import RetrospectiveMeeting from './RetrospectiveMeeting' -import Team from './Team' - -export const StartRetrospectiveSuccess = new GraphQLObjectType({ - name: 'StartRetrospectiveSuccess', - fields: () => ({ - meeting: { - type: new GraphQLNonNull(RetrospectiveMeeting), - resolve: ({meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) - } - }, - meetingId: { - type: new GraphQLNonNull(GraphQLID) - }, - team: { - type: new GraphQLNonNull(Team), - resolve: ({teamId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('teams').load(teamId) - } - }, - hasGcalError: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'True if there was an error creating the Google Calendar event. False if there was no error or no gcalInput was provided.' - } - }) -}) - -const StartRetrospectivePayload = makeMutationPayload( - 'StartRetrospectivePayload', - StartRetrospectiveSuccess -) - -export default StartRetrospectivePayload diff --git a/packages/server/graphql/types/TeamPromptMeeting.ts b/packages/server/graphql/types/TeamPromptMeeting.ts index 7c9206d43f8..daa192811b5 100644 --- a/packages/server/graphql/types/TeamPromptMeeting.ts +++ b/packages/server/graphql/types/TeamPromptMeeting.ts @@ -5,7 +5,7 @@ import {getTeamPromptResponsesByMeetingId} from '../../postgres/queries/getTeamP import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import {GQLContext} from '../graphql' -import NewMeeting, {newMeetingFields} from './NewMeeting' +import NewMeeting from './NewMeeting' import TeamPromptMeetingMember from './TeamPromptMeetingMember' import TeamPromptMeetingSettings from './TeamPromptMeetingSettings' import TeamPromptResponse from './TeamPromptResponse' @@ -15,7 +15,6 @@ const TeamPromptMeeting = new GraphQLObjectType({ interfaces: () => [NewMeeting], description: 'A team prompt meeting', fields: () => ({ - ...newMeetingFields(), meetingPrompt: { type: new GraphQLNonNull(GraphQLString), description: 'The name of the meeting' diff --git a/packages/server/graphql/types/TranscriptBlock.ts b/packages/server/graphql/types/TranscriptBlock.ts deleted file mode 100644 index 18150128c92..00000000000 --- a/packages/server/graphql/types/TranscriptBlock.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' - -const TranscriptBlock = new GraphQLObjectType({ - name: 'TranscriptBlock', - description: 'A block of the meeting transcription with a speaker and text', - fields: () => ({ - speaker: { - type: new GraphQLNonNull(GraphQLString), - description: 'The speaker who said the words' - }, - words: { - type: new GraphQLNonNull(GraphQLString), - description: 'The words that the speaker said' - } - }) -}) - -export default TranscriptBlock From aca16c0b3ddcd567e4308eb26c1d651a527837f8 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 8 Jan 2024 11:24:53 +0100 Subject: [PATCH 2/3] Cleanup --- packages/server/graphql/types/ActionMeeting.ts | 3 ++- packages/server/graphql/types/NewMeeting.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/graphql/types/ActionMeeting.ts b/packages/server/graphql/types/ActionMeeting.ts index c47be79950b..6e56f5f272f 100644 --- a/packages/server/graphql/types/ActionMeeting.ts +++ b/packages/server/graphql/types/ActionMeeting.ts @@ -2,11 +2,12 @@ import {GraphQLObjectType} from 'graphql' import {GQLContext} from '../graphql' import NewMeeting from './NewMeeting' +// Placeholder so legacy types can still reference it until all types using this are migrated to codegen const ActionMeeting = new GraphQLObjectType({ name: 'ActionMeeting', interfaces: () => [NewMeeting], description: 'An action meeting', - fields: () => ({}) + fields: {} }) export default ActionMeeting diff --git a/packages/server/graphql/types/NewMeeting.ts b/packages/server/graphql/types/NewMeeting.ts index d17613fe501..3b633351cc0 100644 --- a/packages/server/graphql/types/NewMeeting.ts +++ b/packages/server/graphql/types/NewMeeting.ts @@ -9,7 +9,7 @@ import TeamPromptMeeting from './TeamPromptMeeting' const NewMeeting: GraphQLInterfaceType = new GraphQLInterfaceType({ name: 'NewMeeting', description: 'A team meeting history for all previous meetings', - fields: () => ({}), + fields: {}, resolveType: ({meetingType}: {meetingType: MeetingTypeEnumType}) => { const resolveTypeLookup = { retrospective: RetrospectiveMeeting, From 75089a9c35486951c42d1772b466e8f725650ec6 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Wed, 10 Jan 2024 15:20:39 +0100 Subject: [PATCH 3/3] Cleanup --- packages/server/graphql/public/types/NewMeeting.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/graphql/public/types/NewMeeting.ts b/packages/server/graphql/public/types/NewMeeting.ts index 51af97fa768..f8fc9998aaa 100644 --- a/packages/server/graphql/public/types/NewMeeting.ts +++ b/packages/server/graphql/public/types/NewMeeting.ts @@ -37,7 +37,7 @@ const NewMeeting: NewMeetingResolvers = { const viewerId = getUserId(authToken) const locked = await isMeetingLocked(viewerId, teamId, endedAt, dataLoader) - const resolvedPhases = phases.map((phase: any) => ({ + const resolvedPhases = phases.map((phase) => ({ ...phase, meetingId, teamId @@ -45,9 +45,9 @@ const NewMeeting: NewMeetingResolvers = { if (locked) { // make all stages non-navigable so even if the user removes the overlay they cannot see all meeting data - return resolvedPhases.map((phase: any) => ({ + return resolvedPhases.map((phase) => ({ ...phase, - stages: phase.stages.map((stage: any) => ({ + stages: phase.stages.map((stage) => ({ ...stage, isNavigable: false, isNavigableByFacilitator: false