Browse Source

Adds direct GH provider data to focus view

main
Keith Daulton 1 year ago
committed by Keith Daulton
parent
commit
4c89f639f7
12 changed files with 768 additions and 16 deletions
  1. +1
    -1
      .vscode/queries.github-graphql-nb
  2. +74
    -1
      src/git/gitProviderService.ts
  3. +78
    -0
      src/git/models/issue.ts
  4. +43
    -0
      src/git/models/pullRequest.ts
  5. +18
    -2
      src/git/remotes/github.ts
  6. +12
    -2
      src/git/remotes/gitlab.ts
  7. +44
    -2
      src/git/remotes/richRemoteProvider.ts
  8. +310
    -3
      src/plus/github/github.ts
  9. +116
    -1
      src/plus/github/models.ts
  10. +64
    -3
      src/plus/webviews/workspaces/workspacesWebview.ts
  11. +7
    -1
      src/plus/workspaces/workspaces.ts
  12. +1
    -0
      src/webviews/apps/plus/workspaces/workspaces.ts

+ 1
- 1
.vscode/queries.github-graphql-nb
File diff suppressed because it is too large
View File


+ 74
- 1
src/git/gitProviderService.ts View File

@ -52,9 +52,10 @@ import type { GitContributor } from './models/contributor';
import type { GitDiff, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from './models/diff';
import type { GitFile } from './models/file';
import type { GitGraph } from './models/graph';
import type { SearchedIssue } from './models/issue';
import type { GitLog } from './models/log';
import type { GitMergeStatus } from './models/merge';
import type { PullRequest, PullRequestState } from './models/pullRequest';
import type { PullRequest, PullRequestState, SearchedPullRequest } from './models/pullRequest';
import type { GitRebaseStatus } from './models/rebase';
import type { GitBranchReference, GitReference } from './models/reference';
import { GitRevision } from './models/reference';
@ -1755,6 +1756,78 @@ export class GitProviderService implements Disposable {
}
}
@debug<GitProviderService['getMyPullRequests']>({ args: { 0: remoteOrProvider => remoteOrProvider.name } })
async getMyPullRequests(
remoteOrProvider: GitRemote | RichRemoteProvider,
options?: { timeout?: number },
): Promise<SearchedPullRequest[] | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasRichIntegration()) return undefined;
} else {
provider = remoteOrProvider;
}
let timeout;
if (options != null) {
({ timeout, ...options } = options);
}
let promiseOrPRs = provider.searchMyPullRequests();
if (promiseOrPRs == null || !isPromise(promiseOrPRs)) {
return promiseOrPRs;
}
if (timeout != null && timeout > 0) {
promiseOrPRs = cancellable(promiseOrPRs, timeout);
}
try {
return await promiseOrPRs;
} catch (ex) {
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
}
@debug<GitProviderService['getMyIssues']>({ args: { 0: remoteOrProvider => remoteOrProvider.name } })
async getMyIssues(
remoteOrProvider: GitRemote | RichRemoteProvider,
options?: { timeout?: number },
): Promise<SearchedIssue[] | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasRichIntegration()) return undefined;
} else {
provider = remoteOrProvider;
}
let timeout;
if (options != null) {
({ timeout, ...options } = options);
}
let promiseOrPRs = provider.searchMyIssues();
if (promiseOrPRs == null || !isPromise(promiseOrPRs)) {
return promiseOrPRs;
}
if (timeout != null && timeout > 0) {
promiseOrPRs = cancellable(promiseOrPRs, timeout);
}
try {
return await promiseOrPRs;
} catch (ex) {
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
}
@log()
async getIncomingActivity(
repoPath: string | Uri,

+ 78
- 0
src/git/models/issue.ts View File

@ -18,6 +18,29 @@ export interface IssueOrPullRequest {
readonly closed: boolean;
}
export interface IssueLabel {
color: string;
name: string;
}
export interface IssueMember {
name: string;
avatarUrl: string;
url: string;
}
export interface IssueShape extends IssueOrPullRequest {
updatedDate: Date;
author: IssueMember;
assignees: IssueMember[];
labels?: IssueLabel[];
}
export interface SearchedIssue {
issue: IssueShape;
reasons: string[];
}
export function serializeIssueOrPullRequest(value: IssueOrPullRequest): IssueOrPullRequest {
const serialized: IssueOrPullRequest = {
type: value.type,
@ -96,3 +119,58 @@ export namespace IssueOrPullRequest {
return new ThemeIcon('issues', new ThemeColor(Colors.OpenAutolinkedIssueIconColor));
}
}
export function serializeIssue(value: IssueShape): IssueShape {
const serialized: IssueShape = {
type: value.type,
provider: {
id: value.provider.id,
name: value.provider.name,
domain: value.provider.domain,
icon: value.provider.icon,
},
id: value.id,
title: value.title,
url: value.url,
date: value.date,
closedDate: value.closedDate,
closed: value.closed,
updatedDate: value.updatedDate,
author: {
name: value.author.name,
avatarUrl: value.author.avatarUrl,
url: value.author.url,
},
assignees: value.assignees.map(assignee => ({
name: assignee.name,
avatarUrl: assignee.avatarUrl,
url: assignee.url,
})),
labels:
value.labels == null
? undefined
: value.labels.map(label => ({
color: label.color,
name: label.name,
})),
};
return serialized;
}
export class Issue implements IssueShape {
readonly type = IssueOrPullRequestType.Issue;
constructor(
public readonly provider: RemoteProviderReference,
public readonly id: string,
public readonly title: string,
public readonly url: string,
public readonly date: Date,
public readonly closed: boolean,
public readonly updatedDate: Date,
public readonly author: IssueMember,
public readonly assignees: IssueMember[],
public readonly closedDate?: Date,
public readonly labels?: IssueLabel[],
) {}
}

+ 43
- 0
src/git/models/pullRequest.ts View File

@ -14,6 +14,20 @@ export const enum PullRequestState {
Merged = 'Merged',
}
export interface PullRequestRef {
owner: string;
repo: string;
branch: string;
sha: string;
exists: boolean;
}
export interface PullRequestRefs {
base: PullRequestRef;
head: PullRequestRef;
isCrossRepository: boolean;
}
export interface PullRequestShape extends IssueOrPullRequest {
readonly author: {
readonly name: string;
@ -22,6 +36,13 @@ export interface PullRequestShape extends IssueOrPullRequest {
};
readonly state: PullRequestState;
readonly mergedDate?: Date;
readonly refs?: PullRequestRefs;
readonly isDraft?: boolean;
}
export interface SearchedPullRequest {
pullRequest: PullRequest;
reasons: string[];
}
export function serializePullRequest(value: PullRequest): PullRequestShape {
@ -46,6 +67,26 @@ export function serializePullRequest(value: PullRequest): PullRequestShape {
},
state: value.state,
mergedDate: value.mergedDate,
refs: value.refs
? {
head: {
exists: value.refs.head.exists,
owner: value.refs.head.owner,
repo: value.refs.head.repo,
sha: value.refs.head.sha,
branch: value.refs.head.branch,
},
base: {
exists: value.refs.base.exists,
owner: value.refs.base.owner,
repo: value.refs.base.repo,
sha: value.refs.base.sha,
branch: value.refs.base.branch,
},
isCrossRepository: value.refs.isCrossRepository,
}
: undefined,
isDraft: value.isDraft,
};
return serialized;
}
@ -103,6 +144,8 @@ export class PullRequest implements PullRequestShape {
public readonly date: Date,
public readonly closedDate?: Date,
public readonly mergedDate?: Date,
public readonly refs?: PullRequestRefs,
public readonly isDraft?: boolean,
) {}
get closed(): boolean {

+ 18
- 2
src/git/remotes/github.ts View File

@ -13,8 +13,8 @@ import { encodeUrl } from '../../system/encoding';
import { equalsIgnoreCase } from '../../system/string';
import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest } from '../models/issue';
import type { PullRequest, PullRequestState } from '../models/pullRequest';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { GitRevision } from '../models/reference';
import type { Repository } from '../models/repository';
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider';
@ -334,6 +334,22 @@ export class GitHubRemote extends RichRemoteProvider {
baseUrl: this.apiBaseUrl,
});
}
protected async searchProviderMyPullRequests({
accessToken,
}: AuthenticationSession): Promise<SearchedPullRequest[] | undefined> {
return (await this.container.github)?.searchMyPullRequests(this, accessToken, {
repos: [this.path],
});
}
protected async searchProviderMyIssues({
accessToken,
}: AuthenticationSession): Promise<SearchedIssue[] | undefined> {
return (await this.container.github)?.searchMyIssues(this, accessToken, {
repos: [this.path],
});
}
}
const gitHubNoReplyAddressRegex = /^(?:(\d+)\+)?([a-zA-Z\d-]{1,39})@users\.noreply\.(.*)$/i;

+ 12
- 2
src/git/remotes/gitlab.ts View File

@ -13,8 +13,8 @@ import { encodeUrl } from '../../system/encoding';
import { equalsIgnoreCase } from '../../system/string';
import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest } from '../models/issue';
import type { PullRequest, PullRequestState } from '../models/pullRequest';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { GitRevision } from '../models/reference';
import type { Repository } from '../models/repository';
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider';
@ -365,6 +365,16 @@ export class GitLabRemote extends RichRemoteProvider {
baseUrl: this.apiBaseUrl,
});
}
protected async searchProviderMyPullRequests(
_session: AuthenticationSession,
): Promise<SearchedPullRequest[] | undefined> {
return Promise.resolve(undefined);
}
protected async searchProviderMyIssues(_session: AuthenticationSession): Promise<SearchedIssue[] | undefined> {
return Promise.resolve(undefined);
}
}
export class GitLabAuthenticationProvider implements Disposable, IntegrationAuthenticationProvider {

+ 44
- 2
src/git/remotes/richRemoteProvider.ts View File

@ -15,8 +15,8 @@ import { debug, log } from '../../system/decorators/log';
import { isPromise } from '../../system/promise';
import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest } from '../models/issue';
import type { PullRequest, PullRequestState } from '../models/pullRequest';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { RemoteProvider } from './remoteProvider';
import { RichRemoteProviders } from './remoteProviderConnections';
@ -289,6 +289,48 @@ export abstract class RichRemoteProvider extends RemoteProvider {
@gate()
@debug()
async searchMyPullRequests(): Promise<SearchedPullRequest[] | undefined> {
const scope = getLogScope();
try {
const pullRequests = await this.searchProviderMyPullRequests(this._session!);
this.resetRequestExceptionCount();
return pullRequests;
} catch (ex) {
Logger.error(ex, scope);
if (ex instanceof AuthenticationError || ex instanceof ProviderRequestClientError) {
this.trackRequestException();
}
return undefined;
}
}
protected abstract searchProviderMyPullRequests(
session: AuthenticationSession,
): Promise<SearchedPullRequest[] | undefined>;
@gate()
@debug()
async searchMyIssues(): Promise<SearchedIssue[] | undefined> {
const scope = getLogScope();
try {
const issues = await this.searchProviderMyIssues(this._session!);
this.resetRequestExceptionCount();
return issues;
} catch (ex) {
Logger.error(ex, scope);
if (ex instanceof AuthenticationError || ex instanceof ProviderRequestClientError) {
this.trackRequestException();
}
return undefined;
}
}
protected abstract searchProviderMyIssues(session: AuthenticationSession): Promise<SearchedIssue[] | undefined>;
@gate()
@debug()
async getIssueOrPullRequest(id: string): Promise<IssueOrPullRequest | undefined> {
const scope = getLogScope();

+ 310
- 3
src/plus/github/github.ts View File

@ -21,8 +21,8 @@ import type { PagedResult } from '../../git/gitProvider';
import { RepositoryVisibility } from '../../git/gitProvider';
import type { Account } from '../../git/models/author';
import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest } from '../../git/models/issue';
import type { PullRequest } from '../../git/models/pullRequest';
import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequest';
import { GitRevision } from '../../git/models/reference';
import type { GitUser } from '../../git/models/user';
import { getGitHubNoReplyAddressParts } from '../../git/remotes/github';
@ -34,6 +34,7 @@ import {
showIntegrationRequestFailed500WarningMessage,
showIntegrationRequestTimedOutWarningMessage,
} from '../../messages';
import { uniqueBy } from '../../system/array';
import { debug } from '../../system/decorators/log';
import { Stopwatch } from '../../system/stopwatch';
import { base64 } from '../../system/string';
@ -46,13 +47,14 @@ import type {
GitHubCommit,
GitHubCommitRef,
GitHubContributor,
GitHubDetailedPullRequest,
GitHubIssueOrPullRequest,
GitHubPagedResult,
GitHubPageInfo,
GitHubPullRequestState,
GitHubTag,
} from './models';
import { GitHubPullRequest } from './models';
import { GitHubDetailedIssue, GitHubPullRequest } from './models';
const emptyPagedResult: PagedResult<any> = Object.freeze({ values: [] });
const emptyBlameResult: GitHubBlame = Object.freeze({ ranges: [] });
@ -2257,8 +2259,313 @@ export class GitHubApi implements Disposable {
// The /u/e endpoint automatically falls back to gravatar if not found
return `https://avatars.githubusercontent.com/u/e?email=${encodeURIComponent(email)}&s=${avatarSize}`;
}
@debug<GitHubApi['searchMyPullRequests']>({ args: { 0: p => p.name, 1: '<token>' } })
async searchMyPullRequests(
provider: RichRemoteProvider,
token: string,
options?: { search?: string; user?: string; repos?: string[] },
): Promise<SearchedPullRequest[]> {
const scope = getLogScope();
interface SearchResult {
related: {
nodes: GitHubDetailedPullRequest[];
};
authored: {
nodes: GitHubDetailedPullRequest[];
};
assigned: {
nodes: GitHubDetailedPullRequest[];
};
reviewRequested: {
nodes: GitHubDetailedPullRequest[];
};
mentioned: {
nodes: GitHubDetailedPullRequest[];
};
}
try {
const query = `query searchPullRequests(
$related: String!
$authored: String!
$assigned: String!
$reviewRequested: String!
$mentioned: String!
) {
related: search(first: 100, query: $related, type: ISSUE) {
nodes {
...on PullRequest {
${prNodeProperties}
}
}
}
authored: search(first: 100, query: $authored, type: ISSUE) {
nodes {
...on PullRequest {
${prNodeProperties}
}
}
}
assigned: search(first: 100, query: $assigned, type: ISSUE) {
nodes {
...on PullRequest {
${prNodeProperties}
}
}
}
reviewRequested: search(first: 100, query: $reviewRequested, type: ISSUE) {
nodes {
...on PullRequest {
${prNodeProperties}
}
}
}
mentioned: search(first: 100, query: $mentioned, type: ISSUE) {
nodes {
...on PullRequest {
${prNodeProperties}
}
}
}
}`;
let search = options?.search?.trim() ?? '';
if (options?.user) {
search += ` user:${options.user}`;
}
if (options?.repos != null && options.repos.length > 0) {
const repo = ' repo:';
search += `${repo}${options.repos.join(repo)}`;
}
const baseFilters = 'is:pr is:open archived:false';
const resp = await this.graphql<SearchResult>(
undefined,
token,
query,
{
related: `${search} ${baseFilters} user:@me`.trim(),
authored: `${search} ${baseFilters} author:@me`.trim(),
assigned: `${search} ${baseFilters} assignee:@me`.trim(),
reviewRequested: `${search} ${baseFilters} review-requested:@me`.trim(),
mentioned: `${search} ${baseFilters} mentions:@me`.trim(),
},
scope,
);
if (resp === undefined) return [];
function toQueryResult(pr: GitHubDetailedPullRequest, reason?: string): SearchedPullRequest {
return {
pullRequest: GitHubPullRequest.fromDetailed(pr, provider),
reasons: reason ? [reason] : [],
};
}
const results: SearchedPullRequest[] = uniqueWithReasons(
[
...resp.assigned.nodes.map(pr => toQueryResult(pr, 'assigned')),
...resp.reviewRequested.nodes.map(pr => toQueryResult(pr, 'review requested')),
...resp.mentioned.nodes.map(pr => toQueryResult(pr, 'mentioned')),
...resp.authored.nodes.map(pr => toQueryResult(pr, 'authored')),
...resp.related.nodes.map(pr => toQueryResult(pr)),
],
r => r.pullRequest.url,
);
return results;
} catch (ex) {
throw this.handleException(ex, undefined, scope);
}
}
@debug<GitHubApi['searchMyIssues']>({ args: { 0: '<token>' } })
async searchMyIssues(
provider: RichRemoteProvider,
token: string,
options?: { search?: string; user?: string; repos?: string[] },
): Promise<SearchedIssue[] | undefined> {
const scope = getLogScope();
interface SearchResult {
related: {
nodes: GitHubDetailedIssue[];
};
authored: {
nodes: GitHubDetailedIssue[];
};
assigned: {
nodes: GitHubDetailedIssue[];
};
mentioned: {
nodes: GitHubDetailedIssue[];
};
}
const query = `query searchIssues(
$related: String!
$authored: String!
$assigned: String!
$mentioned: String!
) {
related: search(first: 100, query: $related, type: ISSUE) {
nodes {
${issueNodeProperties}
}
}
authored: search(first: 100, query: $authored, type: ISSUE) {
nodes {
${issueNodeProperties}
}
}
assigned: search(first: 100, query: $assigned, type: ISSUE) {
nodes {
${issueNodeProperties}
}
}
mentioned: search(first: 100, query: $mentioned, type: ISSUE) {
nodes {
${issueNodeProperties}
}
}
}`;
let search = options?.search?.trim() ?? '';
if (options?.user) {
search += ` user:${options.user}`;
}
if (options?.repos != null && options.repos.length > 0) {
const repo = ' repo:';
search += `${repo}${options.repos.join(repo)}`;
}
const baseFilters = 'type:issue is:open archived:false';
try {
const resp = await this.graphql<SearchResult>(
undefined,
token,
query,
{
related: `${search} ${baseFilters} user:@me`.trim(),
authored: `${search} ${baseFilters} author:@me`.trim(),
assigned: `${search} ${baseFilters} assignee:@me`.trim(),
mentioned: `${search} ${baseFilters} mentions:@me`.trim(),
},
scope,
);
function toQueryResult(issue: GitHubDetailedIssue, reason?: string): SearchedIssue {
return {
issue: GitHubDetailedIssue.from(issue, provider),
reasons: reason ? [reason] : [],
};
}
if (resp === undefined) return [];
const results: SearchedIssue[] = uniqueWithReasons(
[
...resp.assigned.nodes.map(pr => toQueryResult(pr, 'assigned')),
...resp.mentioned.nodes.map(pr => toQueryResult(pr, 'mentioned')),
...resp.authored.nodes.map(pr => toQueryResult(pr, 'authored')),
...resp.related.nodes.map(pr => toQueryResult(pr)),
],
r => r.issue.url,
);
return results;
} catch (ex) {
throw this.handleException(ex, undefined, scope);
}
}
}
function isGitHubDotCom(options?: { baseUrl?: string }) {
return options?.baseUrl == null || options.baseUrl === 'https://api.github.com';
}
function uniqueWithReasons<T extends { reasons: string[] }>(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 prNodeProperties = `
author {
login
avatarUrl
url
}
permalink
number
title
state
updatedAt
closedAt
mergedAt
repository {
isFork
owner {
login
}
}
reviewDecision
mergedBy {
login
}
baseRefName
baseRefOid
baseRepository {
name
owner {
login
}
}
headRefName
headRefOid
headRepository {
name
owner {
login
}
}
`;
const issueNodeProperties = `
... on Issue {
number
title
url
createdAt
closedAt
updatedAt
author {
login
avatarUrl
url
}
repository {
name
owner {
login
}
}
assignees(first: 100) {
nodes {
login
url
avatarUrl
}
}
labels(first: 20) {
nodes {
color
name
}
}
}
`;

+ 116
- 1
src/plus/github/models.ts View File

@ -1,6 +1,7 @@
import type { Endpoints } from '@octokit/types';
import { GitFileIndexStatus } from '../../git/models/file';
import type { IssueOrPullRequestType } from '../../git/models/issue';
import type { IssueLabel, IssueMember, IssueOrPullRequestType } from '../../git/models/issue';
import { Issue } from '../../git/models/issue';
import { PullRequest, PullRequestState } from '../../git/models/pullRequest';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
@ -87,6 +88,48 @@ export interface GitHubPullRequest {
};
}
export interface GitHubDetailedIssue extends GitHubIssueOrPullRequest {
date: Date;
updatedDate: Date;
closedDate: Date;
author: {
login: string;
avatarUrl: string;
url: string;
};
assignees: { nodes: IssueMember[] };
labels?: { nodes: IssueLabel[] };
}
export type GitHubPullRequestReviewDecision = 'CHANGES_REQUESTED' | 'APPROVED' | 'REVIEW_REQUIRED';
export type GitHubPullRequestMergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN';
export interface GitHubDetailedPullRequest extends GitHubPullRequest {
baseRefName: string;
baseRefOid: string;
baseRepository: {
name: string;
owner: {
login: string;
};
};
headRefName: string;
headRefOid: string;
headRepository: {
name: string;
owner: {
login: string;
};
};
reviewDecision: GitHubPullRequestReviewDecision;
isReadByViewer: boolean;
isDraft: boolean;
isCrossRepository: boolean;
checksUrl: string;
totalCommentsCount: number;
mergeable: GitHubPullRequestMergeableState;
}
export namespace GitHubPullRequest {
export function from(pr: GitHubPullRequest, provider: RichRemoteProvider): PullRequest {
return new PullRequest(
@ -117,6 +160,78 @@ export namespace GitHubPullRequest {
export function toState(state: PullRequestState): GitHubPullRequestState {
return state === PullRequestState.Merged ? 'MERGED' : state === PullRequestState.Closed ? 'CLOSED' : 'OPEN';
}
export function fromDetailed(pr: GitHubDetailedPullRequest, provider: RichRemoteProvider): PullRequest {
return new PullRequest(
provider,
{
name: pr.author.login,
avatarUrl: pr.author.avatarUrl,
url: pr.author.url,
},
String(pr.number),
pr.title,
pr.permalink,
fromState(pr.state),
new Date(pr.updatedAt),
pr.closedAt == null ? undefined : new Date(pr.closedAt),
pr.mergedAt == null ? undefined : new Date(pr.mergedAt),
{
head: {
exists: pr.headRepository != null,
owner: pr.headRepository?.owner.login,
repo: pr.baseRepository?.name,
sha: pr.headRefOid,
branch: pr.headRefName,
},
base: {
exists: pr.baseRepository != null,
owner: pr.baseRepository?.owner.login,
repo: pr.baseRepository?.name,
sha: pr.baseRefOid,
branch: pr.baseRefName,
},
isCrossRepository: pr.isCrossRepository,
},
pr.isDraft,
);
}
}
export namespace GitHubDetailedIssue {
export function from(value: GitHubDetailedIssue, provider: RichRemoteProvider): Issue {
return new Issue(
{
id: provider.id,
name: provider.name,
domain: provider.domain,
icon: provider.icon,
},
String(value.number),
value.title,
value.url,
value.date,
value.closed,
value.updatedDate,
{
name: value.author.login,
avatarUrl: value.author.avatarUrl,
url: value.author.url,
},
value.assignees.nodes.map(assignee => ({
name: assignee.name,
avatarUrl: assignee.avatarUrl,
url: assignee.url,
})),
value.closedDate,
value.labels?.nodes == null
? undefined
: value.labels.nodes.map(label => ({
color: label.color,
name: label.name,
})),
);
}
}
export interface GitHubTag {

+ 64
- 3
src/plus/webviews/workspaces/workspacesWebview.ts View File

@ -2,6 +2,12 @@ import type { Disposable } from 'vscode';
import { Commands, ContextKeys } from '../../../constants';
import type { Container } from '../../../container';
import { setContext } from '../../../context';
import type { SearchedIssue } from '../../../git/models/issue';
import { serializeIssue } from '../../../git/models/issue';
import type { SearchedPullRequest } from '../../../git/models/pullRequest';
import { serializePullRequest } from '../../../git/models/pullRequest';
import type { GitRemote } from '../../../git/models/remote';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import { registerCommand } from '../../../system/command';
import { WebviewBase } from '../../../webviews/webviewBase';
import type { State } from './protocol';
@ -47,12 +53,67 @@ export class WorkspacesWebview extends WebviewBase {
}
private async getState(): Promise<State> {
return Promise.resolve({
workspaces: this.getWorkspaces(),
});
const prs = await this.getMyPullRequests();
const serializedPrs = prs.map(pr => ({
pullRequest: serializePullRequest(pr.pullRequest),
reasons: pr.reasons,
}));
const issues = await this.getMyIssues();
const serializedIssues = issues.map(issue => ({
issue: serializeIssue(issue.issue),
reasons: issue.reasons,
}));
return {
// workspaces: await this.getWorkspaces(),
myPullRequests: serializedPrs,
myIssues: serializedIssues,
};
}
protected override async includeBootstrap(): Promise<State> {
return this.getState();
}
private async getRichProviders(): Promise<GitRemote<RichRemoteProvider>[]> {
const remotes: GitRemote<RichRemoteProvider>[] = [];
for (const repo of this.container.git.openRepositories) {
const richRemote = await repo.getRichRemote(true);
if (richRemote == null || remotes.includes(richRemote)) {
continue;
}
remotes.push(richRemote);
}
return remotes;
}
private async getMyPullRequests(): Promise<SearchedPullRequest[]> {
const providers = await this.getRichProviders();
const allPrs = [];
for (const provider of providers) {
const prs = await this.container.git.getMyPullRequests(provider);
if (prs == null) {
continue;
}
allPrs.push(...prs);
}
return allPrs;
}
private async getMyIssues(): Promise<SearchedIssue[]> {
const providers = await this.getRichProviders();
const allIssues = [];
for (const provider of providers) {
const issues = await this.container.git.getMyIssues(provider);
if (issues == null) {
continue;
}
allIssues.push(...issues);
}
return allIssues;
}
}

+ 7
- 1
src/plus/workspaces/workspaces.ts View File

@ -34,7 +34,7 @@ export class WorkspacesApi implements Disposable {
return session.accessToken;
}
private async getRichProvider(): Promise<GitRemote<RichRemoteProvider> | undefined> {
private async getRichProviders(): Promise<GitRemote<RichRemoteProvider>[]> {
const remotes: GitRemote<RichRemoteProvider>[] = [];
for (const repo of this.container.git.openRepositories) {
const richRemote = await repo.getRichRemote(true);
@ -44,6 +44,12 @@ export class WorkspacesApi implements Disposable {
remotes.push(richRemote);
}
return remotes;
}
private async getRichProvider(): Promise<GitRemote<RichRemoteProvider> | undefined> {
const remotes = await this.getRichProviders();
if (remotes.length === 0) {
return undefined;
}

+ 1
- 0
src/webviews/apps/plus/workspaces/workspaces.ts View File

@ -18,6 +18,7 @@ export class WorkspacesApp extends App {
override onInitialize() {
this.log(`${this.appName}.onInitialize`);
this.renderContent();
console.log(this.state);
}
renderContent() {}

Loading…
Cancel
Save