Skip to content

Commit adc9a82

Browse files
committed
Adds Linear autolink matching.
(#4543, #4579)
1 parent a7206b5 commit adc9a82

File tree

4 files changed

+161
-4
lines changed

4 files changed

+161
-4
lines changed

src/autolinks/utils/-webview/autolinks.utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function serializeAutolink(value: Autolink): Autolink {
3232
return serialized;
3333
}
3434

35-
export const supportedAutolinkIntegrations = [IssuesCloudHostIntegrationId.Jira];
35+
export const supportedAutolinkIntegrations = [IssuesCloudHostIntegrationId.Jira, IssuesCloudHostIntegrationId.Linear];
3636

3737
export function isDynamic(ref: AutolinkReference | DynamicAutolinkReference): ref is DynamicAutolinkReference {
3838
return !('prefix' in ref) && !('url' in ref);
@@ -154,10 +154,10 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly<RefSet[
154154
let match;
155155
// Sort refsets so that issue integrations are checked first for matches
156156
const sortedRefSets = [...refsets].sort((a, b) => {
157-
if (a[0]?.id === IssuesCloudHostIntegrationId.Jira || a[0]?.id === IssuesCloudHostIntegrationId.Trello) {
157+
if (a[0]?.id && Object.values<string>(IssuesCloudHostIntegrationId).includes(a[0].id)) {
158158
return -1;
159159
}
160-
if (b[0]?.id === IssuesCloudHostIntegrationId.Jira || b[0]?.id === IssuesCloudHostIntegrationId.Trello) {
160+
if (b[0]?.id && Object.values<string>(IssuesCloudHostIntegrationId).includes(b[0].id)) {
161161
return 1;
162162
}
163163
return 0;

src/plus/integrations/providers/linear.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { CancellationToken } from 'vscode';
1+
import type { AuthenticationSession, CancellationToken } from 'vscode';
2+
import type { AutolinkReference, DynamicAutolinkReference } from '../../../autolinks/models/autolinks';
23
import { IssuesCloudHostIntegrationId } from '../../../constants.integrations';
34
import type { Account } from '../../../git/models/author';
45
import type { Issue, IssueShape } from '../../../git/models/issue';
@@ -17,12 +18,118 @@ const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes })
1718
const maxPagesPerRequest = 10;
1819

1920
export interface LinearTeamDescriptor extends IssueResourceDescriptor {
21+
avatarUrl: string | undefined;
22+
}
23+
24+
export interface LinearOrganizationDescriptor extends IssueResourceDescriptor {
2025
url: string;
2126
}
2227

2328
export interface LinearProjectDescriptor extends IssueResourceDescriptor {}
2429

2530
export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {
31+
private _autolinks: Map<string, (AutolinkReference | DynamicAutolinkReference)[]> | undefined;
32+
override async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {
33+
const connected = this.maybeConnected ?? (await this.isConnected());
34+
if (!connected || this._session == null) {
35+
return [];
36+
}
37+
const cachedAutolinks = this._autolinks?.get(this._session.accessToken);
38+
if (cachedAutolinks != null) return cachedAutolinks;
39+
40+
const organization = await this.getOrganization(this._session);
41+
if (organization == null) return [];
42+
43+
const autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];
44+
45+
const teams = await this.getTeams(this._session);
46+
for (const team of teams ?? []) {
47+
const dashedPrefix = `${team.key}-`;
48+
const underscoredPrefix = `${team.key}_`;
49+
50+
autolinks.push({
51+
prefix: dashedPrefix,
52+
url: `${organization.url}/issue/${dashedPrefix}<num>`,
53+
alphanumeric: false,
54+
ignoreCase: false,
55+
title: `Open Issue ${dashedPrefix}<num> on ${organization.name}`,
56+
57+
type: 'issue',
58+
description: `${organization.name} Issue ${dashedPrefix}<num>`,
59+
descriptor: { ...organization },
60+
});
61+
autolinks.push({
62+
prefix: underscoredPrefix,
63+
url: `${organization.url}/issue/${dashedPrefix}<num>`,
64+
alphanumeric: false,
65+
ignoreCase: false,
66+
referenceType: 'branch',
67+
title: `Open Issue ${dashedPrefix}<num> on ${organization.name}`,
68+
69+
type: 'issue',
70+
description: `${organization.name} Issue ${dashedPrefix}<num>`,
71+
descriptor: { ...organization },
72+
});
73+
}
74+
75+
this._autolinks ??= new Map<string, (AutolinkReference | DynamicAutolinkReference)[]>();
76+
this._autolinks.set(this._session.accessToken, autolinks);
77+
78+
return autolinks;
79+
}
80+
81+
private _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;
82+
private async getOrganization(
83+
{ accessToken }: AuthenticationSession,
84+
force: boolean = false,
85+
): Promise<LinearOrganizationDescriptor | undefined> {
86+
this._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();
87+
88+
const cachedResources = this._organizations.get(accessToken);
89+
90+
if (cachedResources == null || force) {
91+
const api = await this.getProvidersApi();
92+
const organization = await api.getLinearOrganization({ accessToken: accessToken });
93+
const descriptor: LinearOrganizationDescriptor | undefined = organization && {
94+
id: organization.id,
95+
key: organization.key,
96+
name: organization.name,
97+
url: organization.url,
98+
};
99+
if (descriptor) {
100+
this._organizations.set(accessToken, descriptor);
101+
}
102+
}
103+
104+
return this._organizations.get(accessToken);
105+
}
106+
107+
private _teams: Map<string, LinearTeamDescriptor[] | undefined> | undefined;
108+
private async getTeams(
109+
{ accessToken }: AuthenticationSession,
110+
force: boolean = false,
111+
): Promise<LinearTeamDescriptor[] | undefined> {
112+
this._teams ??= new Map<string, LinearTeamDescriptor[] | undefined>();
113+
114+
const cachedResources = this._teams.get(accessToken);
115+
116+
if (cachedResources == null || force) {
117+
const api = await this.getProvidersApi();
118+
const teams = await api.getLinearTeamsForCurrentUser({ accessToken: accessToken });
119+
const descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({
120+
id: t.id,
121+
key: t.key,
122+
name: t.name,
123+
avatarUrl: t.iconUrl,
124+
}));
125+
if (descriptors) {
126+
this._teams.set(accessToken, descriptors);
127+
}
128+
}
129+
130+
return this._teams.get(accessToken);
131+
}
132+
26133
protected override getProviderResourcesForUser(
27134
_session: ProviderAuthenticationSession,
28135
): Promise<ResourceDescriptor[] | undefined> {

src/plus/integrations/providers/models.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import type {
2020
JiraProject,
2121
JiraResource,
2222
Linear,
23+
LinearOrganization,
24+
LinearTeam,
2325
NumberedPageInput,
2426
Issue as ProviderApiIssue,
2527
PullRequestWithUniqueID,
@@ -75,6 +77,8 @@ export type ProviderIssue = ProviderApiIssue;
7577
export type ProviderEnterpriseOptions = EnterpriseOptions;
7678
export type ProviderJiraProject = JiraProject;
7779
export type ProviderJiraResource = JiraResource;
80+
export type ProviderLinearTeam = LinearTeam;
81+
export type ProviderLinearOrganization = LinearOrganization;
7882
export type ProviderAzureProject = AzureProject;
7983
export type ProviderAzureResource = AzureOrganization;
8084
export type ProviderBitbucketResource = BitbucketWorkspaceStub;
@@ -315,6 +319,8 @@ export type GetCurrentUserForResourceFn = (
315319
) => Promise<{ data: ProviderAccount }>;
316320

317321
export type GetJiraResourcesForCurrentUserFn = (options?: EnterpriseOptions) => Promise<{ data: JiraResource[] }>;
322+
export type GetLinearOrganizationFn = (options?: EnterpriseOptions) => Promise<{ data: LinearOrganization }>;
323+
export type GetLinearTeamsForCurrentUserFn = (options?: EnterpriseOptions) => Promise<{ data: LinearTeam[] }>;
318324
export type GetJiraProjectsForResourcesFn = (
319325
input: { resourceIds: string[] },
320326
options?: EnterpriseOptions,
@@ -377,6 +383,8 @@ export interface ProviderInfo extends ProviderMetadata {
377383
getCurrentUserForInstanceFn?: GetCurrentUserForInstanceFn;
378384
getCurrentUserForResourceFn?: GetCurrentUserForResourceFn;
379385
getJiraResourcesForCurrentUserFn?: GetJiraResourcesForCurrentUserFn;
386+
getLinearOrganizationFn?: GetLinearOrganizationFn;
387+
getLinearTeamsForCurrentUserFn?: GetLinearTeamsForCurrentUserFn;
380388
getAzureResourcesForUserFn?: GetAzureResourcesForUserFn;
381389
getBitbucketResourcesForUserFn?: GetBitbucketResourcesForUserFn;
382390
getBitbucketPullRequestsAuthoredByUserForWorkspaceFn?: GetBitbucketPullRequestsAuthoredByUserForWorkspaceFn;

src/plus/integrations/providers/providersApi.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import type {
5454
ProviderIssue,
5555
ProviderJiraProject,
5656
ProviderJiraResource,
57+
ProviderLinearOrganization,
58+
ProviderLinearTeam,
5759
ProviderPullRequest,
5860
ProviderRepoInput,
5961
ProviderReposInput,
@@ -328,6 +330,8 @@ export class ProvidersApi {
328330
provider: providerApis.linear,
329331
getIssueFn: providerApis.linear.getIssue.bind(providerApis.linear) as GetIssueFn,
330332
getIssuesForCurrentUserFn: providerApis.linear.getIssuesForCurrentUser.bind(providerApis.linear),
333+
getLinearOrganizationFn: providerApis.linear.getLinearOrganization.bind(providerApis.linear),
334+
getLinearTeamsForCurrentUserFn: providerApis.linear.getTeamsForCurrentUser.bind(providerApis.linear),
331335
},
332336
[IssuesCloudHostIntegrationId.Trello]: {
333337
...providersMetadata[IssuesCloudHostIntegrationId.Trello],
@@ -657,6 +661,44 @@ export class ProvidersApi {
657661
}
658662
}
659663

664+
async getLinearOrganization(options?: { accessToken?: string }): Promise<ProviderLinearOrganization | undefined> {
665+
const { provider, token } = await this.ensureProviderTokenAndFunction(
666+
IssuesCloudHostIntegrationId.Linear,
667+
'getLinearOrganizationFn',
668+
options?.accessToken,
669+
);
670+
671+
try {
672+
const x = await provider.getLinearOrganizationFn?.({ token: token });
673+
const y = x?.data;
674+
return y;
675+
} catch (e) {
676+
return this.handleProviderError<ProviderLinearOrganization | undefined>(
677+
IssuesCloudHostIntegrationId.Linear,
678+
token,
679+
e,
680+
);
681+
}
682+
}
683+
684+
async getLinearTeamsForCurrentUser(options?: { accessToken?: string }): Promise<ProviderLinearTeam[] | undefined> {
685+
const { provider, token } = await this.ensureProviderTokenAndFunction(
686+
IssuesCloudHostIntegrationId.Linear,
687+
'getLinearTeamsForCurrentUserFn',
688+
options?.accessToken,
689+
);
690+
691+
try {
692+
return (await provider.getLinearTeamsForCurrentUserFn?.({ token: token }))?.data;
693+
} catch (e) {
694+
return this.handleProviderError<ProviderLinearTeam[] | undefined>(
695+
IssuesCloudHostIntegrationId.Linear,
696+
token,
697+
e,
698+
);
699+
}
700+
}
701+
660702
async getAzureResourcesForUser(
661703
userId: string,
662704
integrationId: GitCloudHostIntegrationId.AzureDevOps | GitSelfManagedHostIntegrationId.AzureDevOpsServer,

0 commit comments

Comments
 (0)