Skip to content

Commit c8632b1

Browse files
committed
feat: support pairing sessions in review info shortcut
1 parent cd0b688 commit c8632b1

2 files changed

Lines changed: 124 additions & 9 deletions

File tree

src/bot/__tests__/getReviewInfo.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { App } from '@slack/bolt';
22
import { getReviewInfo } from '@bot/getReviewInfo';
33
import { buildMockGlobalShortcutParam, buildMockWebClient } from '@utils/slackMocks';
4-
import { CandidateType, Deadline, Interaction } from '@bot/enums';
4+
import { CandidateType, Deadline, Interaction, InterviewFormat } from '@bot/enums';
55
import { GlobalShortcutParam } from '@/slackTypes';
66
import { activeReviewRepo } from '@repos/activeReviewsRepo';
77
import { ActiveReview } from '@models/ActiveReview';
88
import { userRepo } from '@repos/userRepo';
99
import { reviewActionService } from '@/services/ReviewActionService';
1010
import { CreatedReviewAction } from '@/services/models/ReviewAction';
11+
import { pairingSessionsRepo } from '@repos/pairingSessionsRepo';
12+
import { PairingSession } from '@models/PairingSession';
1113

1214
describe('getReviewInfo', () => {
1315
let app: App;
@@ -56,6 +58,7 @@ describe('getReviewInfo', () => {
5658
yardstickUrl: '',
5759
};
5860
activeReviewRepo.getReviewByThreadIdOrFail = jest.fn().mockResolvedValue(review);
61+
pairingSessionsRepo.getByThreadIdOrUndefined = jest.fn().mockResolvedValue(undefined);
5962
userRepo.listAll = jest.fn().mockResolvedValue([]);
6063
reviewActionService.getActions = jest
6164
.fn()
@@ -67,6 +70,54 @@ describe('getReviewInfo', () => {
6770
expect(param.ack).toHaveBeenCalled();
6871
});
6972

73+
describe('when the message is a pairing session', () => {
74+
let pairingParam: GlobalShortcutParam;
75+
const session: PairingSession = {
76+
threadId: '123',
77+
requestorId: 'U123',
78+
candidateName: 'Dana',
79+
languages: ['Python', 'Go'],
80+
format: InterviewFormat.REMOTE,
81+
requestedAt: new Date(1650504468906),
82+
teammatesNeededCount: 2,
83+
slots: [
84+
{
85+
id: 'slot-1',
86+
date: '2026-04-20',
87+
startTime: '09:00',
88+
endTime: '10:00',
89+
interestedTeammates: [
90+
{ userId: 'U456', acceptedAt: 1650504468906, formats: [InterviewFormat.REMOTE] },
91+
],
92+
},
93+
],
94+
pendingTeammates: [],
95+
declinedTeammates: [],
96+
};
97+
98+
beforeEach(async () => {
99+
pairingParam = buildMockGlobalShortcutParam();
100+
pairingParam.shortcut.message = { ts: '123' } as any;
101+
pairingSessionsRepo.getByThreadIdOrUndefined = jest.fn().mockResolvedValue(session);
102+
activeReviewRepo.getReviewByThreadIdOrFail = jest.fn();
103+
await getReviewInfo.shortcut(pairingParam);
104+
});
105+
106+
it('should not query the active review repo', () => {
107+
expect(activeReviewRepo.getReviewByThreadIdOrFail).not.toHaveBeenCalled();
108+
});
109+
110+
it('should open the pairing dialog', () => {
111+
expect(pairingParam.client.views.open).toHaveBeenCalledWith(
112+
expect.objectContaining({
113+
view: expect.objectContaining({
114+
title: { text: 'Pairing Session Info', type: 'plain_text' },
115+
}),
116+
}),
117+
);
118+
});
119+
});
120+
70121
it('should open a view with the correct review information', () => {
71122
expect(param.client.views.open).toHaveBeenCalledWith({
72123
trigger_id: param.shortcut.trigger_id,

src/bot/getReviewInfo.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { GlobalShortcutParam } from '@/slackTypes';
22
import { userRepo } from '@repos/userRepo';
33
import { App } from '@slack/bolt';
4-
import { View } from '@slack/types';
4+
import { KnownBlock, View } from '@slack/types';
55
import log from '@utils/log';
6-
import { ul } from '@utils/text';
7-
import { Interaction } from './enums';
6+
import { bold, formatSlot, mention, ul } from '@utils/text';
7+
import { InterviewFormatLabel, Interaction } from './enums';
88
import { activeReviewRepo } from '@repos/activeReviewsRepo';
99
import { ActiveReview } from '@models/ActiveReview';
1010
import { User } from '@models/User';
1111
import { reviewActionService } from '@/services/ReviewActionService';
12+
import { pairingSessionsRepo } from '@repos/pairingSessionsRepo';
13+
import { PairingSession } from '@models/PairingSession';
1214

1315
export const getReviewInfo = {
1416
app: undefined as unknown as App,
@@ -51,6 +53,60 @@ export const getReviewInfo = {
5153
};
5254
},
5355

56+
pairingDialog(session: PairingSession): View {
57+
const blocks: KnownBlock[] = [
58+
{
59+
type: 'section',
60+
text: {
61+
type: 'mrkdwn',
62+
text: `:mag: Pairing session for ${bold(session.candidateName)} — requested by ${mention({ id: session.requestorId })}.`,
63+
},
64+
},
65+
{ type: 'divider' },
66+
{
67+
type: 'section',
68+
text: {
69+
type: 'mrkdwn',
70+
text: ul(
71+
`${bold('Format:')} ${InterviewFormatLabel.get(session.format) ?? session.format}`,
72+
`${bold('Languages:')} ${session.languages.join(', ')}`,
73+
`${bold('Teammates needed per slot:')} ${session.teammatesNeededCount}`,
74+
`${bold('Pending responses:')} ${session.pendingTeammates.length}`,
75+
`${bold('Declined:')} ${session.declinedTeammates.length}`,
76+
),
77+
},
78+
},
79+
{ type: 'divider' },
80+
{
81+
type: 'section',
82+
text: {
83+
type: 'mrkdwn',
84+
text: bold(
85+
`Candidate availability (${session.slots.length} slot${session.slots.length !== 1 ? 's' : ''}):`,
86+
),
87+
},
88+
},
89+
...session.slots.map<KnownBlock>(slot => ({
90+
type: 'section',
91+
text: {
92+
type: 'mrkdwn',
93+
text: ul(
94+
`${formatSlot(slot.date, slot.startTime, slot.endTime)}${slot.interestedTeammates.length} interested${
95+
slot.interestedTeammates.length > 0
96+
? `: ${slot.interestedTeammates.map(t => mention({ id: t.userId })).join(', ')}`
97+
: ''
98+
}`,
99+
),
100+
},
101+
})),
102+
];
103+
return {
104+
title: { text: 'Pairing Session Info', type: 'plain_text' },
105+
type: 'modal',
106+
blocks,
107+
};
108+
},
109+
54110
missingReviewDialog(): View {
55111
return {
56112
title: {
@@ -74,19 +130,27 @@ export const getReviewInfo = {
74130
log.d('getReviewInfo.shortcut', `Requesting review info, user.id=${shortcut.user.id}`);
75131
await ack();
76132

133+
const threadTs = shortcut.message.ts;
134+
135+
const pairingSession = await pairingSessionsRepo.getByThreadIdOrUndefined(threadTs);
136+
if (pairingSession) {
137+
await client.views.open({
138+
trigger_id: shortcut.trigger_id,
139+
view: this.pairingDialog(pairingSession),
140+
});
141+
return;
142+
}
143+
77144
try {
78-
const activeReview = await activeReviewRepo.getReviewByThreadIdOrFail(shortcut.message.ts);
145+
const activeReview = await activeReviewRepo.getReviewByThreadIdOrFail(threadTs);
79146
const allUsers = await userRepo.listAll();
80147
await client.views.open({
81148
trigger_id: shortcut.trigger_id,
82149
view: this.dialog(activeReview, allUsers),
83150
});
84151
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85152
} catch (_err: any) {
86-
log.d(
87-
'getReviewInfo.shortcut',
88-
`Unable to find active review with ts ${shortcut.message.ts}`,
89-
);
153+
log.d('getReviewInfo.shortcut', `Unable to find active review with ts ${threadTs}`);
90154
await client.views.open({
91155
trigger_id: shortcut.trigger_id,
92156
view: this.missingReviewDialog(),

0 commit comments

Comments
 (0)