From e6434e181a864b2e61428f55a98994fb1137ac8f Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 25 Mar 2024 19:17:13 +0000 Subject: [PATCH] feat: add functionality to change templates during a retro (#9544) --- codegen.json | 11 ++-- packages/client/components/RetroDrawer.tsx | 14 ++++- .../components/RetroDrawerTemplateCard.tsx | 29 +++++++++-- .../UpdateMeetingTemplateMutation.ts | 51 +++++++++++++++++++ .../subscriptions/MeetingSubscription.ts | 3 ++ .../public/mutations/updateMeetingTemplate.ts | 49 ++++++++++++++++++ .../public/typeDefs/Subscriptions.graphql | 1 + .../typeDefs/updateMeetingTemplate.graphql | 27 ++++++++++ .../types/UpdateMeetingTemplateSuccess.ts | 14 +++++ 9 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 packages/client/mutations/UpdateMeetingTemplateMutation.ts create mode 100644 packages/server/graphql/public/mutations/updateMeetingTemplate.ts create mode 100644 packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql create mode 100644 packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts diff --git a/codegen.json b/codegen.json index a0a38ad29f0..83f9993f249 100644 --- a/codegen.json +++ b/codegen.json @@ -49,6 +49,7 @@ "ActionMeeting": "../../database/types/MeetingAction#default", "ActionMeetingMember": "../../database/types/ActionMeetingMember#default as ActionMeetingMemberDB", "AddApprovedOrganizationDomainsSuccess": "./types/AddApprovedOrganizationDomainsSuccess#AddApprovedOrganizationDomainsSuccessSource", + "AddReactjiToReactableSuccess": "./types/AddReactjiToReactableSuccess#AddReactjiToReactableSuccessSource", "AddReflectTemplateSuccess": "./types/AddReflectTemplateSuccess#AddReflectTemplateSuccessSource", "AddPokerTemplateSuccess": "./types/AddPokerTemplateSuccess#AddPokerTemplateSuccessSource", "AddTranscriptionBotSuccess": "./types/AddTranscriptionBotSuccess#AddTranscriptionBotSuccessSource", @@ -74,8 +75,8 @@ "InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource", "JiraIssue": "./types/JiraIssue#JiraIssueSource", "JiraRemoteProject": "../types/JiraRemoteProject#JiraRemoteProjectSource", - "MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries", "Kudos": "../../postgres/types/Kudos#Kudos", + "MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries", "MeetingTemplate": "../../database/types/MeetingTemplate#default", "NewMeeting": "../../postgres/types/Meeting#AnyMeeting", "NewMeetingPhase": "../../database/types/GenericMeetingPhase #default as GenericMeetingPhaseDB", @@ -83,11 +84,11 @@ "NotificationTeamInvitation": "../../database/types/NotificationTeamInvitation#default as NotificationTeamInvitationDB", "NotifyDiscussionMentioned": "../../database/types/NotificationDiscussionMentioned#default as NotificationDiscussionMentionedDB", "NotifyKickedOut": "../../database/types/NotificationKickedOut#default", + "NotifyMentioned": "../../database/types/NotificationMentioned#default as NotificationMentionedDB", "NotifyPaymentRejected": "../../database/types/NotificationPaymentRejected#default", "NotifyPromoteToOrgLeader": "../../database/types/NotificationPromoteToBillingLeader#default", "NotifyRequestToJoinOrg": "../../database/types/NotificationRequestToJoinOrg#default", "NotifyResponseMentioned": "../../database/types/NotificationResponseMentioned#default as NotificationResponseMentionedDB", - "NotifyMentioned": "../../database/types/NotificationMentioned#default as NotificationMentionedDB", "NotifyResponseReplied": "../../database/types/NotifyResponseReplied#default as NotifyResponseRepliedDB", "NotifyTaskInvolves": "../../database/types/NotificationTaskInvolves#default", "NotifyTeamArchived": "../../database/types/NotificationTeamArchived#default", @@ -96,6 +97,7 @@ "PokerMeeting": "../../database/types/MeetingPoker#default as MeetingPoker", "PokerMeetingMember": "../../database/types/MeetingPokerMeetingMember#default as PokerMeetingMemberDB", "RRule": "rrule#RRule", + "Reactable": "../../database/types/Reactable#Reactable", "ReflectPrompt": "../../database/types/RetrospectivePrompt#default", "ReflectTemplate": "../../database/types/ReflectTemplate#default", "RemoveApprovedOrganizationDomainsSuccess": "./types/RemoveApprovedOrganizationDomainsSuccess#RemoveApprovedOrganizationDomainsSuccessSource", @@ -103,11 +105,10 @@ "RemoveTeamMemberPayload": "./types/RemoveTeamMemberPayload#RemoveTeamMemberPayloadSource", "RequestToJoinDomainSuccess": "./types/RequestToJoinDomainSuccess#RequestToJoinDomainSuccessSource", "ResetReflectionGroupsSuccess": "./types/ResetReflectionGroupsSuccess#ResetReflectionGroupsSuccessSource", - "RetroReflectionGroup": "../../database/types/RetroReflectionGroup#default as RetroReflectionGroupDB", "RetroReflection": "../../database/types/RetroReflection#default as RetroReflectionDB", + "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", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", @@ -141,13 +142,13 @@ "UpdateMeetingPromptSuccess": "./types/UpdateMeetingPromptSuccess#UpdateMeetingPromptSuccessSource", "UpdateOrgPayload": "./types/UpdateOrgPayload#UpdateOrgPayloadSource", "UpdateRecurrenceSettingsSuccess": "./types/UpdateRecurrenceSettingsSuccess#UpdateRecurrenceSettingsSuccessSource", + "UpdateMeetingTemplateSuccess": "./types/UpdateMeetingTemplateSuccess#UpdateMeetingTemplateSuccessSource", "UpdateTaskPayload": "./types/UpdateTaskPayload#UpdateTaskPayloadSource", "UpdateTemplateCategorySuccess": "./types/UpdateTemplateCategorySuccess#UpdateTemplateCategorySuccessSource", "UpdateUserProfilePayload": "./types/UpdateUserProfilePayload#UpdateUserProfilePayloadSource", "UpdatedNotification": "./types/AddedNotification#UpdatedNotificationSource", "UpgradeToTeamTierSuccess": "./types/UpgradeToTeamTierSuccess#UpgradeToTeamTierSuccessSource", "UpsertTeamPromptResponseSuccess": "./types/UpsertTeamPromptResponseSuccess#UpsertTeamPromptResponseSuccessSource", - "AddReactjiToReactableSuccess": "./types/AddReactjiToReactableSuccess#AddReactjiToReactableSuccessSource", "User": "../../postgres/types/IUser#default as IUser", "UserLogInPayload": "./types/UserLogInPayload#UserLogInPayloadSource" } diff --git a/packages/client/components/RetroDrawer.tsx b/packages/client/components/RetroDrawer.tsx index 8d6404ada0e..6249386cabf 100644 --- a/packages/client/components/RetroDrawer.tsx +++ b/packages/client/components/RetroDrawer.tsx @@ -24,6 +24,7 @@ const RetroDrawer = (props: Props) => { viewer { meeting(meetingId: $meetingId) { ... on RetrospectiveMeeting { + id reflectionGroups { id } @@ -60,9 +61,13 @@ const RetroDrawer = (props: Props) => { setShowDrawer(!showDrawer) } + const handleCloseDrawer = () => { + setShowDrawer(false) + } + useEffect(() => { if (hasReflections && showDrawer) { - setShowDrawer(false) + handleCloseDrawer() } }, [hasReflections]) @@ -97,7 +102,12 @@ const RetroDrawer = (props: Props) => { {templates.map((template) => ( - + ))} diff --git a/packages/client/components/RetroDrawerTemplateCard.tsx b/packages/client/components/RetroDrawerTemplateCard.tsx index c126f6d0d6c..73fa7c30e3e 100644 --- a/packages/client/components/RetroDrawerTemplateCard.tsx +++ b/packages/client/components/RetroDrawerTemplateCard.tsx @@ -7,17 +7,25 @@ import {ActivityLibraryCard} from './ActivityLibrary/ActivityLibraryCard' import {ActivityCardImage} from './ActivityLibrary/ActivityCard' import {RetroDrawerTemplateCard_template$key} from '~/__generated__/RetroDrawerTemplateCard_template.graphql' import {CategoryID, CATEGORY_THEMES} from '././ActivityLibrary/Categories' +import UpdateMeetingTemplateMutation from '../mutations/UpdateMeetingTemplateMutation' +import useMutationProps from '../hooks/useMutationProps' +import useAtmosphere from '../hooks/useAtmosphere' interface Props { templateRef: RetroDrawerTemplateCard_template$key + meetingId: string + handleCloseDrawer: () => void } const RetroDrawerTemplateCard = (props: Props) => { - const {templateRef} = props + const {templateRef, meetingId, handleCloseDrawer} = props + const {onError, onCompleted} = useMutationProps() + const atmosphere = useAtmosphere() const template = useFragment( graphql` fragment RetroDrawerTemplateCard_template on MeetingTemplate { ...ActivityLibraryCardDescription_template + id name category illustrationUrl @@ -27,12 +35,25 @@ const RetroDrawerTemplateCard = (props: Props) => { templateRef ) + const handleClick = () => { + UpdateMeetingTemplateMutation( + atmosphere, + { + meetingId: meetingId, + templateId: template.id + }, + {onError, onCompleted} + ) + handleCloseDrawer() + } + return ( -
+
Premium @@ -40,16 +61,16 @@ const RetroDrawerTemplateCard = (props: Props) => { } > -
+ ) } export default RetroDrawerTemplateCard diff --git a/packages/client/mutations/UpdateMeetingTemplateMutation.ts b/packages/client/mutations/UpdateMeetingTemplateMutation.ts new file mode 100644 index 00000000000..c4dd1d6951b --- /dev/null +++ b/packages/client/mutations/UpdateMeetingTemplateMutation.ts @@ -0,0 +1,51 @@ +import graphql from 'babel-plugin-relay/macro' +import {commitMutation} from 'react-relay' +import {StandardMutation} from '../types/relayMutations' +import {UpdateMeetingTemplateMutation as TUpdateMeetingTemplateMutation} from '../__generated__/UpdateMeetingTemplateMutation.graphql' + +graphql` + fragment UpdateMeetingTemplateMutation_meeting on UpdateMeetingTemplateSuccess { + meeting { + ... on RetrospectiveMeeting { + id + templateId + phases { + id + ... on ReflectPhase { + reflectPrompts { + id + } + } + } + } + } + } +` + +const mutation = graphql` + mutation UpdateMeetingTemplateMutation($meetingId: ID!, $templateId: ID!) { + updateMeetingTemplate(meetingId: $meetingId, templateId: $templateId) { + ... on ErrorPayload { + error { + message + } + } + ...UpdateMeetingTemplateMutation_meeting @relay(mask: false) + } + } +` + +const UpdateMeetingTemplateMutation: StandardMutation = ( + atmosphere, + variables, + {onError, onCompleted} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + onCompleted, + onError + }) +} + +export default UpdateMeetingTemplateMutation diff --git a/packages/client/subscriptions/MeetingSubscription.ts b/packages/client/subscriptions/MeetingSubscription.ts index 65e3978b03c..d6f30e0a0f1 100644 --- a/packages/client/subscriptions/MeetingSubscription.ts +++ b/packages/client/subscriptions/MeetingSubscription.ts @@ -150,6 +150,9 @@ const subscription = graphql` UpdateRetroMaxVotesSuccess { ...UpdateRetroMaxVotesMutation_meeting @relay(mask: false) } + UpdateMeetingTemplateSuccess { + ...UpdateMeetingTemplateMutation_meeting @relay(mask: false) + } VoteForReflectionGroupPayload { ...VoteForReflectionGroupMutation_meeting @relay(mask: false) } diff --git a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts new file mode 100644 index 00000000000..a0be265f8de --- /dev/null +++ b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts @@ -0,0 +1,49 @@ +import {SubscriptionChannel} from '../../../../client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import getPhase from '../../../utils/getPhase' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async ( + _source, + {meetingId, templateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const r = await getRethink() + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (!isTeamMember(authToken, meeting.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + const reflections = await dataLoader.get('retroReflectionsByMeetingId').load(meetingId) + if (reflections.length > 0) { + return standardError(new Error('Cannot change template after reflections have been created'), { + userId: viewerId + }) + } + const reflectPhase = getPhase(meeting.phases, 'reflect') + const hasCompletedReflectPhase = reflectPhase.stages.every((stage) => stage.isComplete) + if (hasCompletedReflectPhase) { + return standardError( + new Error('Cannot change template after reflection phase has been completed'), + { + userId: viewerId + } + ) + } + + await r.table('NewMeeting').get(meetingId).update({templateId}).run() + meeting.templateId = templateId + + const data = {meetingId, templateId} + publish(SubscriptionChannel.MEETING, meetingId, 'UpdateMeetingTemplateSuccess', data, subOptions) + return data +} + +export default updateMeetingTemplate diff --git a/packages/server/graphql/public/typeDefs/Subscriptions.graphql b/packages/server/graphql/public/typeDefs/Subscriptions.graphql index f3391104caf..3f2e1a48de5 100644 --- a/packages/server/graphql/public/typeDefs/Subscriptions.graphql +++ b/packages/server/graphql/public/typeDefs/Subscriptions.graphql @@ -54,6 +54,7 @@ type MeetingSubscriptionPayload { SetPokerSpectateSuccess: SetPokerSpectateSuccess SetTaskEstimateSuccess: SetTaskEstimateSuccess UpsertTeamPromptResponseSuccess: UpsertTeamPromptResponseSuccess + UpdateMeetingTemplateSuccess: UpdateMeetingTemplateSuccess } type NotificationSubscriptionPayload { diff --git a/packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql b/packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql new file mode 100644 index 00000000000..1f378e42dbe --- /dev/null +++ b/packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql @@ -0,0 +1,27 @@ +extend type Mutation { + """ + Update a meeting template + """ + updateMeetingTemplate( + """ + The id of the meeting + """ + meetingId: ID! + """ + The id of the meeting template + """ + templateId: ID! + ): UpdateMeetingTemplatePayload! +} + +""" +Return value for updateMeetingTemplate, which could be an error +""" +union UpdateMeetingTemplatePayload = ErrorPayload | UpdateMeetingTemplateSuccess + +type UpdateMeetingTemplateSuccess { + """ + The updated meeting + """ + meeting: NewMeeting! +} diff --git a/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts b/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts new file mode 100644 index 00000000000..33a52658893 --- /dev/null +++ b/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts @@ -0,0 +1,14 @@ +import {UpdateMeetingTemplateSuccessResolvers} from '../resolverTypes' + +export type UpdateMeetingTemplateSuccessSource = { + meetingId: string +} + +const UpdateMeetingTemplateSuccess: UpdateMeetingTemplateSuccessResolvers = { + meeting: async ({meetingId}, _args, {dataLoader}) => { + const meeting = await dataLoader.get('newMeetings').load(meetingId) + return meeting + } +} + +export default UpdateMeetingTemplateSuccess