diff --git a/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx b/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx index f338fe6a7a0..5733d314b27 100644 --- a/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx +++ b/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx @@ -55,7 +55,6 @@ const AgendaListAndInput = (props: Props) => { agendaItems { id content - sortOrder ...AgendaList_agendaItems } } diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 98146663a5d..51e21b4ca0b 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -1,6 +1,7 @@ import getKysely from '../postgres/getKysely' import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPromptResponsesByMeetingIds' import { + selectAgendaItems, selectOrganizations, selectRetroReflections, selectSuggestedAction, @@ -170,3 +171,23 @@ export const teamPromptResponsesByMeetingId = foreignKeyLoaderMaker( 'meetingId', getTeamPromptResponsesByMeetingIds ) + +export const _pgagendaItemsByTeamId = foreignKeyLoaderMaker( + '_pgagendaItems', + 'teamId', + async (teamIds) => { + return selectAgendaItems() + .where('teamId', 'in', teamIds) + .where('isActive', '=', true) + .orderBy('sortOrder') + .execute() + } +) + +export const _pgagendaItemsByMeetingId = foreignKeyLoaderMaker( + '_pgagendaItems', + 'meetingId', + async (meetingIds) => { + return selectAgendaItems().where('meetingId', 'in', meetingIds).orderBy('sortOrder').execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 0aa989dd98a..0849187482d 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -6,6 +6,7 @@ import getMeetingTemplatesByIds from '../postgres/queries/getMeetingTemplatesByI import getTemplateRefsByIds from '../postgres/queries/getTemplateRefsByIds' import {getUsersByIds} from '../postgres/queries/getUsersByIds' import { + selectAgendaItems, selectMeetingSettings, selectOrganizations, selectRetroReflections, @@ -90,3 +91,7 @@ export const suggestedActions = primaryKeyLoaderMaker((ids: readonly string[]) = export const meetingSettings = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectMeetingSettings().where('id', 'in', ids).execute() }) + +export const _pgagendaItems = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectAgendaItems().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/addAgendaItem.ts b/packages/server/graphql/mutations/addAgendaItem.ts index c9200d89215..c6fc4425fc0 100644 --- a/packages/server/graphql/mutations/addAgendaItem.ts +++ b/packages/server/graphql/mutations/addAgendaItem.ts @@ -1,9 +1,11 @@ import {GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import makeAgendaItemSchema from 'parabol-client/validation/makeAgendaItemSchema' +import {positionAfter} from '../../../client/shared/sortOrder' import getRethink from '../../database/rethinkDriver' import AgendaItem, {AgendaItemInput} from '../../database/types/AgendaItem' import generateUID from '../../generateUID' +import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -45,18 +47,33 @@ export default { } // RESOLUTION + const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const lastAgendaItem = teamAgendaItems.at(-1) + const lastSortOrder = lastAgendaItem?.sortOrder ? String(lastAgendaItem.sortOrder) : '' + // this is just during the migration of AgendaItem table + const sortOrder = positionAfter(lastSortOrder) const agendaItemId = `${teamId}::${generateUID()}` await r .table('AgendaItem') .insert( new AgendaItem({ ...validNewAgendaItem, - id: agendaItemId, teamId } as AgendaItemInput) ) .run() - + await getKysely() + .insertInto('AgendaItem') + .values({ + id: agendaItemId, + content: newAgendaItem.content, + meetingId: newAgendaItem.meetingId, + pinned: newAgendaItem.pinned, + sortOrder, + teamId, + teamMemberId: newAgendaItem.teamMemberId + }) + .execute() const meetingId = await addAgendaItemToActiveActionMeeting(agendaItemId, teamId, dataLoader) analytics.addedAgendaItem(viewer, teamId, meetingId) const data = {agendaItemId, meetingId} diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index f009f8347d3..c5bfbca7a28 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {AGENDA_ITEMS, DONE, LAST_CALL} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' +import {positionAfter} from '../../../client/shared/sortOrder' import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' import getRethink from '../../database/rethinkDriver' import {RDatum} from '../../database/stricterR' @@ -10,6 +11,7 @@ import AgendaItem from '../../database/types/AgendaItem' import MeetingAction from '../../database/types/MeetingAction' import Task from '../../database/types/Task' import TimelineEventCheckinComplete from '../../database/types/TimelineEventCheckinComplete' +import {DataLoaderInstance} from '../../dataloader/RootDataLoader' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import archiveTasksForDB from '../../safeMutations/archiveTasksForDB' @@ -61,8 +63,14 @@ const updateTaskSortOrders = async (userIds: string[], tasks: SortOrderTask[]) = return tasks } -const clearAgendaItems = async (teamId: string) => { +const clearAgendaItems = async (teamId: string, dataLoader: DataLoaderInstance) => { + await getKysely() + .updateTable('AgendaItem') + .set({isActive: false}) + .where('teamId', '=', teamId) + .execute() const r = await getRethink() + dataLoader.clearAll('agendaItems') return r .table('AgendaItem') .getAll(teamId, {index: 'teamId'}) @@ -72,16 +80,15 @@ const clearAgendaItems = async (teamId: string) => { .run() } -const getPinnedAgendaItems = async (teamId: string) => { - const r = await getRethink() - return r - .table('AgendaItem') - .getAll(teamId, {index: 'teamId'}) - .filter({isActive: true, pinned: true}) - .run() +const getPinnedAgendaItems = async (teamId: string, dataLoader: DataLoaderInstance) => { + const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + return agendaItems.filter((agendaItem) => agendaItem.pinned) } -const clonePinnedAgendaItems = async (pinnedAgendaItems: AgendaItem[]) => { +const clonePinnedAgendaItems = async ( + pinnedAgendaItems: AgendaItem[], + dataLoader: DataLoaderInstance +) => { const r = await getRethink() const clonedPins = pinnedAgendaItems.map((agendaItem) => { const agendaItemId = `${agendaItem.teamId}::${generateUID()}` @@ -96,6 +103,17 @@ const clonePinnedAgendaItems = async (pinnedAgendaItems: AgendaItem[]) => { }) }) await r.table('AgendaItem').insert(clonedPins).run() + let curSortOrder = '' + const pgClonedPins = clonedPins.map((agendaItems) => { + const sortOrder = positionAfter(curSortOrder) + curSortOrder = sortOrder + return { + ...agendaItems, + sortOrder + } + }) + await getKysely().insertInto('AgendaItem').values(pgClonedPins).execute() + dataLoader.clearAll('agendaItems') } const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataLoaderWorker) => { @@ -120,7 +138,7 @@ const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataL .filter({status: DONE}) .filter((task: RDatum) => task('tags').contains('archived').not()) .run(), - r.table('AgendaItem').getAll(teamId, {index: 'teamId'}).filter({isActive: true}).run() + dataLoader.get('agendaItemsByTeamId').load(teamId) ]) const agendaItemPhase = getPhase(phases, 'agendaitems') @@ -128,12 +146,12 @@ const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataL const discussionIds = stages.map((stage) => stage.discussionId) const userIds = meetingMembers.map(({userId}) => userId) const meetingPhase = getMeetingPhase(phases) - const pinnedAgendaItems = await getPinnedAgendaItems(teamId) + const pinnedAgendaItems = await getPinnedAgendaItems(teamId, dataLoader) const isKill = !!(meetingPhase && ![AGENDA_ITEMS, LAST_CALL].includes(meetingPhase.phaseType)) - if (!isKill) await clearAgendaItems(teamId) + if (!isKill) await clearAgendaItems(teamId, dataLoader) await Promise.all([ isKill ? undefined : archiveTasksForDB(doneTasks, meetingId), - isKill ? undefined : clonePinnedAgendaItems(pinnedAgendaItems), + isKill ? undefined : clonePinnedAgendaItems(pinnedAgendaItems, dataLoader), updateTaskSortOrders(userIds, tasks), r .table('NewMeeting') diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index dbd1859dd78..db01b387b0d 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -1,6 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import MeetingAction from '../../../database/types/MeetingAction' +import getKysely from '../../../postgres/getKysely' import insertDiscussions from '../../../postgres/queries/insertDiscussions' import getPhase from '../../../utils/getPhase' import {DataLoaderWorker} from '../../graphql' @@ -47,6 +48,7 @@ const addAgendaItemToActiveActionMeeting = async ( }) .run(), r.table('AgendaItem').get(agendaItemId).update({meetingId: meetingId}).run(), + getKysely().updateTable('AgendaItem').set({meetingId}).where('id', '=', agendaItemId).execute(), insertDiscussions([ { id: discussionId, diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index d9b92314e63..e1e7c1d4fee 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -105,13 +105,11 @@ const removeTeamMember = async ( const archivedTasks = await archiveTasksForDB(integratedTasksToArchive) const archivedTaskIds = archivedTasks.map(({id}) => id) - const agendaItemIds = await r - .table('AgendaItem') - .getAll(teamId, {index: 'teamId'}) - .filter((row: RDatum) => row('teamMemberId').eq(teamMemberId)) - .getField('id') - .run() - + const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const agendaItemIds = teamAgendaItems + .filter((agendaItem) => agendaItem.teamMemberId === teamMemberId) + .map(({id}) => id) + dataLoader.clearAll('agendaItems') // if a new meeting was currently running, remove them from it const filterFn = (stage: CheckInStage | UpdatesStage | EstimateStage | AgendaItemsStage) => (stage as CheckInStage | UpdatesStage).teamMemberId === teamMemberId || diff --git a/packages/server/graphql/mutations/removeAgendaItem.ts b/packages/server/graphql/mutations/removeAgendaItem.ts index 4cf87cb57bd..941c0f9dc30 100644 --- a/packages/server/graphql/mutations/removeAgendaItem.ts +++ b/packages/server/graphql/mutations/removeAgendaItem.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import AgendaItemsStage from '../../database/types/AgendaItemsStage' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -42,6 +43,12 @@ export default { .update({isActive: false}, {returnChanges: true})('changes')(0)('old_val') .default(null) .run() + await getKysely() + .updateTable('AgendaItem') + .set({isActive: false}) + .where('id', '=', agendaItemId) + .returning('id') + .execute() if (!agendaItem) { return standardError(new Error('Agenda item not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/mutations/updateAgendaItem.ts b/packages/server/graphql/mutations/updateAgendaItem.ts index 6a9c688ce5d..46d08998dbc 100644 --- a/packages/server/graphql/mutations/updateAgendaItem.ts +++ b/packages/server/graphql/mutations/updateAgendaItem.ts @@ -1,8 +1,10 @@ import {GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import makeUpdateAgendaItemSchema from 'parabol-client/validation/makeUpdateAgendaItemSchema' +import {getSortOrder} from '../../../client/shared/sortOrder' import getRethink from '../../database/rethinkDriver' import AgendaItemsStage from '../../database/types/AgendaItemsStage' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -27,6 +29,7 @@ export default { ) { const now = new Date() const r = await getRethink() + const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -49,6 +52,8 @@ export default { } // RESOLUTION + const oldAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const fromIdx = oldAgendaItems.findIndex((agendaItem) => agendaItem.id === id) await r .table('AgendaItem') .get(id) @@ -57,6 +62,25 @@ export default { updatedAt: now }) .run() + dataLoader.clearAll('agendaItems') + if (doc.sortOrder !== null && doc.sortOrder !== undefined) { + const nextAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const pgagendaItems = await dataLoader.get('_pgagendaItemsByTeamId').load(teamId) + const toIdx = nextAgendaItems.findIndex((agendaItem) => agendaItem.id === id) + const pgSortOrder = getSortOrder(pgagendaItems, fromIdx, toIdx) + await pg + .updateTable('AgendaItem') + .set({sortOrder: pgSortOrder}) + .where('id', '=', id) + .execute() + } else { + await pg + .updateTable('AgendaItem') + .set({pinned: doc.pinned, content: doc.content}) + .where('id', '=', id) + .execute() + } + const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const actionMeeting = activeMeetings.find( (activeMeeting) => activeMeeting.meetingType === 'action' diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index c411c3fcd9d..a832930b505 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -3,6 +3,7 @@ import getRethink from '../../../database/rethinkDriver' import ActionMeetingMember from '../../../database/types/ActionMeetingMember' import MeetingAction from '../../../database/types/MeetingAction' import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' import {analytics} from '../../../utils/analytics/analytics' @@ -89,7 +90,12 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( .insert(new ActionMeetingMember({meetingId, userId: viewerId, teamId})) .run(), updateTeamByTeamId(updates, teamId), - r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run() + r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run(), + getKysely() + .updateTable('AgendaItem') + .set({meetingId}) + .where('id', 'in', agendaItemIds) + .execute() ]) IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting) diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 77559cab3ce..9a6df8e0d30 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -203,3 +203,5 @@ export const selectMeetingSettings = () => fn('to_json', ['jiraSearchQueries']).as('jiraSearchQueries'), fn('to_json', ['phaseTypes']).as('phaseTypes') ]) + +export const selectAgendaItems = () => getKysely().selectFrom('AgendaItem').selectAll()