diff --git a/src/features.ts b/src/features.ts index 74f2400..ac01f0f 100644 --- a/src/features.ts +++ b/src/features.ts @@ -35,4 +35,5 @@ export const enum PlusFeatures { Timeline = 'timeline', Worktrees = 'worktrees', Graph = 'graph', + Focus = 'focus', } diff --git a/src/plus/webviews/workspaces/protocol.ts b/src/plus/webviews/workspaces/protocol.ts index 81bb64d..b922dc2 100644 --- a/src/plus/webviews/workspaces/protocol.ts +++ b/src/plus/webviews/workspaces/protocol.ts @@ -1,11 +1,14 @@ import type { IssueShape } from '../../../git/models/issue'; import type { PullRequestShape } from '../../../git/models/pullRequest'; +import type { Subscription } from '../../../subscription'; import { IpcNotificationType } from '../../../webviews/protocol'; export type State = { + isPlus: boolean; + subscription: Subscription; pullRequests?: PullRequestResult[]; issues?: IssueResult[]; - repos?: IssueResult[]; + repos?: RepoWithRichProvider[]; [key: string]: unknown; }; @@ -21,6 +24,12 @@ export interface PullRequestResult extends SearchResultBase { pullRequest: PullRequestShape; } +export interface RepoWithRichProvider { + repo: string; + isGitHub: boolean; + isConnected: boolean; +} + export interface DidChangeStateNotificationParams { state: State; } @@ -29,3 +38,12 @@ export const DidChangeStateNotificationType = new IpcNotificationType( + 'graph/subscription/didChange', + true, +); diff --git a/src/plus/webviews/workspaces/workspacesWebview.ts b/src/plus/webviews/workspaces/workspacesWebview.ts index 423d58a..b9b845a 100644 --- a/src/plus/webviews/workspaces/workspacesWebview.ts +++ b/src/plus/webviews/workspaces/workspacesWebview.ts @@ -2,6 +2,7 @@ import type { Disposable } from 'vscode'; import { Commands, ContextKeys } from '../../../constants'; import type { Container } from '../../../container'; import { setContext } from '../../../context'; +import { PlusFeatures } from '../../../features'; import type { SearchedIssue } from '../../../git/models/issue'; import { serializeIssue } from '../../../git/models/issue'; import type { SearchedPullRequest } from '../../../git/models/pullRequest'; @@ -13,15 +14,27 @@ import { import type { GitRemote } from '../../../git/models/remote'; import type { Repository } from '../../../git/models/repository'; import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider'; +import type { Subscription } from '../../../subscription'; +import { SubscriptionState } from '../../../subscription'; import { registerCommand } from '../../../system/command'; import { WebviewBase } from '../../../webviews/webviewBase'; +import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService'; import { ensurePlusFeaturesEnabled } from '../../subscription/utils'; import type { State } from './protocol'; -import { DidChangeStateNotificationType } from './protocol'; +import { DidChangeStateNotificationType, DidChangeSubscriptionNotificationType } from './protocol'; + +interface RepoWithRichRemote { + repo: Repository; + remote: GitRemote; + isConnected: boolean; + isGitHub: boolean; +} export class WorkspacesWebview extends WebviewBase { + private _bootstrapping = true; private _pullRequests: SearchedPullRequest[] = []; private _issues: SearchedIssue[] = []; + private _etagSubscription?: number; constructor(container: Container) { super( @@ -34,6 +47,8 @@ export class WorkspacesWebview extends WebviewBase { 'workspacesWebview', Commands.ShowWorkspacesPage, ); + + this.disposables.push(this.container.subscription.onDidChange(this.onSubscriptionChanged, this)); } protected override registerCommands(): Disposable[] { @@ -51,6 +66,22 @@ export class WorkspacesWebview extends WebviewBase { void setContext(ContextKeys.WorkspacesFocused, focused); } + private async onSubscriptionChanged(e: SubscriptionChangeEvent) { + if (e.etag === this._etagSubscription) return; + + this._etagSubscription = e.etag; + + const access = await this.container.git.access(PlusFeatures.Focus); + const { subscription, isPlus } = await this.getSubscription(access.subscription.current); + if (isPlus) { + void this.notifyDidChangeState(); + } + return this.notify(DidChangeSubscriptionNotificationType, { + subscription: subscription, + isPlus: isPlus, + }); + } + private async getWorkspaces() { try { const rsp = await this.container.workspaces.getWorkspacesWithPullRequests(); @@ -62,116 +93,154 @@ export class WorkspacesWebview extends WebviewBase { return {}; } + private async getSubscription(subscription?: Subscription) { + const currentSubscription = subscription ?? (await this.container.subscription.getSubscription(true)); + const isPlus = ![ + SubscriptionState.Free, + SubscriptionState.FreePreviewTrialExpired, + SubscriptionState.FreePlusTrialExpired, + SubscriptionState.VerificationRequired, + ].includes(currentSubscription.state); + + return { + subscription: currentSubscription, + isPlus: isPlus, + }; + } + private async getState(deferState = false): Promise { - const prs = await this.getMyPullRequests(); + const { subscription, isPlus } = await this.getSubscription(); + if (deferState || !isPlus) { + return { + isPlus: isPlus, + subscription: subscription, + }; + } + + const richRepos = await this.getRichRepos(); + + const prs = await this.getMyPullRequests(richRepos); const serializedPrs = prs.map(pr => ({ pullRequest: serializePullRequest(pr.pullRequest), reasons: pr.reasons, })); - const issues = await this.getMyIssues(); + const issues = await this.getMyIssues(richRepos); const serializedIssues = issues.map(issue => ({ issue: serializeIssue(issue.issue), reasons: issue.reasons, })); return { - // workspaces: await this.getWorkspaces(), + isPlus: isPlus, + subscription: subscription, pullRequests: serializedPrs, issues: serializedIssues, }; } protected override async includeBootstrap(): Promise { + if (this._bootstrapping) { + const state = await this.getState(true); + if (state.isPlus) { + void this.notifyDidChangeState(); + } + return state; + } + return this.getState(); } - private async getRichRepos(): Promise<{ repo: Repository; provider: GitRemote }[]> { - const repos: { repo: Repository; provider: GitRemote }[] = []; + private async getRichRepos(): Promise { + const repos = []; for (const repo of this.container.git.openRepositories) { const richRemote = await repo.getRichRemote(true); - if (richRemote == null || repos.findIndex(repo => repo.provider === richRemote) > -1) { + if (richRemote == null || repos.findIndex(repo => repo.remote === richRemote) > -1) { continue; } + repos.push({ repo: repo, - provider: richRemote, + remote: richRemote, + isConnected: await richRemote.provider.isConnected(), + isGitHub: richRemote.provider.name === 'GitHub', }); } return repos; } - private async getMyPullRequests(): Promise { - if (this._pullRequests.length === 0) { - const richRepos = await this.getRichRepos(); - const allPrs = []; - for (const { provider } of richRepos) { - const prs = await this.container.git.getMyPullRequests(provider); - if (prs == null) { - continue; - } - allPrs.push(...prs.filter(pr => pr.reasons.length > 0)); + private async getMyPullRequests(richReposWithRemote?: RepoWithRichRemote[]): Promise { + // if (this._pullRequests.length === 0) { + const richRepos = richReposWithRemote ?? (await this.getRichRepos()); + const allPrs = []; + for (const { remote } of richRepos) { + const prs = await this.container.git.getMyPullRequests(remote); + if (prs == null) { + continue; } + allPrs.push(...prs.filter(pr => pr.reasons.length > 0)); + } - function getScore(pr: SearchedPullRequest) { - let score = 0; - if (pr.reasons.includes('author')) { - score += 1000; - } else if (pr.reasons.includes('assignee')) { - score += 900; - } else if (pr.reasons.includes('reviewer')) { - score += 800; - } else if (pr.reasons.includes('mentioned')) { - score += 700; - } + function getScore(pr: SearchedPullRequest) { + let score = 0; + if (pr.reasons.includes('authored')) { + score += 1000; + } else if (pr.reasons.includes('assigned')) { + score += 900; + } else if (pr.reasons.includes('review-requested')) { + score += 800; + } else if (pr.reasons.includes('mentioned')) { + score += 700; + } - if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.Approved) { - if (pr.pullRequest.mergeableState === PullRequestMergeableState.Mergeable) { - score += 100; - } else if (pr.pullRequest.mergeableState === PullRequestMergeableState.Conflicting) { - score += 90; - } else { - score += 80; - } - } else if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.ChangesRequested) { - score += 70; + if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.Approved) { + if (pr.pullRequest.mergeableState === PullRequestMergeableState.Mergeable) { + score += 100; + } else if (pr.pullRequest.mergeableState === PullRequestMergeableState.Conflicting) { + score += 90; + } else { + score += 80; } - - return score; + } else if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.ChangesRequested) { + score += 70; } - this._pullRequests = allPrs.sort((a, b) => { - const scoreA = getScore(a); - const scoreB = getScore(b); - - if (scoreA === scoreB) { - return a.pullRequest.date.getTime() - b.pullRequest.date.getTime(); - } - return (scoreB ?? 0) - (scoreA ?? 0); - }); + return score; } + this._pullRequests = allPrs.sort((a, b) => { + const scoreA = getScore(a); + const scoreB = getScore(b); + + if (scoreA === scoreB) { + return a.pullRequest.date.getTime() - b.pullRequest.date.getTime(); + } + return (scoreB ?? 0) - (scoreA ?? 0); + }); + // } + return this._pullRequests; } - private async getMyIssues(): Promise { - if (this._issues.length === 0) { - const richRepos = await this.getRichRepos(); - const allIssues = []; - for (const { provider } of richRepos) { - const issues = await this.container.git.getMyIssues(provider); - if (issues == null) { - continue; - } - allIssues.push(...issues.filter(pr => pr.reasons.length > 0)); + private async getMyIssues(richReposWithRemote?: RepoWithRichRemote[]): Promise { + // if (this._issues.length === 0) { + const richRepos = richReposWithRemote ?? (await this.getRichRepos()); + const allIssues = []; + for (const { remote } of richRepos) { + const issues = await this.container.git.getMyIssues(remote); + if (issues == null) { + continue; } - - this._issues = allIssues.sort((a, b) => b.issue.updatedDate.getTime() - a.issue.updatedDate.getTime()); + allIssues.push(...issues.filter(pr => pr.reasons.length > 0)); } + this._issues = allIssues.sort((a, b) => b.issue.updatedDate.getTime() - a.issue.updatedDate.getTime()); + // } + return this._issues; } + override async show(options?: { preserveFocus?: boolean | undefined; preserveVisibility?: boolean | undefined; @@ -182,6 +251,10 @@ export class WorkspacesWebview extends WebviewBase { } private async notifyDidChangeState() { - return this.notify(DidChangeStateNotificationType, { state: await this.getState() }); + if (!this.visible) return; + + const state = await this.getState(); + this._bootstrapping = false; + void this.notify(DidChangeStateNotificationType, { state: state }); } } diff --git a/src/webviews/apps/plus/workspaces/components/pull-request-row.ts b/src/webviews/apps/plus/workspaces/components/pull-request-row.ts index 3c3c6a2..52476f7 100644 --- a/src/webviews/apps/plus/workspaces/components/pull-request-row.ts +++ b/src/webviews/apps/plus/workspaces/components/pull-request-row.ts @@ -37,6 +37,7 @@ const template = html` title="cannot be merged due to merge conflicts" >`, )} + ${when(x => x.indicator === 'checks', html``)} ${x => x.lastUpdated} @@ -84,11 +85,6 @@ const template = html` ${x => x.pullRequest!.comments} - - ${when(x => x.checks == null, html``)} - ${when(x => x.checks === false, html``)} - ${when(x => x.checks === true, html``)} - +${x => x.pullRequest!.additions} -${x => x.pullRequest!.deletions} -