Browse Source

Adds focus view WIP

main
Keith Daulton 1 year ago
committed by Keith Daulton
parent
commit
c141b9ff37
7 changed files with 383 additions and 151 deletions
  1. +1
    -1
      package.json
  2. +1
    -1
      src/plus/github/github.ts
  3. +10
    -0
      src/plus/webviews/workspaces/protocol.ts
  4. +82
    -55
      src/plus/webviews/workspaces/workspacesWebview.ts
  5. +179
    -94
      src/webviews/apps/plus/workspaces/workspaces.html
  6. +73
    -0
      src/webviews/apps/plus/workspaces/workspaces.scss
  7. +37
    -0
      src/webviews/apps/plus/workspaces/workspaces.ts

+ 1
- 1
package.json View File

@ -4488,7 +4488,7 @@
},
{
"command": "gitlens.showWorkspacesPage",
"title": "Show Workspaces",
"title": "Show Focus View",
"category": "GitLens+",
"icon": "$(layers)"
},

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

@ -2358,7 +2358,7 @@ export class GitHubApi implements Disposable {
const results: SearchedPullRequest[] = uniqueWithReasons(
[
...resp.assigned.nodes.map(pr => toQueryResult(pr, 'assigned')),
...resp.reviewRequested.nodes.map(pr => toQueryResult(pr, 'review requested')),
...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')),
],

+ 10
- 0
src/plus/webviews/workspaces/protocol.ts View File

@ -1,5 +1,6 @@
import type { IssueShape } from '../../../git/models/issue';
import type { PullRequestShape } from '../../../git/models/pullRequest';
import { IpcNotificationType } from '../../../webviews/protocol';
export type State = {
pullRequests?: PullRequestResult[];
@ -19,3 +20,12 @@ export interface IssueResult extends SearchResultBase {
export interface PullRequestResult extends SearchResultBase {
pullRequest: PullRequestShape;
}
export interface DidChangeStateNotificationParams {
state: State;
}
export const DidChangeStateNotificationType = new IpcNotificationType<DidChangeStateNotificationParams>(
'focus/state/didChange',
true,
);

+ 82
- 55
src/plus/webviews/workspaces/workspacesWebview.ts View File

@ -11,19 +11,25 @@ import {
serializePullRequest,
} from '../../../git/models/pullRequest';
import type { GitRemote } from '../../../git/models/remote';
import type { Repository } from '../../../git/models/repository';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import { registerCommand } from '../../../system/command';
import { WebviewBase } from '../../../webviews/webviewBase';
import { ensurePlusFeaturesEnabled } from '../../subscription/utils';
import type { State } from './protocol';
import { DidChangeStateNotificationType } from './protocol';
export class WorkspacesWebview extends WebviewBase<State> {
private _pullRequests: SearchedPullRequest[] = [];
private _issues: SearchedIssue[] = [];
constructor(container: Container) {
super(
container,
'gitlens.workspaces',
'workspaces.html',
'images/gitlens-icon.png',
'Workspaces',
'Focus View',
`${ContextKeys.WebviewPrefix}workspaces`,
'workspacesWebview',
Commands.ShowWorkspacesPage,
@ -56,9 +62,7 @@ export class WorkspacesWebview extends WebviewBase {
return {};
}
private async getState(): Promise<State> {
// return Promise.resolve({});
private async getState(deferState = false): Promise<State> {
const prs = await this.getMyPullRequests();
const serializedPrs = prs.map(pr => ({
pullRequest: serializePullRequest(pr.pullRequest),
@ -82,79 +86,102 @@ export class WorkspacesWebview extends WebviewBase {
return this.getState();
}
private async getRichProviders(): Promise<GitRemote<RichRemoteProvider>[]> {
const remotes: GitRemote<RichRemoteProvider>[] = [];
private async getRichRepos(): Promise<{ repo: Repository; provider: GitRemote<RichRemoteProvider> }[]> {
const repos: { repo: Repository; provider: GitRemote<RichRemoteProvider> }[] = [];
for (const repo of this.container.git.openRepositories) {
const richRemote = await repo.getRichRemote(true);
if (richRemote == null || remotes.includes(richRemote)) {
if (richRemote == null || repos.findIndex(repo => repo.provider === richRemote) > -1) {
continue;
}
remotes.push(richRemote);
repos.push({
repo: repo,
provider: richRemote,
});
}
return remotes;
return repos;
}
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;
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));
}
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('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;
}
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;
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;
}
} else if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.ChangesRequested) {
score += 70;
return score;
}
return score;
}
this._pullRequests = allPrs.sort((a, b) => {
const scoreA = getScore(a);
const scoreB = getScore(b);
return 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);
});
}
if (scoreA === scoreB) {
return a.pullRequest.date.getTime() - b.pullRequest.date.getTime();
}
return (scoreB ?? 0) - (scoreA ?? 0);
});
return this._pullRequests;
}
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;
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));
}
allIssues.push(...issues.filter(pr => pr.reasons.length > 0));
this._issues = allIssues.sort((a, b) => b.issue.updatedDate.getTime() - a.issue.updatedDate.getTime());
}
return 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;
}): Promise<void> {
if (!(await ensurePlusFeaturesEnabled())) return;
return super.show(options);
}
private async notifyDidChangeState() {
return this.notify(DidChangeStateNotificationType, { state: await this.getState() });
}
}

+ 179
- 94
src/webviews/apps/plus/workspaces/workspaces.html View File

@ -5,105 +5,190 @@
</head>
<body class="preload app">
<header class="app__header">
<header class="app__header" style="display: none">
<h1>Focus View</h1>
</header>
<main class="app__main">
<section class="workspace-section app__section">
<header class="workspace-section__header">
<h2>My Pull Requests</h2>
</header>
<div class="workspace-section__content">
<table-container id="pull-requests">
<table-row slot="head">
<table-cell class="pr-status" header="column" pinned title="PR status"
><code-icon icon="git-pull-request"></code-icon
></table-cell>
<table-cell class="pr-time" header="column" pinned title="Last updated">
<code-icon icon="gl-clock"></code-icon>
</table-cell>
<table-cell class="pr-body" header="column" pinned>Pull Request</table-cell>
<table-cell class="pr-author" header="column" pinned>Author</table-cell>
<table-cell class="pr-assigned" header="column" pinned>Assigned</table-cell>
<table-cell class="pr-comments" header="column" pinned title="Comments">
<code-icon icon="comment-discussion"></code-icon>
</table-cell>
<table-cell class="pr-checks" header="column" pinned title="Checks">
<code-icon icon="tasklist"></code-icon>
</table-cell>
<table-cell class="pr-stats" header="column" pinned title="Change stats">
<code-icon icon="add"></code-icon>
<code-icon icon="dash"></code-icon>
</table-cell>
<table-cell class="pr-actions" header="column" pinned
><code-icon icon="blank" title="actions"></code-icon
></table-cell>
</table-row>
</table-container>
</div>
</section>
<div class="app__content">
<main class="app__main">
<section class="workspace-section app__section">
<header class="workspace-section__header">
<h2>My Pull Requests</h2>
</header>
<div class="workspace-section__content">
<table-container id="pull-requests">
<table-row slot="head">
<table-cell class="pr-status" header="column" pinned title="PR status"
><code-icon icon="git-pull-request"></code-icon
></table-cell>
<table-cell class="pr-time" header="column" pinned title="Last updated">
<code-icon icon="gl-clock"></code-icon>
</table-cell>
<table-cell class="pr-body" header="column" pinned>Pull Request</table-cell>
<table-cell class="pr-author" header="column" pinned>Author</table-cell>
<table-cell class="pr-assigned" header="column" pinned>Assigned</table-cell>
<table-cell class="pr-comments" header="column" pinned title="Comments">
<code-icon icon="comment-discussion"></code-icon>
</table-cell>
<table-cell class="pr-checks" header="column" pinned title="Checks">
<code-icon icon="tasklist"></code-icon>
</table-cell>
<table-cell class="pr-stats" header="column" pinned title="Change stats">
<code-icon icon="add"></code-icon>
<code-icon icon="dash"></code-icon>
</table-cell>
<table-cell class="pr-actions" header="column" pinned
><code-icon icon="blank" title="actions"></code-icon
></table-cell>
</table-row>
</table-container>
</div>
</section>
<section class="workspace-section app__section">
<header class="workspace-section__header">
<h2>My Issues</h2>
</header>
<div class="workspace-section__content">
<table-container id="issues">
<table-row slot="head">
<table-cell class="pr-status" header="column" pinned title="PR status">
<code-icon icon="issues"></code-icon>
</table-cell>
<table-cell class="pr-time" header="column" pinned title="Last updated">
<code-icon icon="gl-clock"></code-icon>
</table-cell>
<table-cell header="column" pinned>Title</table-cell>
<table-cell class="pr-author" header="column" pinned>Author</table-cell>
<table-cell class="pr-assigned" header="column" pinned>Assignees</table-cell>
<table-cell class="pr-comments" header="column" pinned title="Comments">
<code-icon icon="comment-discussion"></code-icon>
</table-cell>
<table-cell class="pr-checks" header="column" pinned title="Thumbs up">
<code-icon icon="thumbsup"></code-icon>
</table-cell>
</table-row>
</table-container>
</div>
</section>
<section class="workspace-section app__section">
<header class="workspace-section__header">
<h2>My Issues</h2>
</header>
<div class="workspace-section__content">
<table-container id="issues">
<table-row slot="head">
<table-cell class="pr-status" header="column" pinned title="PR status">
<code-icon icon="issues"></code-icon>
</table-cell>
<table-cell class="pr-time" header="column" pinned title="Last updated">
<code-icon icon="gl-clock"></code-icon>
</table-cell>
<table-cell header="column" pinned>Title</table-cell>
<table-cell class="pr-author" header="column" pinned>Author</table-cell>
<table-cell class="pr-assigned" header="column" pinned>Assignees</table-cell>
<table-cell class="pr-comments" header="column" pinned title="Comments">
<code-icon icon="comment-discussion"></code-icon>
</table-cell>
<table-cell class="pr-checks" header="column" pinned title="Thumbs up">
<code-icon icon="thumbsup"></code-icon>
</table-cell>
</table-row>
</table-container>
</div>
</section>
<section class="workspace-section app__section">
<header class="workspace-section__header">
<h2>Work in Progress</h2>
</header>
<div class="workspace-section__content">
<table-container>
<table-row slot="head">
<table-cell header="column" pinned>Repo</table-cell>
<table-cell header="column" pinned>Stats</table-cell>
<table-cell header="column" pinned>Branch</table-cell>
<table-cell header="column" pinned>Remote</table-cell>
</table-row>
<table-row>
<table-cell>vscode-gitlens</table-cell>
<table-cell
><span class="stat-added">+50</span> <span class="stat-modified">~100</span>
<span class="stat-deleted">-206</span></table-cell
>
<table-cell
><span class="tag"
><code-icon icon="source-control"></code-icon>feature/workspaces</span
></table-cell
>
<table-cell
><span class="tag"
><code-icon icon="repo"></code-icon>gitkraken/vscode-gitlens</span
></table-cell
>
</table-row>
</table-container>
</div>
</section>
</main>
<section class="workspace-section app__section">
<header class="workspace-section__header">
<h2>Work in Progress</h2>
</header>
<div class="workspace-section__content">
<table-container>
<table-row slot="head">
<table-cell header="column" pinned>Repo</table-cell>
<table-cell header="column" pinned>Stats</table-cell>
<table-cell header="column" pinned>Branch</table-cell>
<table-cell header="column" pinned>Remote</table-cell>
</table-row>
<table-row>
<table-cell>vscode-gitlens</table-cell>
<table-cell
><span class="stat-added">+50</span> <span class="stat-modified">~100</span>
<span class="stat-deleted">-206</span></table-cell
>
<table-cell
><span class="tag"
><code-icon icon="source-control"></code-icon>feature/workspaces</span
></table-cell
>
<table-cell
><span class="tag"
><code-icon icon="repo"></code-icon>gitkraken/vscode-gitlens</span
></table-cell
>
</table-row>
</table-container>
</div>
</section>
</main>
<menu-list class="app__controls">
<menu-label>Type</menu-label>
<menu-item role="none">
<div class="choice">
<input class="choice__input" type="checkbox" id="filter-prs" value="mentioned" checked />
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-prs">Pull requests</label>
</div>
</menu-item>
<menu-item role="none">
<div class="choice">
<input class="choice__input" type="checkbox" id="filter-issues" value="mentioned" checked />
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-issues">Issues</label>
</div>
</menu-item>
<menu-divider></menu-divider>
<menu-label>Association</menu-label>
<menu-item role="none">
<div class="choice">
<input class="choice__input" type="checkbox" id="filter-assigned" value="assigned" checked />
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-assigned">Assigned to me</label>
</div>
</menu-item>
<menu-item role="none">
<div class="choice">
<input class="choice__input" type="checkbox" id="filter-created" value="authored" checked />
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-created">Created by me</label>
</div>
</menu-item>
<menu-item role="none">
<div class="choice">
<input
class="choice__input"
type="checkbox"
id="filter-review-requested"
value="review-requested"
checked
/>
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-review-requested">Review requested</label>
</div>
</menu-item>
<menu-item role="none">
<div class="choice">
<input class="choice__input" type="checkbox" id="filter-mentioned" value="mentioned" checked />
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-mentioned">Mentioned</label>
</div>
</menu-item>
<menu-divider></menu-divider>
<menu-label>Repos</menu-label>
<menu-item role="none">
<div class="choice">
<input
class="choice__input"
type="checkbox"
id="filter-repo-1"
value="mentioned"
checked
readonly
/>
<span class="choice__indicator"
><code-icon class="choice__indicator" icon="check"></code-icon
></span>
<label class="choice__label" for="filter-repo-1">vscode-gitlens</label>
</div>
</menu-item>
</menu-list>
</div>
#{endOfBody}
<style nonce="#{cspNonce}">
@font-face {

+ 73
- 0
src/webviews/apps/plus/workspaces/workspaces.scss View File

@ -144,11 +144,24 @@ code-icon {
flex-direction: column;
height: 100vh;
overflow: hidden;
padding-right: 0;
&__header {
flex: none;
}
&__content {
flex: 1 1 auto;
display: flex;
flex-direction: row;
overflow: hidden;
gap: 1rem;
}
&__controls {
flex: 0 0 20rem;
}
&__main {
flex: 1 1 auto;
display: flex;
@ -212,3 +225,63 @@ code-icon {
&-actions {
}
}
.choice {
display: inline-flex;
flex-direction: row;
align-items: center;
color: var(--vscode-checkbox-foreground);
margin: 0.4rem 0;
user-select: none;
&:focus-within {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
&__input {
clip: rect(0 0 0 0);
clip-path: inset(50%);
width: 1px;
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
&__indicator {
position: relative;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
background: var(--vscode-checkbox-background);
border: 0.1rem solid var(--vscode-checkbox-border);
width: 1.8rem;
height: 1.8rem;
outline: none;
cursor: pointer;
&,
code-icon {
border-radius: 0.3rem;
overflow: hidden;
}
}
&__input[type='radio'] + &__indicator {
border-radius: 99.9rem;
}
&__input:not(:checked) + &__indicator code-icon {
opacity: 0;
}
&__label {
font-family: var(--font-family);
color: var(--vscode-checkbox-foreground);
padding-inline-start: 1rem;
margin-inline-end: 1rem;
cursor: pointer;
}
}

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

@ -1,10 +1,17 @@
import type { State } from '../../../../plus/webviews/workspaces/protocol';
import { DidChangeStateNotificationType } from '../../../../plus/webviews/workspaces/protocol';
import type { IpcMessage } from '../../../protocol';
import { onIpc } from '../../../protocol';
import { App } from '../../shared/appBase';
import type { IssueRow } from './components/issue-row';
import type { PullRequestRow } from './components/pull-request-row';
import '../../shared/components/code-icon';
import '../../shared/components/avatars/avatar-item';
import '../../shared/components/avatars/avatar-stack';
import '../../shared/components/menu/menu-list';
import '../../shared/components/menu/menu-item';
import '../../shared/components/menu/menu-label';
import '../../shared/components/menu/menu-divider';
import '../../shared/components/table/table-container';
import '../../shared/components/table/table-row';
import '../../shared/components/table/table-cell';
@ -23,6 +30,20 @@ export class WorkspacesApp extends App {
console.log(this.state);
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
switch (msg.method) {
case DidChangeStateNotificationType.method:
onIpc(DidChangeStateNotificationType, msg, params => {
this.setState({ ...this.state, ...params.state });
this.renderContent();
});
break;
}
}
renderContent() {
this.renderPullRequests();
this.renderIssues();
@ -30,6 +51,14 @@ export class WorkspacesApp extends App {
renderPullRequests() {
const tableEl = document.getElementById('pull-requests');
if (tableEl == null) return;
if (tableEl.childNodes.length > 1) {
tableEl.childNodes.forEach((node, index) => {
if (index > 0) {
tableEl.removeChild(node);
}
});
}
if (this.state.pullRequests != null && this.state.pullRequests?.length > 0) {
const els = this.state.pullRequests.map(({ pullRequest, reasons }) => {
@ -45,6 +74,14 @@ export class WorkspacesApp extends App {
renderIssues() {
const tableEl = document.getElementById('issues');
if (tableEl == null) return;
if (tableEl.childNodes.length > 1) {
tableEl.childNodes.forEach((node, index) => {
if (index > 0) {
tableEl.removeChild(node);
}
});
}
if (this.state.issues != null && this.state.issues?.length > 0) {
const els = this.state.issues.map(({ issue, reasons }) => {

Loading…
Cancel
Save