diff --git a/src/git/models/issue.ts b/src/git/models/issue.ts index 3e18e7861a907..da4f8f2998a0b 100644 --- a/src/git/models/issue.ts +++ b/src/git/models/issue.ts @@ -76,11 +76,6 @@ export interface IssueRepository { id?: string; } -export interface SearchedIssue { - issue: IssueShape; - reasons: string[]; -} - export type IssueRepositoryIdentityDescriptor = RequireSomeWithProps< RequireSome, 'provider'>, 'provider', diff --git a/src/git/models/pullRequest.ts b/src/git/models/pullRequest.ts index 7812d8d74a4c4..733fe61567082 100644 --- a/src/git/models/pullRequest.ts +++ b/src/git/models/pullRequest.ts @@ -185,8 +185,3 @@ export type PullRequestRepositoryIdentityDescriptor = RequireSomeWithProps< 'id' | 'domain' | 'repoDomain' | 'repoName' > & RequireSomeWithProps, 'remote'>, 'remote', 'domain'>; - -export interface SearchedPullRequest { - pullRequest: PullRequest; - reasons: string[]; -} diff --git a/src/plus/integrations/integration.ts b/src/plus/integrations/integration.ts index f3e3418258128..b6ff5e8a2f44e 100644 --- a/src/plus/integrations/integration.ts +++ b/src/plus/integrations/integration.ts @@ -14,14 +14,9 @@ import { AuthenticationError, CancellationError, RequestClientError } from '../. import type { PagedResult } from '../../git/gitProvider'; import type { Account, UnidentifiedAuthor } from '../../git/models/author'; import type { DefaultBranch } from '../../git/models/defaultBranch'; -import type { Issue, SearchedIssue } from '../../git/models/issue'; +import type { Issue, IssueShape } from '../../git/models/issue'; import type { IssueOrPullRequest } from '../../git/models/issueOrPullRequest'; -import type { - PullRequest, - PullRequestMergeMethod, - PullRequestState, - SearchedPullRequest, -} from '../../git/models/pullRequest'; +import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../git/models/pullRequest'; import type { RepositoryMetadata } from '../../git/models/repositoryMetadata'; import type { PullRequestUrlIdentity } from '../../git/utils/pullRequest.utils'; import { showIntegrationDisconnectedTooManyFailedRequestsWarningMessage } from '../../messages'; @@ -418,16 +413,16 @@ export abstract class IntegrationBase< async searchMyIssues( resource?: ResourceDescriptor, cancellation?: CancellationToken, - ): Promise; + ): Promise; async searchMyIssues( resources?: ResourceDescriptor[], cancellation?: CancellationToken, - ): Promise; + ): Promise; @debug() async searchMyIssues( resources?: ResourceDescriptor | ResourceDescriptor[], cancellation?: CancellationToken, - ): Promise { + ): Promise { const scope = getLogScope(); const connected = this.maybeConnected ?? (await this.isConnected()); if (!connected) return undefined; @@ -441,7 +436,7 @@ export abstract class IntegrationBase< this.resetRequestExceptionCount(); return issues; } catch (ex) { - return this.handleProviderException(ex, scope, undefined); + return this.handleProviderException(ex, scope, undefined); } } @@ -449,7 +444,7 @@ export abstract class IntegrationBase< session: ProviderAuthenticationSession, resources?: ResourceDescriptor[], cancellation?: CancellationToken, - ): Promise; + ): Promise; @debug() async getIssueOrPullRequest( @@ -660,7 +655,7 @@ export abstract class IssueIntegration< async getIssuesForProject( project: T, options?: { user?: string; filters?: IssueFilter[] }, - ): Promise { + ): Promise { const connected = this.maybeConnected ?? (await this.isConnected()); if (!connected) return undefined; @@ -669,7 +664,7 @@ export abstract class IssueIntegration< this.resetRequestExceptionCount(); return issues; } catch (ex) { - return this.handleProviderException(ex, undefined, undefined); + return this.handleProviderException(ex, undefined, undefined); } } @@ -677,7 +672,7 @@ export abstract class IssueIntegration< session: ProviderAuthenticationSession, project: T, options?: { user?: string; filters?: IssueFilter[] }, - ): Promise; + ): Promise; } export abstract class HostingIntegration< @@ -1293,18 +1288,18 @@ export abstract class HostingIntegration< repo?: T, cancellation?: CancellationToken, silent?: boolean, - ): Promise>; + ): Promise>; async searchMyPullRequests( repos?: T[], cancellation?: CancellationToken, silent?: boolean, - ): Promise>; + ): Promise>; @debug() async searchMyPullRequests( repos?: T | T[], cancellation?: CancellationToken, silent?: boolean, - ): Promise> { + ): Promise> { const scope = getLogScope(); const connected = this.maybeConnected ?? (await this.isConnected()); if (!connected) return undefined; @@ -1329,7 +1324,7 @@ export abstract class HostingIntegration< repos?: T[], cancellation?: CancellationToken, silent?: boolean, - ): Promise; + ): Promise; async searchPullRequests( searchQuery: string, diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index 38d808b701614..d1482d9fd070c 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -11,8 +11,8 @@ import type { Source } from '../../constants.telemetry'; import { sourceToContext } from '../../constants.telemetry'; import type { Container } from '../../container'; import type { Account } from '../../git/models/author'; -import type { SearchedIssue } from '../../git/models/issue'; -import type { SearchedPullRequest } from '../../git/models/pullRequest'; +import type { IssueShape } from '../../git/models/issue'; +import type { PullRequest } from '../../git/models/pullRequest'; import type { GitRemote } from '../../git/models/remote'; import type { RemoteProvider, RemoteProviderId } from '../../git/remotes/remoteProvider'; import { configuration } from '../../system/-webview/configuration'; @@ -726,7 +726,7 @@ export class IntegrationService implements Disposable { | SupportedSelfHostedIntegrationIds )[], options?: { openRepositoriesOnly?: boolean; cancellation?: CancellationToken }, - ): Promise { + ): Promise { const integrations: Map = new Map(); const hostingIntegrationIds = integrationIds?.filter( id => id in HostingIntegrationId || id in SelfHostedIntegrationId, @@ -796,8 +796,8 @@ export class IntegrationService implements Disposable { private async getMyIssuesCore( integrations: Map, cancellation?: CancellationToken, - ): Promise { - const promises: Promise[] = []; + ): Promise { + const promises: Promise[] = []; for (const [integration, repos] of integrations) { if (integration == null) continue; @@ -808,12 +808,12 @@ export class IntegrationService implements Disposable { return [...flatten(filterMap(results, r => (r.status === 'fulfilled' ? r.value : undefined)))]; } - async getMyIssuesForRemotes(remote: GitRemote): Promise; - async getMyIssuesForRemotes(remotes: GitRemote[]): Promise; + async getMyIssuesForRemotes(remote: GitRemote): Promise; + async getMyIssuesForRemotes(remotes: GitRemote[]): Promise; @debug({ args: { 0: (r: GitRemote | GitRemote[]) => (Array.isArray(r) ? r.map(rp => rp.name) : r.name) }, }) - async getMyIssuesForRemotes(remoteOrRemotes: GitRemote | GitRemote[]): Promise { + async getMyIssuesForRemotes(remoteOrRemotes: GitRemote | GitRemote[]): Promise { if (!Array.isArray(remoteOrRemotes)) { remoteOrRemotes = [remoteOrRemotes]; } @@ -874,7 +874,7 @@ export class IntegrationService implements Disposable { integrationIds?: (HostingIntegrationId | CloudSelfHostedIntegrationId)[], cancellation?: CancellationToken, silent?: boolean, - ): Promise> { + ): Promise> { const integrations: Map = new Map(); for (const integrationId of integrationIds?.length ? integrationIds : Object.values(HostingIntegrationId)) { let integration; @@ -894,10 +894,10 @@ export class IntegrationService implements Disposable { integrations: Map, cancellation?: CancellationToken, silent?: boolean, - ): Promise> { + ): Promise> { const start = Date.now(); - const promises: Promise>[] = []; + const promises: Promise>[] = []; for (const [integration, repos] of integrations) { if (integration == null) continue; @@ -932,16 +932,14 @@ export class IntegrationService implements Disposable { }; } - async getMyPullRequestsForRemotes(remote: GitRemote): Promise>; - async getMyPullRequestsForRemotes( - remotes: GitRemote[], - ): Promise>; + async getMyPullRequestsForRemotes(remote: GitRemote): Promise>; + async getMyPullRequestsForRemotes(remotes: GitRemote[]): Promise>; @debug({ args: { 0: (r: GitRemote | GitRemote[]) => (Array.isArray(r) ? r.map(rp => rp.name) : r.name) }, }) async getMyPullRequestsForRemotes( remoteOrRemotes: GitRemote | GitRemote[], - ): Promise> { + ): Promise> { if (!Array.isArray(remoteOrRemotes)) { remoteOrRemotes = [remoteOrRemotes]; } diff --git a/src/plus/integrations/providers/azureDevOps.ts b/src/plus/integrations/providers/azureDevOps.ts index a8549081a050b..3eb4fcc420c4e 100644 --- a/src/plus/integrations/providers/azureDevOps.ts +++ b/src/plus/integrations/providers/azureDevOps.ts @@ -3,14 +3,9 @@ import { window } from 'vscode'; import { HostingIntegrationId } from '../../../constants.integrations'; import type { Account } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { Issue, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueShape } from '../../../git/models/issue'; import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest'; -import type { - PullRequest, - PullRequestMergeMethod, - PullRequestState, - SearchedPullRequest, -} from '../../../git/models/pullRequest'; +import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../../git/models/pullRequest'; import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata'; import { getSettledValue } from '../../../system/promise'; import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider'; @@ -316,7 +311,7 @@ export class AzureDevOpsIntegration extends HostingIntegration< protected override async searchProviderMyPullRequests( session: AuthenticationSession, repos?: AzureRepositoryDescriptor[], - ): Promise { + ): Promise { const api = await this.getProvidersApi(); if (repos != null) { // TODO: implement repos version @@ -351,17 +346,15 @@ export class AzureDevOpsIntegration extends HostingIntegration< authorLogin: user.username, }) )?.map(pr => this.fromAzureProviderPullRequest(pr, repoDescriptors, projects)); - const prsById = new Map(); + const prsById = new Map(); for (const pr of authoredPrs ?? []) { - prsById.set(pr.id, { pullRequest: pr, reasons: ['authored'] }); + prsById.set(pr.id, pr); } for (const pr of assignedPrs ?? []) { const existing = prsById.get(pr.id); - if (existing != null) { - existing.reasons.push('assigned'); - } else { - prsById.set(pr.id, { pullRequest: pr, reasons: ['assigned'] }); + if (existing == null) { + prsById.set(pr.id, pr); } } @@ -371,7 +364,7 @@ export class AzureDevOpsIntegration extends HostingIntegration< protected override async searchProviderMyIssues( session: AuthenticationSession, _repos?: AzureRepositoryDescriptor[], - ): Promise { + ): Promise { const api = await this.getProvidersApi(); const user = await this.getProviderCurrentAccount(session); @@ -410,18 +403,16 @@ export class AzureDevOpsIntegration extends HostingIntegration< ) ).flat(); // TODO: Add mentioned issues - const issuesById = new Map(); + const issuesById = new Map(); for (const issue of authoredIssues ?? []) { - issuesById.set(issue.id, { issue: issue, reasons: ['authored'] }); + issuesById.set(issue.id, issue); } for (const issue of assignedIssues ?? []) { const existing = issuesById.get(issue.id); - if (existing != null) { - existing.reasons.push('assigned'); - } else { - issuesById.set(issue.id, { issue: issue, reasons: ['assigned'] }); + if (existing == null) { + issuesById.set(issue.id, issue); } } diff --git a/src/plus/integrations/providers/bitbucket.ts b/src/plus/integrations/providers/bitbucket.ts index fd1a5d25e6e0d..0f496300b5ba8 100644 --- a/src/plus/integrations/providers/bitbucket.ts +++ b/src/plus/integrations/providers/bitbucket.ts @@ -2,14 +2,9 @@ import type { AuthenticationSession, CancellationToken } from 'vscode'; import { HostingIntegrationId } from '../../../constants.integrations'; import type { Account } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { Issue, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueShape } from '../../../git/models/issue'; import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest'; -import type { - PullRequest, - PullRequestMergeMethod, - PullRequestState, - SearchedPullRequest, -} from '../../../git/models/pullRequest'; +import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../../git/models/pullRequest'; import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata'; import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider'; import type { ResourceDescriptor } from '../integration'; @@ -126,14 +121,14 @@ export class BitbucketIntegration extends HostingIntegration< protected override async searchProviderMyPullRequests( _session: AuthenticationSession, _repos?: BitbucketRepositoryDescriptor[], - ): Promise { + ): Promise { return Promise.resolve(undefined); } protected override async searchProviderMyIssues( _session: AuthenticationSession, _repos?: BitbucketRepositoryDescriptor[], - ): Promise { + ): Promise { return Promise.resolve(undefined); } } diff --git a/src/plus/integrations/providers/github.ts b/src/plus/integrations/providers/github.ts index 518fc4a2bcb55..36702e9fccc30 100644 --- a/src/plus/integrations/providers/github.ts +++ b/src/plus/integrations/providers/github.ts @@ -4,14 +4,9 @@ import type { Sources } from '../../../constants.telemetry'; import type { Container } from '../../../container'; import type { Account, UnidentifiedAuthor } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { Issue, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueShape } from '../../../git/models/issue'; import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest'; -import type { - PullRequest, - PullRequestMergeMethod, - PullRequestState, - SearchedPullRequest, -} from '../../../git/models/pullRequest'; +import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../../git/models/pullRequest'; import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata'; import type { PullRequestUrlIdentity } from '../../../git/utils/pullRequest.utils'; import { log } from '../../../system/decorators/log'; @@ -190,7 +185,7 @@ abstract class GitHubIntegrationBase extends repos?: GitHubRepositoryDescriptor[], cancellation?: CancellationToken, silent?: boolean, - ): Promise { + ): Promise { return (await this.container.github)?.searchMyPullRequests( this, accessToken, @@ -207,7 +202,7 @@ abstract class GitHubIntegrationBase extends { accessToken }: AuthenticationSession, repos?: GitHubRepositoryDescriptor[], cancellation?: CancellationToken, - ): Promise { + ): Promise { return (await this.container.github)?.searchMyIssues( this, accessToken, diff --git a/src/plus/integrations/providers/github/github.ts b/src/plus/integrations/providers/github/github.ts index aa1de064251da..99cd74599bd73 100644 --- a/src/plus/integrations/providers/github/github.ts +++ b/src/plus/integrations/providers/github/github.ts @@ -19,9 +19,9 @@ import { import type { PagedResult, RepositoryVisibility } from '../../../../git/gitProvider'; import type { Account, UnidentifiedAuthor } from '../../../../git/models/author'; import type { DefaultBranch } from '../../../../git/models/defaultBranch'; -import type { Issue, SearchedIssue } from '../../../../git/models/issue'; +import type { Issue, IssueShape } from '../../../../git/models/issue'; import type { IssueOrPullRequest } from '../../../../git/models/issueOrPullRequest'; -import type { PullRequest, SearchedPullRequest } from '../../../../git/models/pullRequest'; +import type { PullRequest } from '../../../../git/models/pullRequest'; import { PullRequestMergeMethod } from '../../../../git/models/pullRequest'; import type { Provider } from '../../../../git/models/remoteProvider'; import type { RepositoryMetadata } from '../../../../git/models/repositoryMetadata'; @@ -2904,7 +2904,7 @@ export class GitHubApi implements Disposable { silent?: boolean; }, cancellation?: CancellationToken, - ): Promise { + ): Promise { const scope = getLogScope(); const limit = Math.min(100, configuration.get('launchpad.experimental.queryLimit') ?? 100); @@ -2981,7 +2981,7 @@ export class GitHubApi implements Disposable { const viewer = rsp.viewer.login; - function toQueryResult(pr: GitHubPullRequest): SearchedPullRequest { + function toQueryResult(pr: GitHubPullRequest): PullRequest { const reasons = []; if (pr.author.login === viewer) { reasons.push('authored'); @@ -2996,13 +2996,10 @@ export class GitHubApi implements Disposable { reasons.push('mentioned'); } - return { - pullRequest: fromGitHubPullRequest(pr, provider), - reasons: reasons, - }; + return fromGitHubPullRequest(pr, provider); } - const results: SearchedPullRequest[] = rsp.search.nodes.map(pr => toQueryResult(pr)); + const results: PullRequest[] = rsp.search.nodes.map(pr => toQueryResult(pr)); return results; } catch (ex) { throw this.handleException(ex, provider, scope, options?.silent); @@ -3022,7 +3019,7 @@ export class GitHubApi implements Disposable { includeBody?: boolean; }, cancellation?: CancellationToken, - ): Promise { + ): Promise { const scope = getLogScope(); interface SearchResult { @@ -3102,24 +3099,18 @@ export class GitHubApi implements Disposable { cancellation, ); - function toQueryResult(issue: GitHubIssue, reason?: string): SearchedIssue { - return { - issue: fromGitHubIssue(issue, provider), - reasons: reason ? [reason] : [], - }; + function toQueryResult(issue: GitHubIssue): IssueShape { + return fromGitHubIssue(issue, provider); } if (rsp == null) return []; - const results: SearchedIssue[] = uniqueWithReasons( - [ - ...rsp.assigned.nodes.map(pr => toQueryResult(pr, 'assigned')), - ...rsp.mentioned.nodes.map(pr => toQueryResult(pr, 'mentioned')), - ...rsp.authored.nodes.map(pr => toQueryResult(pr, 'authored')), - ], - r => r.issue.url, + const results: IterableIterator = uniqueBy( + [...rsp.assigned.nodes, ...rsp.mentioned.nodes, ...rsp.authored.nodes].map(toQueryResult), + r => r.url, + (original, _current) => original, ); - return results; + return [...results]; } catch (ex) { throw this.handleException(ex, provider, scope); } @@ -3255,14 +3246,3 @@ export class GitHubApi implements Disposable { function isGitHubDotCom(options?: { baseUrl?: string }) { return options?.baseUrl == null || options.baseUrl === 'https://api.github.com'; } - -function uniqueWithReasons(items: T[], lookup: (item: T) => unknown): T[] { - return [ - ...uniqueBy(items, lookup, (original, current) => { - if (current.reasons.length !== 0) { - original.reasons.push(...current.reasons); - } - return original; - }), - ]; -} diff --git a/src/plus/integrations/providers/gitlab.ts b/src/plus/integrations/providers/gitlab.ts index 138d74bb045c0..a682ed6df0366 100644 --- a/src/plus/integrations/providers/gitlab.ts +++ b/src/plus/integrations/providers/gitlab.ts @@ -5,14 +5,9 @@ import type { Sources } from '../../../constants.telemetry'; import type { Container } from '../../../container'; import type { Account } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { Issue, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueShape } from '../../../git/models/issue'; import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest'; -import type { - PullRequest, - PullRequestMergeMethod, - PullRequestState, - SearchedPullRequest, -} from '../../../git/models/pullRequest'; +import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../../git/models/pullRequest'; import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata'; import type { PullRequestUrlIdentity } from '../../../git/utils/pullRequest.utils'; import { log } from '../../../system/decorators/log'; @@ -24,8 +19,7 @@ import type { RepositoryDescriptor } from '../integration'; import { HostingIntegration } from '../integration'; import { getGitLabPullRequestIdentityFromMaybeUrl } from './gitlab/gitlab.utils'; import { fromGitLabMergeRequestProvidersApi } from './gitlab/models'; -import type { ProviderPullRequest } from './models'; -import { ProviderPullRequestReviewState, providersMetadata, toSearchedIssue } from './models'; +import { ProviderPullRequestReviewState, providersMetadata, toIssueShape } from './models'; import type { ProvidersApi } from './providersApi'; const metadata = providersMetadata[HostingIntegrationId.GitLab]; @@ -138,7 +132,7 @@ abstract class GitLabIntegrationBase< baseUrl: isEnterprise ? `https://${this.domain}` : undefined, }, ); - const issue = apiResult != null ? toSearchedIssue(apiResult, this)?.issue : undefined; + const issue = apiResult != null ? toIssueShape(apiResult, this) : undefined; return issue != null ? { ...issue, type: 'issue' } : undefined; } @@ -216,7 +210,7 @@ abstract class GitLabIntegrationBase< protected override async searchProviderMyPullRequests( { accessToken }: AuthenticationSession, repos?: GitLabRepositoryDescriptor[], - ): Promise { + ): Promise { const api = await this.getProvidersApi(); const isEnterprise = this.id === SelfHostedIntegrationId.GitLabSelfHosted || @@ -250,64 +244,35 @@ abstract class GitLabIntegrationBase< prs = apiResult.values; } - const toQueryResult = (pr: ProviderPullRequest, reason?: string): SearchedPullRequest => { - return { - pullRequest: fromGitLabMergeRequestProvidersApi(pr, this), - reasons: reason ? [reason] : [], - }; - }; - - function uniqueWithReasons(items: T[], lookup: (item: T) => unknown): T[] { - return [ - ...uniqueBy(items, lookup, (original, current) => { - if (current.reasons.length !== 0) { - original.reasons.push(...current.reasons); - } - return original; - }), - ]; - } - - const results: SearchedPullRequest[] = uniqueWithReasons( + const results: IterableIterator = uniqueBy( [ - ...prs.flatMap(pr => { - const result: SearchedPullRequest[] = []; - if (pr.assignees?.some(a => a.username === username)) { - result.push(toQueryResult(pr, 'assigned')); - } - - if ( - pr.reviews?.some( + ...prs + .filter(pr => { + const isAssignee = pr.assignees?.some(a => a.username === username); + const isRequestedReviewer = pr.reviews?.some( review => review.reviewer?.username === username || review.state === ProviderPullRequestReviewState.ReviewRequested, - ) - ) { - result.push(toQueryResult(pr, 'review-requested')); - } - - if (pr.author?.username === username) { - result.push(toQueryResult(pr, 'authored')); - } - - // It seems like GitLab doesn't give us mentioned PRs. - // if (???) { - // return toQueryResult(pr, 'mentioned'); - // } - - return result; - }), + ); + const isAuthor = pr.author?.username === username; + // It seems like GitLab doesn't give us mentioned PRs. + // const isMentioned = ???; + + return isAssignee || isRequestedReviewer || isAuthor; + }) + .map(pr => fromGitLabMergeRequestProvidersApi(pr, this)), ], - r => r.pullRequest.url, + r => r.url, + (original, _current) => original, ); - return results; + return [...results]; } protected override async searchProviderMyIssues( { accessToken }: AuthenticationSession, repos?: GitLabRepositoryDescriptor[], - ): Promise { + ): Promise { const api = await this.container.gitlab; const providerApi = await this.getProvidersApi(); const isEnterprise = @@ -334,8 +299,8 @@ abstract class GitLabIntegrationBase< }); return apiResult.values - .map(issue => toSearchedIssue(issue, this)) - .filter((result): result is SearchedIssue => result != null); + .map(issue => toIssueShape(issue, this)) + .filter((result): result is IssueShape => result != null); } protected override async searchProviderPullRequests( diff --git a/src/plus/integrations/providers/jira.ts b/src/plus/integrations/providers/jira.ts index 906405e64c0c9..bb0d7a0710cd1 100644 --- a/src/plus/integrations/providers/jira.ts +++ b/src/plus/integrations/providers/jira.ts @@ -2,14 +2,14 @@ import type { AuthenticationSession, CancellationToken } from 'vscode'; import type { AutolinkReference, DynamicAutolinkReference } from '../../../autolinks/models/autolinks'; import { IssueIntegrationId } from '../../../constants.integrations'; import type { Account } from '../../../git/models/author'; -import type { Issue, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueShape } from '../../../git/models/issue'; import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest'; import { filterMap, flatten } from '../../../system/iterable'; import { Logger } from '../../../system/logger'; import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider'; import type { IssueResourceDescriptor } from '../integration'; import { IssueIntegration } from '../integration'; -import { IssueFilter, providersMetadata, toAccount, toSearchedIssue } from './models'; +import { IssueFilter, providersMetadata, toAccount, toIssueShape } from './models'; const metadata = providersMetadata[IssueIntegrationId.Jira]; const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes }); @@ -166,7 +166,7 @@ export class JiraIntegration extends IssueIntegration { { accessToken }: AuthenticationSession, project: JiraProjectDescriptor, options?: { user: string; filters: IssueFilter[] }, - ): Promise { + ): Promise { let results; const api = await this.getProvidersApi(); @@ -174,7 +174,7 @@ export class JiraIntegration extends IssueIntegration { const getSearchedUserIssuesForFilter = async ( user: string, filter: IssueFilter, - ): Promise => { + ): Promise => { const results = await api.getIssuesForProject(this.id, project.name, project.resourceId, { authorLogin: filter === IssueFilter.Author ? user : undefined, assigneeLogins: filter === IssueFilter.Assignee ? [user] : undefined, @@ -183,8 +183,8 @@ export class JiraIntegration extends IssueIntegration { }); return results - ?.map(issue => toSearchedIssue(issue, this, filter)) - .filter((result): result is SearchedIssue => result !== undefined); + ?.map(issue => toIssueShape(issue, this)) + .filter((result): result is IssueShape => result !== undefined); }; if (options?.user != null && options.filters.length > 0) { @@ -200,13 +200,10 @@ export class JiraIntegration extends IssueIntegration { ), ]; - const resultsById = new Map(); - for (const result of results) { - if (resultsById.has(result.issue.id)) { - const existing = resultsById.get(result.issue.id)!; - existing.reasons = [...existing.reasons, ...result.reasons]; - } else { - resultsById.set(result.issue.id, result); + const resultsById = new Map(); + for (const resultIssue of results) { + if (!resultsById.has(resultIssue.id)) { + resultsById.set(resultIssue.id, resultIssue); } } @@ -217,24 +214,23 @@ export class JiraIntegration extends IssueIntegration { accessToken: accessToken, }); return results - ?.map(issue => toSearchedIssue(issue, this)) - .filter((result): result is SearchedIssue => result !== undefined); + ?.map(issue => toIssueShape(issue, this)) + .filter((result): result is IssueShape => result !== undefined); } protected override async searchProviderMyIssues( session: AuthenticationSession, resources?: JiraOrganizationDescriptor[], _cancellation?: CancellationToken, - ): Promise { + ): Promise { const myResources = resources ?? (await this.getProviderResourcesForUser(session)); if (!myResources) return undefined; const api = await this.getProvidersApi(); - const results: SearchedIssue[] = []; + const results: IssueShape[] = []; for (const resource of myResources) { try { - const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; let cursor = undefined; let hasMore = false; let requestCount = 0; @@ -247,8 +243,8 @@ export class JiraIntegration extends IssueIntegration { hasMore = resourceIssues.paging?.more ?? false; cursor = resourceIssues.paging?.cursor; const formattedIssues = resourceIssues.values - .map(issue => toSearchedIssue(issue, this, undefined, userLogin)) - .filter((result): result is SearchedIssue => result != null); + .map(issue => toIssueShape(issue, this)) + .filter((result): result is IssueShape => result != null); if (formattedIssues.length > 0) { results.push(...formattedIssues); } @@ -269,13 +265,12 @@ export class JiraIntegration extends IssueIntegration { id: string, ): Promise { const api = await this.getProvidersApi(); - const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; const issue = await api.getIssue( this.id, { resourceId: resource.id, number: id }, { accessToken: session.accessToken }, ); - return issue != null ? toSearchedIssue(issue, this, undefined, userLogin)?.issue : undefined; + return issue != null ? toIssueShape(issue, this) : undefined; } protected override async getProviderIssue( @@ -284,13 +279,12 @@ export class JiraIntegration extends IssueIntegration { id: string, ): Promise { const api = await this.getProvidersApi(); - const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; const apiResult = await api.getIssue( this.id, { resourceId: resource.id, number: id }, { accessToken: session.accessToken }, ); - const issue = apiResult != null ? toSearchedIssue(apiResult, this, undefined, userLogin)?.issue : undefined; + const issue = apiResult != null ? toIssueShape(apiResult, this) : undefined; return issue != null ? { ...issue, type: 'issue' } : undefined; } diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts index d22f7eaefc02e..dde5b82adc0ad 100644 --- a/src/plus/integrations/providers/models.ts +++ b/src/plus/integrations/providers/models.ts @@ -36,7 +36,7 @@ import { GitProviderUtils } from '@gitkraken/provider-apis/provider-utils'; import type { CloudSelfHostedIntegrationId, IntegrationId } from '../../../constants.integrations'; import { HostingIntegrationId, IssueIntegrationId, SelfHostedIntegrationId } from '../../../constants.integrations'; import type { Account as UserAccount } from '../../../git/models/author'; -import type { IssueMember, IssueProject, SearchedIssue } from '../../../git/models/issue'; +import type { IssueMember, IssueProject, IssueShape } from '../../../git/models/issue'; import { Issue, RepositoryAccessLevel } from '../../../git/models/issue'; import type { PullRequestMember, @@ -594,65 +594,52 @@ export function getReasonsForUserIssue(issue: ProviderIssue, userLogin: string): return reasons; } -export function toSearchedIssue( - issue: ProviderIssue, - provider: ProviderReference, - filterUsed?: IssueFilter, - userLogin?: string, -): SearchedIssue | undefined { +export function toIssueShape(issue: ProviderIssue, provider: ProviderReference): IssueShape | undefined { // TODO: Add some protections/baselines rather than killing the transformation here if (issue.updatedDate == null || issue.author == null || issue.url == null) return undefined; return { - reasons: - filterUsed != null - ? [issueFilterToReason(filterUsed)] - : userLogin != null - ? getReasonsForUserIssue(issue, userLogin) - : [], - issue: { - type: 'issue', - provider: provider, - id: issue.number, - nodeId: issue.graphQLId ?? issue.id, - title: issue.title, - url: issue.url, - createdDate: issue.createdDate, - updatedDate: issue.updatedDate, - closedDate: issue.closedDate ?? undefined, - closed: issue.closedDate != null, - state: issue.closedDate != null ? 'closed' : 'opened', - author: { - id: issue.author.id ?? '', - name: issue.author.name ?? '', - avatarUrl: issue.author.avatarUrl ?? undefined, - url: issue.author.url ?? undefined, - }, - assignees: - issue.assignees?.map(assignee => ({ - id: assignee.id ?? '', - name: assignee.name ?? '', - avatarUrl: assignee.avatarUrl ?? undefined, - url: assignee.url ?? undefined, - })) ?? [], - project: { - id: issue.project?.id ?? '', - name: issue.project?.name ?? '', - resourceId: issue.project?.resourceId ?? '', - resourceName: issue.project?.namespace ?? '', - }, - repository: - issue.repository?.owner?.login != null - ? { - owner: issue.repository.owner.login, - repo: issue.repository.name, - } - : undefined, - labels: issue.labels.map(label => ({ color: label.color ?? undefined, name: label.name })), - commentsCount: issue.commentCount ?? undefined, - thumbsUpCount: issue.upvoteCount ?? undefined, - body: issue.description ?? undefined, + type: 'issue', + provider: provider, + id: issue.number, + nodeId: issue.graphQLId ?? issue.id, + title: issue.title, + url: issue.url, + createdDate: issue.createdDate, + updatedDate: issue.updatedDate, + closedDate: issue.closedDate ?? undefined, + closed: issue.closedDate != null, + state: issue.closedDate != null ? 'closed' : 'opened', + author: { + id: issue.author.id ?? '', + name: issue.author.name ?? '', + avatarUrl: issue.author.avatarUrl ?? undefined, + url: issue.author.url ?? undefined, }, + assignees: + issue.assignees?.map(assignee => ({ + id: assignee.id ?? '', + name: assignee.name ?? '', + avatarUrl: assignee.avatarUrl ?? undefined, + url: assignee.url ?? undefined, + })) ?? [], + project: { + id: issue.project?.id ?? '', + name: issue.project?.name ?? '', + resourceId: issue.project?.resourceId ?? '', + resourceName: issue.project?.namespace ?? '', + }, + repository: + issue.repository?.owner?.login != null + ? { + owner: issue.repository.owner.login, + repo: issue.repository.name, + } + : undefined, + labels: issue.labels.map(label => ({ color: label.color ?? undefined, name: label.name })), + commentsCount: issue.commentCount ?? undefined, + thumbsUpCount: issue.upvoteCount ?? undefined, + body: issue.description ?? undefined, }; } diff --git a/src/plus/launchpad/launchpadProvider.ts b/src/plus/launchpad/launchpadProvider.ts index cb4bb15b3366c..4c58b0cdfd3cf 100644 --- a/src/plus/launchpad/launchpadProvider.ts +++ b/src/plus/launchpad/launchpadProvider.ts @@ -14,7 +14,7 @@ import { CancellationError } from '../../errors'; import { openComparisonChanges } from '../../git/actions/commit'; import type { Account } from '../../git/models/author'; import type { GitBranch } from '../../git/models/branch'; -import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequest'; +import type { PullRequest } from '../../git/models/pullRequest'; import type { GitRemote } from '../../git/models/remote'; import type { ProviderReference } from '../../git/models/remoteProvider'; import type { Repository } from '../../git/models/repository'; @@ -121,7 +121,7 @@ type CachedLaunchpadPromise = { const cacheExpiration = 1000 * 60 * 30; // 30 minutes type PullRequestsWithSuggestionCounts = { - prs: IntegrationResult | undefined; + prs: IntegrationResult | undefined; suggestionCounts: TimedResult | undefined; }; @@ -225,7 +225,7 @@ export class LaunchpadProvider implements Disposable { try { suggestionCounts = await withDurationAndSlowEventOnTimeout( this.container.drafts.getCodeSuggestionCounts( - prs.value.map(pr => pr.pullRequest).filter(pr => supportsCodeSuggest(pr.provider)), + prs.value.filter(pr => supportsCodeSuggest(pr.provider)), ), 'getCodeSuggestionCounts', this.container, @@ -244,14 +244,14 @@ export class LaunchpadProvider implements Disposable { search, connectedIntegrations, ); - const result: { readonly value: SearchedPullRequest[]; duration: number } = { + const result: { readonly value: PullRequest[]; duration: number } = { value: [], duration: 0, }; const findByPrIdentity = async ( integration: HostingIntegration, - ): Promise> => { + ): Promise> => { const { provider, ownerAndRepo, prNumber } = prUrlIdentity ?? {}; const providerMatch = provider == null || provider === integration.id; if (providerMatch && prNumber != null && ownerAndRepo != null) { @@ -267,7 +267,7 @@ export class LaunchpadProvider implements Disposable { this.container, ); if (pr?.value != null) { - return { value: [{ pullRequest: pr.value, reasons: [] }], duration: pr.duration }; + return { value: [pr.value], duration: pr.duration }; } } return undefined; @@ -275,14 +275,14 @@ export class LaunchpadProvider implements Disposable { const findByQuery = async ( integration: HostingIntegration, - ): Promise> => { + ): Promise> => { const prs = await withDurationAndSlowEventOnTimeout( integration?.searchPullRequests(search, undefined, cancellation), 'searchPullRequests', this.container, ); if (prs != null) { - return { value: prs.value?.map(pr => ({ pullRequest: pr, reasons: [] })), duration: prs.duration }; + return { value: prs.value, duration: prs.duration }; } return undefined; }; @@ -644,14 +644,12 @@ export class LaunchpadProvider implements Disposable { @gate( o => `${o?.force ?? false}|${ - o?.search != null && typeof o.search !== 'string' - ? o.search.map(s => s.pullRequest.url).join(',') - : o?.search + o?.search != null && typeof o.search !== 'string' ? o.search.map(pr => pr.url).join(',') : o?.search }`, ) @log({ args: { 0: o => `force=${o?.force}`, 1: false } }) async getCategorizedItems( - options?: { force?: boolean; search?: string | SearchedPullRequest[] }, + options?: { force?: boolean; search?: string | PullRequest[] }, cancellation?: CancellationToken, ): Promise { const scope = getLogScope(); @@ -735,7 +733,7 @@ export class LaunchpadProvider implements Disposable { : prs.value.filter( pr => !ignoredRepositories.has( - `${pr.pullRequest.repository.owner.toLowerCase()}/${pr.pullRequest.repository.repo.toLowerCase()}`, + `${pr.repository.owner.toLowerCase()}/${pr.repository.repo.toLowerCase()}`, ), ); @@ -747,9 +745,9 @@ export class LaunchpadProvider implements Disposable { await this.container.integrations.getMyCurrentAccounts(supportedLaunchpadIntegrations); const inputPrs: (EnrichablePullRequest | undefined)[] = filteredPrs.map(pr => { - const providerPr = toProviderPullRequestWithUniqueId(pr.pullRequest); + const providerPr = toProviderPullRequestWithUniqueId(pr); - const providerId = pr.pullRequest.provider.id; + const providerId = pr.provider.id; if ( !isSupportedLaunchpadIntegrationId(providerId) || @@ -762,24 +760,24 @@ export class LaunchpadProvider implements Disposable { const enrichable = { type: 'pr', id: providerPr.uuid, - url: pr.pullRequest.url, + url: pr.url, provider: providerId === HostingIntegrationId.AzureDevOps ? convertIntegrationIdToEnrichProvider(providerId) : convertRemoteProviderIdToEnrichProvider(providerId), } satisfies EnrichableItem; - const repoIdentity = getRepositoryIdentityForPullRequest(pr.pullRequest); + const repoIdentity = getRepositoryIdentityForPullRequest(pr); return { ...providerPr, type: 'pullrequest', uuid: providerPr.uuid, - provider: pr.pullRequest.provider, + provider: pr.provider, enrichable: enrichable, repoIdentity: repoIdentity, - refs: pr.pullRequest.refs, - underlyingPullRequest: pr.pullRequest, + refs: pr.refs, + underlyingPullRequest: pr, } satisfies EnrichablePullRequest; }) satisfies (EnrichablePullRequest | undefined)[]; diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 6a2afe4cb69d4..92d9e8fb3038a 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -31,7 +31,7 @@ import type { IntegrationId } from '../../constants.integrations'; import { HostingIntegrationId, IssueIntegrationId, SelfHostedIntegrationId } from '../../constants.integrations'; import type { Source, Sources, StartWorkTelemetryContext, TelemetryEvents } from '../../constants.telemetry'; import type { Container } from '../../container'; -import type { Issue, IssueShape, SearchedIssue } from '../../git/models/issue'; +import type { Issue, IssueShape } from '../../git/models/issue'; import type { GitBranchReference } from '../../git/models/reference'; import type { Repository } from '../../git/models/repository'; import { addAssociatedIssueToBranch } from '../../git/utils/-webview/branch.issue.utils'; @@ -50,7 +50,7 @@ import { some } from '../../system/iterable'; import { getIssueOwner } from '../integrations/providers/utils'; export type StartWorkItem = { - item: SearchedIssue; + issue: IssueShape; }; export type StartWorkResult = { items: StartWorkItem[] }; @@ -402,20 +402,19 @@ export abstract class StartWorkBaseCommand extends QuickCommand { const hasDisconnectedIntegrations = [...context.connectedIntegrations.values()].some(c => !c); const buildStartWorkQuickPickItem = (i: StartWorkItem) => { - const onWebButton = i.item.issue.url ? getOpenOnWebQuickInputButton(i.item.issue.provider.id) : undefined; + const onWebButton = i.issue.url ? getOpenOnWebQuickInputButton(i.issue.provider.id) : undefined; const buttons = onWebButton ? [onWebButton] : []; - const hoverContent = i.item.issue.body ? `${repeatSpaces(200)}\n\n${i.item.issue.body}` : ''; + const hoverContent = i.issue.body ? `${repeatSpaces(200)}\n\n${i.issue.body}` : ''; return { - label: - i.item.issue.title.length > 60 ? `${i.item.issue.title.substring(0, 60)}...` : i.item.issue.title, + label: i.issue.title.length > 60 ? `${i.issue.title.substring(0, 60)}...` : i.issue.title, description: `\u00a0 ${ - i.item.issue.repository ? `${i.item.issue.repository.owner}/${i.item.issue.repository.repo}#` : '' - }${i.item.issue.id} \u00a0`, + i.issue.repository ? `${i.issue.repository.owner}/${i.issue.repository.repo}#` : '' + }${i.issue.id} \u00a0`, // The spacing here at the beginning is used to align the description with the title. Otherwise it starts under the avatar icon: - detail: ` ${fromNow(i.item.issue.updatedDate)} by @${i.item.issue.author.name}${hoverContent}`, - iconPath: i.item.issue.author?.avatarUrl != null ? Uri.parse(i.item.issue.author.avatarUrl) : undefined, + detail: ` ${fromNow(i.issue.updatedDate)} by @${i.issue.author.name}${hoverContent}`, + iconPath: i.issue.author?.avatarUrl != null ? Uri.parse(i.issue.author.avatarUrl) : undefined, item: i, - picked: i.item.issue.id === state.item?.item?.issue.id, + picked: i.issue.id === state.item?.issue.id, buttons: buttons, }; }; @@ -525,8 +524,8 @@ export abstract class StartWorkBaseCommand extends QuickCommand { } private open(item: StartWorkItem): void { - if (item.item.issue.url == null) return; - void openUrl(item.item.issue.url); + if (item.issue.url == null) return; + void openUrl(item.issue.url); } private sendItemActionTelemetry(action: 'soft-open', item: StartWorkItem, context: Context) { @@ -571,7 +570,7 @@ export class StartWorkCommand extends StartWorkBaseCommand { state: StartWorkStepState, _context: Context, ): AsyncStepResultGenerator { - const issue = state.item.item.issue; + const issue = state.item.issue; const repo = issue && (await this.getIssueRepositoryIfExists(issue)); const result = yield* getSteps( @@ -639,7 +638,7 @@ export class AssociateIssueWithBranchCommand extends StartWorkBaseCommand { return; } - const issue = state.item.item.issue; + const issue = state.item.issue; if (this.branch == null) { this.branch = await showBranchPicker( @@ -673,7 +672,7 @@ async function updateContextItems(container: Container, context: Context) { items: (await container.integrations.getMyIssues(connectedIntegrations, { openRepositoriesOnly: true }))?.map( i => ({ - item: i, + issue: i, }), ) ?? [], }; @@ -694,22 +693,22 @@ function repeatSpaces(count: number) { } export function getStartWorkItemIdHash(item: StartWorkItem): string { - return md5(item.item.issue.id); + return md5(item.issue.id); } function buildItemTelemetryData(item: StartWorkItem) { return { 'item.id': getStartWorkItemIdHash(item), - 'item.type': item.item.issue.type, - 'item.provider': item.item.issue.provider.id, - 'item.assignees.count': item.item.issue.assignees?.length ?? undefined, - 'item.createdDate': item.item.issue.createdDate.getTime(), - 'item.updatedDate': item.item.issue.updatedDate.getTime(), + 'item.type': item.issue.type, + 'item.provider': item.issue.provider.id, + 'item.assignees.count': item.issue.assignees?.length ?? undefined, + 'item.createdDate': item.issue.createdDate.getTime(), + 'item.updatedDate': item.issue.updatedDate.getTime(), - 'item.comments.count': item.item.issue.commentsCount ?? undefined, - 'item.upvotes.count': item.item.issue.thumbsUpCount ?? undefined, + 'item.comments.count': item.issue.commentsCount ?? undefined, + 'item.upvotes.count': item.issue.thumbsUpCount ?? undefined, - 'item.issue.state': item.item.issue.state, + 'item.issue.state': item.issue.state, }; } diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 0afc83b39a6d9..54a4083d65841 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -1590,7 +1590,7 @@ async function getLaunchpadItemInfo( let lpi = result.items.find(i => i.url === pr.url); if (lpi == null) { // result = await container.launchpad.getCategorizedItems({ search: pr.url }); - result = await container.launchpad.getCategorizedItems({ search: [{ pullRequest: pr, reasons: [] }] }); + result = await container.launchpad.getCategorizedItems({ search: [pr] }); if (result.error != null) return undefined; lpi = result.items.find(i => i.url === pr.url);