Skip to content

Commit f397c51

Browse files
committed
Connects integrations if they are not connected
(#3621)
1 parent 73575c2 commit f397c51

File tree

3 files changed

+106
-2
lines changed

3 files changed

+106
-2
lines changed

docs/telemetry-events.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,7 @@ void
13201320
'repository.visibility': 'private' | 'public' | 'local',
13211321
'repoPrivacy': 'private' | 'public' | 'local',
13221322
'filesChanged': number,
1323-
'source': 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'welcome' | 'home' | 'code-suggest' | 'account' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'trial-indicator' | 'scm-input' | 'subscription' | 'walkthrough' | 'worktrees'
1323+
'source': 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'welcome' | 'home' | 'code-suggest' | 'account' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'startWork' | 'trial-indicator' | 'scm-input' | 'subscription' | 'walkthrough' | 'worktrees'
13241324
}
13251325
```
13261326

src/constants.telemetry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ export type Sources =
611611
| 'quick-wizard'
612612
| 'remoteProvider'
613613
| 'settings'
614+
| 'startWork'
614615
| 'timeline'
615616
| 'trial-indicator'
616617
| 'scm-input'

src/plus/startWork/startWork.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { QuickInputButton } from 'vscode';
1+
import type { QuickInputButton, QuickPick } from 'vscode';
22
import { ThemeIcon, Uri } from 'vscode';
33
import type {
4+
AsyncStepResultGenerator,
45
PartialStepState,
56
StepGenerator,
67
StepResultGenerator,
@@ -11,12 +12,16 @@ import {
1112
canPickStepContinue,
1213
createPickStep,
1314
endSteps,
15+
freezeStep,
1416
QuickCommand,
1517
StepResultBreak,
1618
} from '../../commands/quickCommand';
1719
import { ensureAccessStep } from '../../commands/quickCommand.steps';
1820
import { getSteps } from '../../commands/quickWizard.utils';
21+
import type { OpenWalkthroughCommandArgs } from '../../commands/walkthroughs';
1922
import { proBadge } from '../../constants';
23+
import { Commands } from '../../constants.commands';
24+
import type { IntegrationId } from '../../constants.integrations';
2025
import { HostingIntegrationId } from '../../constants.integrations';
2126
import type { Container } from '../../container';
2227
import { PlusFeatures } from '../../features';
@@ -26,6 +31,8 @@ import { createQuickPickItemOfT, createQuickPickSeparator } from '../../quickpic
2631
import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive';
2732
import { createDirectiveQuickPickItem, Directive, isDirectiveQuickPickItem } from '../../quickpicks/items/directive';
2833
import { fromNow } from '../../system/date';
34+
import { some } from '../../system/iterable';
35+
import { executeCommand } from '../../system/vscode/command';
2936

3037
export type StartWorkItem = {
3138
item: SearchedIssue;
@@ -41,6 +48,7 @@ export type StartWorkResult = { items: StartWorkItem[] };
4148
interface Context {
4249
result: StartWorkResult;
4350
title: string;
51+
connectedIntegrations: Map<IntegrationId, boolean>;
4452
}
4553

4654
interface State {
@@ -63,6 +71,8 @@ function assertsStartWorkStepState(state: StepState<State>): asserts state is St
6371
throw new Error('Missing item');
6472
}
6573

74+
export const supportedStartWorkIntegrations = [HostingIntegrationId.GitHub];
75+
6676
export class StartWorkCommand extends QuickCommand<State> {
6777
constructor(container: Container) {
6878
super(container, 'startWork', 'startWork', `Start Work\u00a0\u00a0${proBadge}`, {
@@ -82,10 +92,19 @@ export class StartWorkCommand extends QuickCommand<State> {
8292
const context: Context = {
8393
result: { items: [] },
8494
title: this.title,
95+
connectedIntegrations: await this.getConnectedIntegrations(),
8596
};
8697

8798
while (this.canStepsContinue(state)) {
8899
context.title = this.title;
100+
101+
const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c);
102+
if (!hasConnectedIntegrations) {
103+
const result = yield* this.confirmCloudIntegrationsConnectStep(state, context);
104+
if (result === StepResultBreak) {
105+
return result;
106+
}
107+
}
89108
const result = yield* ensureAccessStep(state, context, PlusFeatures.Launchpad);
90109
if (result === StepResultBreak) continue;
91110

@@ -135,6 +154,78 @@ export class StartWorkCommand extends QuickCommand<State> {
135154
return state.counter < 0 ? StepResultBreak : undefined;
136155
}
137156

157+
private async *confirmCloudIntegrationsConnectStep(
158+
state: StepState<State>,
159+
context: Context,
160+
): AsyncStepResultGenerator<{ connected: boolean | IntegrationId; resume: () => void }> {
161+
// TODO: This step is almost an exact copy of the similar one from launchpad.ts. Do we want to do anything about it? Maybe to move it to an util function with ability to parameterize labels?
162+
const hasConnectedIntegration = some(context.connectedIntegrations.values(), c => c);
163+
const step = this.createConfirmStep(
164+
`${this.title} \u00a0\u2022\u00a0 Connect an ${hasConnectedIntegration ? 'Additional ' : ''}Integration`,
165+
[
166+
createDirectiveQuickPickItem(Directive.Cancel, undefined, {
167+
label: 'Start Work lets you start work on an issue',
168+
detail: 'Click to learn more about Start Work',
169+
iconPath: new ThemeIcon('rocket'),
170+
onDidSelect: () =>
171+
// TODO: navigate to "start-work" related place
172+
void executeCommand<OpenWalkthroughCommandArgs>(Commands.OpenWalkthrough, {
173+
step: 'launchpad',
174+
source: 'launchpad',
175+
detail: 'info',
176+
}),
177+
}),
178+
createQuickPickSeparator(),
179+
createQuickPickItemOfT(
180+
{
181+
label: `Connect an ${hasConnectedIntegration ? 'Additional ' : ''}Integration...`,
182+
detail: hasConnectedIntegration
183+
? 'Connect additional integrations to view their issues in Start Work'
184+
: 'Connect an integration to accelerate your work',
185+
picked: true,
186+
},
187+
true,
188+
),
189+
],
190+
createDirectiveQuickPickItem(Directive.Cancel, false, { label: 'Cancel' }),
191+
{
192+
placeholder: hasConnectedIntegration
193+
? 'Connect additional integrations to Start Work'
194+
: 'Connect an integration to get started with Start Work',
195+
buttons: [],
196+
ignoreFocusOut: true,
197+
},
198+
);
199+
200+
// Note: This is a hack to allow the quickpick to stay alive after the user finishes connecting the integration.
201+
// Otherwise it disappears.
202+
let freeze!: () => Disposable;
203+
let quickpick!: QuickPick<any>;
204+
step.onDidActivate = qp => {
205+
quickpick = qp;
206+
freeze = () => freezeStep(step, qp);
207+
};
208+
209+
const selection: StepSelection<typeof step> = yield step;
210+
211+
if (canPickStepContinue(step, state, selection)) {
212+
const previousPlaceholder = quickpick.placeholder;
213+
quickpick.placeholder = 'Connecting integrations...';
214+
quickpick.ignoreFocusOut = true;
215+
const resume = freeze();
216+
const connected = await this.container.integrations.connectCloudIntegrations(
217+
{ integrationIds: supportedStartWorkIntegrations },
218+
{
219+
source: 'startWork',
220+
},
221+
);
222+
quickpick.placeholder = previousPlaceholder;
223+
return { connected: connected, resume: () => resume[Symbol.dispose]() };
224+
}
225+
226+
return StepResultBreak;
227+
}
228+
138229
private *pickIssueStep(state: StepState<State>, context: Context): StepResultGenerator<StartWorkItem> {
139230
const buildIssueItem = (i: StartWorkItem) => {
140231
const buttons = [StartWorkQuickInputButton];
@@ -261,6 +352,18 @@ export class StartWorkCommand extends QuickCommand<State> {
261352
state.item = item;
262353
}
263354
}
355+
356+
private async getConnectedIntegrations(): Promise<Map<IntegrationId, boolean>> {
357+
const connected = new Map<IntegrationId, boolean>();
358+
await Promise.allSettled(
359+
supportedStartWorkIntegrations.map(async integrationId => {
360+
const integration = await this.container.integrations.get(integrationId);
361+
connected.set(integrationId, integration.maybeConnected ?? (await integration.isConnected()));
362+
}),
363+
);
364+
365+
return connected;
366+
}
264367
}
265368

266369
async function updateContextItems(container: Container, context: Context) {

0 commit comments

Comments
 (0)