From 6294f565a675a3b4bb6c35cedee7e8538eaff289 Mon Sep 17 00:00:00 2001 From: Keith Daulton <keith.daulton@gitkraken.com> Date: Tue, 11 Jul 2023 14:59:30 -0400 Subject: [PATCH] Adds basic focus view component WIP --- .../apps/plus/focus/components/gk-issue-row.ts | 112 ++++++++++++++++++ .../plus/focus/components/gk-pull-request-row.ts | 125 +++++++++++++++++++++ .../apps/plus/focus/components/pull-request-row.ts | 17 ++- src/webviews/apps/plus/focus/focus.html | 34 +++++- src/webviews/apps/plus/focus/focus.scss | 7 ++ src/webviews/apps/plus/focus/focus.ts | 92 ++++++++++++++- 6 files changed, 375 insertions(+), 12 deletions(-) create mode 100644 src/webviews/apps/plus/focus/components/gk-issue-row.ts create mode 100644 src/webviews/apps/plus/focus/components/gk-pull-request-row.ts diff --git a/src/webviews/apps/plus/focus/components/gk-issue-row.ts b/src/webviews/apps/plus/focus/components/gk-issue-row.ts new file mode 100644 index 0000000..eb7c2e3 --- /dev/null +++ b/src/webviews/apps/plus/focus/components/gk-issue-row.ts @@ -0,0 +1,112 @@ +import { css, html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { when } from 'lit/directives/when.js'; +import type { IssueMember, IssueShape } from '../../../../../git/models/issue'; +import { elementBase } from '../../../shared/components/styles/lit/base.css'; +import '@gitkraken/shared-web-components'; + +@customElement('gk-issue-row') +export class GkIssueRow extends LitElement { + static override styles = [ + elementBase, + css` + :host { + --gk-avatar-background-color: var(--background-10); + --color-background: var(--vscode-editor-background); + display: block; + } + + p { + margin: 0; + } + + a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + `, + ]; + + @property({ type: Number }) + public rank?: number; + + @property({ type: Object }) + public issue?: IssueShape; + + get lastUpdatedDate() { + return new Date(this.issue!.date); + } + + get assignees() { + const assignees = this.issue?.assignees; + if (assignees == null) { + return []; + } + const author: IssueMember | undefined = this.issue!.author; + if (author != null) { + return assignees.filter(assignee => assignee.name !== author.name); + } + + return assignees; + } + + override render() { + if (!this.issue) return undefined; + + return html` + <gk-focus-row> + <span slot="rank">${this.rank}</span> + <gk-focus-item> + <span slot="type" + ><code-icon icon="${this.issue.closed === true ? 'pass' : 'issues'}"></code-icon + ></span> + <p> + <strong>${this.issue.title} <a href="${this.issue.url}">#${this.issue.id}</a></strong> + <!-- + <gk-badge>pending suggestions</gk-badge> --> + </p> + <p> + <gk-tag> + <span slot="prefix"><code-icon icon="repo"></code-icon></span> + ${this.issue.repository.repo} + </gk-tag> + <gk-tag> + <span slot="prefix"><code-icon icon="comment-discussion"></code-icon></span> + ${this.issue.commentsCount} + </gk-tag> + <gk-tag> + <span slot="prefix"><code-icon icon="thumbsup"></code-icon></span> + ${this.issue.thumbsUpCount} + </gk-tag> + </p> + <span slot="people"> + <gk-avatar-group> + ${when( + this.issue.author != null, + () => html`<gk-avatar src="${this.issue!.author.avatarUrl}"></gk-avatar>`, + )} + ${when( + this.assignees.length > 0, + () => html` + ${repeat( + this.assignees, + item => item.url, + (item, index) => html`<gk-avatar src="${item.avatarUrl}"></gk-avatar>`, + )} + `, + )} + </gk-avatar-group> + </span> + <span slot="date"> + <gk-date-from date="${this.lastUpdatedDate}"></gk-date-from> + </span> + <nav slot="actions"></nav> + </gk-focus-item> + </gk-focus-row> + `; + } +} diff --git a/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts b/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts new file mode 100644 index 0000000..a715222 --- /dev/null +++ b/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts @@ -0,0 +1,125 @@ +import { css, html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { when } from 'lit/directives/when.js'; +import type { PullRequestMember, PullRequestShape } from '../../../../../git/models/pullRequest'; +import { elementBase } from '../../../shared/components/styles/lit/base.css'; +import '@gitkraken/shared-web-components'; + +@customElement('gk-pull-request-row') +export class GkPullRequestRow extends LitElement { + static override styles = [ + elementBase, + css` + :host { + --gk-avatar-background-color: var(--background-10); + --color-background: var(--vscode-editor-background); + display: block; + } + + p { + margin: 0; + } + + a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + `, + ]; + + @property({ type: Number }) + public rank?: number; + + @property({ type: Object }) + public pullRequest?: PullRequestShape; + + @property({ type: Boolean }) + public isCurrentBranch = false; + + @property({ type: Boolean }) + public isCurrentWorktree = false; + + @property({ type: Boolean }) + public hasWorktree = false; + + @property({ type: Boolean }) + public hasLocalBranch = false; + + get lastUpdatedDate() { + return new Date(this.pullRequest!.date); + } + + get assignees() { + const assignees = this.pullRequest?.assignees; + if (assignees == null) { + return []; + } + const author: PullRequestMember | undefined = this.pullRequest!.author; + if (author != null) { + return assignees.filter(assignee => assignee.name !== author.name); + } + + return assignees; + } + + override render() { + if (!this.pullRequest) return undefined; + + return html` + <gk-focus-row> + <span slot="rank">${this.rank}</span> + <gk-focus-item> + <span slot="type"><code-icon icon="git-pull-request"></code-icon></span> + <p> + <strong + >${this.pullRequest.title} + <a href="${this.pullRequest.url}">#${this.pullRequest.id}</a></strong + > + <!-- + <gk-badge>pending suggestions</gk-badge> --> + </p> + <p> + <gk-tag> + <span slot="prefix"><code-icon icon="source-control"></code-icon></span> + ${this.pullRequest.refs?.base.branch} + </gk-tag> + <gk-tag> + <span slot="prefix"><code-icon icon="repo"></code-icon></span> + ${this.pullRequest.refs?.base.repo} + </gk-tag> + <gk-additions-deletions> + <span slot="additions">${this.pullRequest.additions}</span> + <span slot="deletions">${this.pullRequest.deletions}</span> + </gk-additions-deletions> + </p> + <span slot="people"> + <gk-avatar-group> + ${when( + this.pullRequest.author != null, + () => html`<gk-avatar src="${this.pullRequest!.author.avatarUrl}"></gk-avatar>`, + )} + ${when( + this.assignees.length > 0, + () => html` + ${repeat( + this.assignees, + item => item.url, + (item, index) => html`<gk-avatar src="${item.avatarUrl}"></gk-avatar>`, + )} + `, + )} + </gk-avatar-group> + </span> + <span slot="date"> + <gk-date-from date="${this.lastUpdatedDate}"></gk-date-from> + </span> + <nav slot="actions"><gkc-button variant="ghost">Checkout branch</gkc-button></nav> + </gk-focus-item> + </gk-focus-row> + `; + } +} diff --git a/src/webviews/apps/plus/focus/components/pull-request-row.ts b/src/webviews/apps/plus/focus/components/pull-request-row.ts index 2be4dd4..0953c77 100644 --- a/src/webviews/apps/plus/focus/components/pull-request-row.ts +++ b/src/webviews/apps/plus/focus/components/pull-request-row.ts @@ -14,11 +14,11 @@ const template = html<PullRequestRow>` <template role="row"> <table-cell class="status"> ${when( - x => x.pullRequest!.isDraft === true, + x => x.pullRequest?.isDraft === true, html`<code-icon icon="git-pull-request-draft" title="draft"></code-icon>`, )} ${when( - x => x.pullRequest!.isDraft !== true, + x => x.pullRequest?.isDraft !== true, html`<code-icon class="pull-request-draft" icon="git-pull-request" title="open"></code-icon>`, )} ${when( @@ -295,27 +295,36 @@ export class PullRequestRow extends FASTElement { @volatile get lastUpdatedDate() { - return new Date(this.pullRequest!.date); + return this.pullRequest ? new Date(this.pullRequest.date) : undefined; } @volatile get lastUpdatedState() { + if (!this.lastUpdatedDate) { + return; + } return fromDateRange(this.lastUpdatedDate); } @volatile get lastUpdated() { + if (!this.lastUpdatedDate) { + return; + } return fromNow(this.lastUpdatedDate, true); } @volatile get lastUpdatedLabel() { + if (!this.lastUpdatedDate) { + return; + } return fromNow(this.lastUpdatedDate); } @volatile get lastUpdatedClass() { - switch (this.lastUpdatedState.status) { + switch (this.lastUpdatedState?.status) { case 'danger': return 'indicator-error'; case 'warning': diff --git a/src/webviews/apps/plus/focus/focus.html b/src/webviews/apps/plus/focus/focus.html index e178f03..1f47471 100644 --- a/src/webviews/apps/plus/focus/focus.html +++ b/src/webviews/apps/plus/focus/focus.html @@ -57,6 +57,33 @@ <main class="app__main"> <section class="focus-section app__section"> <header class="focus-section__header"> + <h2>Focus View</h2> + <nav class="tab-filter" id="filter-focus-items"> + <button class="tab-filter__tab is-active" type="button" data-tab="">All</button> + <button class="tab-filter__tab" type="button" data-tab="authored">Opened by Me</button> + <button class="tab-filter__tab" type="button" data-tab="assigned"> + Assigned to Me + </button> + <button class="tab-filter__tab" type="button" data-tab="review-requested"> + Needs my Review + </button> + <button class="tab-filter__tab" type="button" data-tab="mentioned">Mentions Me</button> + </nav> + </header> + <div class="focus-section__content"> + <gk-focus-container id="list-focus-items"></gk-focus-container> + <div class="alert" id="loading-focus-items"> + <span class="alert__content" + ><code-icon modifier="spin" icon="loading"></code-icon> Loading</span + > + </div> + <div class="alert" id="no-focus-items"> + <span class="alert__content">None found</span> + </div> + </div> + </section> + <!-- <section class="focus-section app__section"> + <header class="focus-section__header"> <h2>My Pull Requests</h2> <nav class="tab-filter" id="pr-filter"> <button class="tab-filter__tab is-active" type="button" data-tab="">All</button> @@ -80,8 +107,6 @@ <code-icon icon="gl-clock"></code-icon> </table-cell> <table-cell class="data-body" header="column" pinned>Pull Request</table-cell> - <!-- <table-cell class="data-author" header="column" pinned>Author</table-cell> - <table-cell class="data-assigned" header="column" pinned>Assigned</table-cell> --> <table-cell class="data-participants" header="column" pinned title="Participants"> <code-icon icon="organization"></code-icon> </table-cell> @@ -97,6 +122,7 @@ ></table-cell> </table-row> </table-container> + <gk-focus-container id="share-pull-requests"></gk-focus-container> <div class="alert" id="loading-pull-requests"> <span class="alert__content" ><code-icon modifier="spin" icon="loading"></code-icon> Loading</span @@ -129,8 +155,6 @@ <code-icon icon="gl-clock"></code-icon> </table-cell> <table-cell header="column" pinned>Title</table-cell> - <!-- <table-cell class="data-author" header="column" pinned>Author</table-cell> - <table-cell class="data-assigned" header="column" pinned>Assigned</table-cell> --> <table-cell class="data-participants" header="column" pinned title="Participants"> <code-icon icon="organization"></code-icon> </table-cell> @@ -154,7 +178,7 @@ <span class="alert__content">No issues found</span> </div> </div> - </section> + </section> --> </main> </div> </div> diff --git a/src/webviews/apps/plus/focus/focus.scss b/src/webviews/apps/plus/focus/focus.scss index e05a731..bd27fe1 100644 --- a/src/webviews/apps/plus/focus/focus.scss +++ b/src/webviews/apps/plus/focus/focus.scss @@ -22,6 +22,8 @@ body { --table-text: var(--color-foreground--65); --table-pinned-background: var(--color-background); --layout-gutter-outer: 20px; + + --gk-color-primary: var(--vscode-errorForeground); } .vscode-high-contrast, @@ -259,6 +261,11 @@ h3 { } } +gk-focus-view::part(base) { + font-size: 1.5rem; + font-weight: bold; +} + .app { display: flex; flex-direction: column; diff --git a/src/webviews/apps/plus/focus/focus.ts b/src/webviews/apps/plus/focus/focus.ts index 4e2304e..20fa902 100644 --- a/src/webviews/apps/plus/focus/focus.ts +++ b/src/webviews/apps/plus/focus/focus.ts @@ -1,3 +1,4 @@ +// import { FocusView } from '@gitkraken/shared-web-components'; import type { PullRequestShape } from '../../../../git/models/pullRequest'; import type { State } from '../../../../plus/webviews/focus/protocol'; import { @@ -11,6 +12,8 @@ import { App } from '../../shared/appBase'; import type { FeatureGate } from '../../shared/components/feature-gate'; import type { FeatureGateBadge } from '../../shared/components/feature-gate-badge'; import { DOM } from '../../shared/dom'; +import type { GkIssueRow } from './components/gk-issue-row'; +import type { GkPullRequestRow } from './components/gk-pull-request-row'; import type { IssueRow } from './components/issue-row'; import type { PullRequestRow } from './components/pull-request-row'; import '../../shared/components/button'; @@ -24,13 +27,17 @@ import '../../shared/components/table/table-cell'; import '../../shared/components/feature-gate-badge'; import './components/issue-row'; import './components/pull-request-row'; +import './components/gk-pull-request-row'; +import './components/gk-issue-row'; import './focus.scss'; +import '@gitkraken/shared-web-components'; export class FocusApp extends App<State> { constructor() { super('FocusApp'); } + private _focusFilter?: string; private _prFilter?: string; private _issueFilter?: string; @@ -54,6 +61,12 @@ export class FocusApp extends App<State> { this.renderIssues(); }), ), + DOM.on('#focus-filter [data-tab]', 'click', e => + this.onSelectTab(e, val => { + this._focusFilter = val; + this.renderIssues(); + }), + ), DOM.on<PullRequestRow, PullRequestShape>('pull-request-row', 'open-worktree', (e, target: HTMLElement) => this.onOpenWorktree(e, target), ), @@ -106,13 +119,65 @@ export class FocusApp extends App<State> { const $badge = document.getElementById('subscription-gate-badge')! as FeatureGateBadge; $badge.subscription = this.state.access.subscription.current; - this.renderPullRequests(); - this.renderIssues(); + // this.renderPullRequests(); + // this.renderIssues(); + this.renderFocusList(); + } + + renderFocusList() { + const tableEl = document.getElementById('list-focus-items'); + if (tableEl == null) return; + + tableEl.innerHTML = ''; + + const noneEl = document.getElementById('no-focus-items')!; + const loadingEl = document.getElementById('loading-focus-items')!; + if (this.state.access.allowed !== true || (this.state.pullRequests == null && this.state.issues == null)) { + noneEl.setAttribute('hidden', 'true'); + loadingEl.removeAttribute('hidden'); + } else if ( + (this.state.pullRequests == null || this.state.pullRequests.length === 0) && + (this.state.issues == null || this.state.issues.length === 0) + ) { + noneEl.removeAttribute('hidden'); + loadingEl.setAttribute('hidden', 'true'); + } else { + noneEl.setAttribute('hidden', 'true'); + loadingEl.setAttribute('hidden', 'true'); + let rank = 0; + this.state.pullRequests?.forEach( + ({ pullRequest, reasons, isCurrentBranch, isCurrentWorktree, hasWorktree, hasLocalBranch }, i) => { + if (this._focusFilter == null || this._focusFilter === '' || reasons.includes(this._focusFilter)) { + const rowEl = document.createElement('gk-pull-request-row') as GkPullRequestRow; + rowEl.pullRequest = pullRequest; + rowEl.rank = ++rank; + // rowEl2.reasons = reasons; + rowEl.isCurrentBranch = isCurrentBranch; + rowEl.isCurrentWorktree = isCurrentWorktree; + rowEl.hasWorktree = hasWorktree; + rowEl.hasLocalBranch = hasLocalBranch; + + tableEl.append(rowEl); + } + }, + ); + + this.state.issues?.forEach(({ issue, reasons }) => { + if (this._focusFilter == null || this._focusFilter === '' || reasons.includes(this._focusFilter)) { + const rowEl = document.createElement('gk-issue-row') as GkIssueRow; + rowEl.rank = ++rank; + rowEl.issue = issue; + + tableEl.append(rowEl); + } + }); + } } renderPullRequests() { const tableEl = document.getElementById('pull-requests'); if (tableEl == null) return; + const tableEl2 = document.getElementById('share-pull-requests')!; const rowEls = tableEl.querySelectorAll('pull-request-row'); rowEls.forEach(el => el.remove()); @@ -128,8 +193,9 @@ export class FocusApp extends App<State> { } else { noneEl.setAttribute('hidden', 'true'); loadingEl.setAttribute('hidden', 'true'); + tableEl2.innerHTML = ''; this.state.pullRequests.forEach( - ({ pullRequest, reasons, isCurrentBranch, isCurrentWorktree, hasWorktree, hasLocalBranch }) => { + ({ pullRequest, reasons, isCurrentBranch, isCurrentWorktree, hasWorktree, hasLocalBranch }, i) => { if (this._prFilter == null || this._prFilter === '' || reasons.includes(this._prFilter)) { const rowEl = document.createElement('pull-request-row') as PullRequestRow; rowEl.pullRequest = pullRequest; @@ -140,6 +206,17 @@ export class FocusApp extends App<State> { rowEl.hasLocalBranch = hasLocalBranch; tableEl.append(rowEl); + + const rowEl2 = document.createElement('gk-pull-request-row') as GkPullRequestRow; + rowEl2.pullRequest = pullRequest; + rowEl2.rank = i + 1; + // rowEl2.reasons = reasons; + rowEl2.isCurrentBranch = isCurrentBranch; + rowEl2.isCurrentWorktree = isCurrentWorktree; + rowEl2.hasWorktree = hasWorktree; + rowEl2.hasLocalBranch = hasLocalBranch; + + tableEl2.append(rowEl2); } }, ); @@ -151,6 +228,7 @@ export class FocusApp extends App<State> { const rowEls = tableEl.querySelectorAll('issue-row'); rowEls.forEach(el => el.remove()); + const tableEl2 = document.getElementById('share-pull-requests')!; const noneEl = document.getElementById('no-issues')!; const loadingEl = document.getElementById('loading-issues')!; @@ -170,6 +248,11 @@ export class FocusApp extends App<State> { rowEl.reasons = reasons; tableEl.append(rowEl); + + const rowEl2 = document.createElement('gk-issue-row') as GkIssueRow; + rowEl2.issue = issue; + + tableEl2.append(rowEl2); } }); } @@ -191,4 +274,7 @@ export class FocusApp extends App<State> { } } +// customElements.define(FocusView.tag, FocusView); +// FocusView.define(); + new FocusApp();