diff --git a/packages/client/hooks/useMeetingMemberAvatars.ts b/packages/client/hooks/useMeetingMemberAvatars.ts index 57be6fe00a2..1a4d81136fa 100644 --- a/packages/client/hooks/useMeetingMemberAvatars.ts +++ b/packages/client/hooks/useMeetingMemberAvatars.ts @@ -30,7 +30,7 @@ const useMeetingMemberAvatars = (meetingRef: useMeetingMemberAvatars_meeting$key return meetingMembers .map(({user}) => user) .filter((user) => { - return user.lastSeenAtURLs?.includes(`/meet/${meetingId}`) && user.isConnected + return user?.lastSeenAtURLs?.includes(`/meet/${meetingId}`) && user?.isConnected }) .sort((a, b) => (a.id === viewerId ? -1 : a.lastSeenAt < b.lastSeenAt ? -1 : 1)) }, [meetingMembers]) diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index e1e7c1d4fee..8088ee60491 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -32,14 +32,27 @@ const removeTeamMember = async ( // see if they were a leader, make a new guy leader so later we can reassign tasks const activeTeamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) const teamMember = activeTeamMembers.find((t) => t.id === teamMemberId) - const {isLead, isNotRemoved} = teamMember ?? {} - // if the guy being removed is the leader & not the last, pick a new one. else, use him - const teamLeader = activeTeamMembers.find((t) => t.isLead === !isLead) || teamMember - if (!isNotRemoved || !teamMember || !teamLeader) { - throw new Error('Team member already removed') + if (!teamMember) { + return { + user: undefined, + notificationId: undefined, + archivedTaskIds: [] as string[], + reassignedTaskIds: [] as string[] + } } + const currentTeamLeader = activeTeamMembers.find((t) => t.isLead)! + if (!currentTeamLeader) { + throw new Error('Team lead does not exist') + } + + const {isLead} = teamMember + const willArchive = activeTeamMembers.length === 1 + const nextTeamLead = + isLead && !willArchive + ? activeTeamMembers.find((teamMember) => teamMember.id !== teamMemberId)! + : currentTeamLeader - if (activeTeamMembers.length === 1) { + if (willArchive) { await Promise.all([ // archive single-person teams pg.updateTable('Team').set({isArchived: true}).where('id', '=', teamId).execute(), @@ -51,7 +64,7 @@ const removeTeamMember = async ( await pg .updateTable('TeamMember') .set(({not}) => ({isLead: not('isLead')})) - .where('id', 'in', [teamMemberId, teamLeader.id]) + .where('id', 'in', [teamMemberId, nextTeamLead.id]) .execute() } @@ -82,7 +95,7 @@ const removeTeamMember = async ( ) .update( { - userId: teamLeader.userId + userId: nextTeamLead.userId }, {returnChanges: true} )('changes')('new_val') diff --git a/packages/server/graphql/mutations/promoteToTeamLead.ts b/packages/server/graphql/mutations/promoteToTeamLead.ts index 83cd3f9b4ca..2da1f08fddc 100644 --- a/packages/server/graphql/mutations/promoteToTeamLead.ts +++ b/packages/server/graphql/mutations/promoteToTeamLead.ts @@ -36,7 +36,10 @@ export default { dataLoader.get('teamMembersByTeamId').load(teamId), dataLoader.get('teams').loadNonNull(teamId) ]) - const oldLead = teamMembers.find(({isLead}) => isLead)! + const oldLead = teamMembers.find(({isLead}) => isLead) + if (!oldLead) { + return standardError(new Error('Team has no team lead'), {userId: viewerId}) + } const {id: oldLeadTeamMemberId} = oldLead if (!isSuperUser(authToken)) { const isOrgAdmin = await isUserOrgAdmin(viewerId, team.orgId, dataLoader) diff --git a/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts b/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts index e67a1728c9f..f1d20f32d18 100644 --- a/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts +++ b/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts @@ -106,7 +106,7 @@ const acceptRequestToJoinDomain: MutationResolvers['acceptRequestToJoinDomain'] email, openDrawer: 'manageTeam' }) - .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true})) + .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false})) .execute() ]) diff --git a/packages/server/postgres/migrations/1724174924811_oneTeamLead.ts b/packages/server/postgres/migrations/1724174924811_oneTeamLead.ts new file mode 100644 index 00000000000..c0d0da60ebf --- /dev/null +++ b/packages/server/postgres/migrations/1724174924811_oneTeamLead.ts @@ -0,0 +1,47 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + const teamsWithout1Leader = await pg + .selectFrom('TeamMember') + .select('teamId') + .select(({fn}) => fn.count('id').as('count')) + .where('isNotRemoved', '=', true) + .groupBy('teamId') + .having(sql`SUM(CASE WHEN "isLead" = true THEN 1 ELSE 0 END) != 1`) + .execute() + + await Promise.all( + teamsWithout1Leader.map(async (row) => { + const {teamId} = row + // remove all leads for the cases where we had more than 1 + await pg.updateTable('TeamMember').set({isLead: false}).where('teamId', '=', teamId).execute() + await pg + .with('NextLead', (qb) => + qb + .selectFrom('TeamMember') + .select('id') + .where('teamId', '=', teamId) + .where('isNotRemoved', '=', true) + .orderBy('createdAt', 'asc') + .limit(1) + ) + .updateTable('TeamMember') + .set({isLead: true}) + .where(({eb, selectFrom}) => eb('id', '=', selectFrom('NextLead').select('id'))) + .returning('id') + .execute() + }) + ) +} + +export async function down() { + // noop +} diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index c512b6bef49..ec91003e8c2 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -103,7 +103,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data email, openDrawer: 'manageTeam' }) - .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true})) + .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false})) .execute(), r .table('TeamInvitation')