From 72036fddd6963f03a08c4ff244a851495925ca58 Mon Sep 17 00:00:00 2001 From: Keith Daulton <keith.daulton@gitkraken.com> Date: Tue, 25 Jul 2023 16:35:02 -0400 Subject: [PATCH] Updates focus view to Lit component --- .../apps/plus/focus/components/focus-app.ts | 211 +++++++++++++------- .../plus/focus/components/gk-pull-request-row.ts | 77 +++++++- src/webviews/apps/plus/focus/focus.scss | 2 + src/webviews/apps/plus/focus/focus.ts | 217 ++------------------- 4 files changed, 236 insertions(+), 271 deletions(-) diff --git a/src/webviews/apps/plus/focus/components/focus-app.ts b/src/webviews/apps/plus/focus/components/focus-app.ts index 72f9da3..ad9c88d 100644 --- a/src/webviews/apps/plus/focus/components/focus-app.ts +++ b/src/webviews/apps/plus/focus/components/focus-app.ts @@ -1,13 +1,23 @@ import { html, LitElement } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; +import { map } from 'lit/directives/map.js'; import { repeat } from 'lit/directives/repeat.js'; import { when } from 'lit/directives/when.js'; import type { State } from '../../../../../plus/webviews/focus/protocol'; +import { debounce } from '../../../../../system/function'; import type { FeatureGate } from '../../../shared/components/feature-gate'; import type { FeatureGateBadge } from '../../../shared/components/feature-gate-badge'; @customElement('gl-focus-app') export class GlFocusApp extends LitElement { + private readonly tabFilters = ['authored', 'assigned', 'review-requested', 'mentioned']; + private readonly tabFilterOptions = [ + { label: 'All', value: '' }, + { label: 'Opened by Me', value: 'authored' }, + { label: 'Assigned to Me', value: 'assigned' }, + { label: 'Needs my Review', value: 'review-requested' }, + { label: 'Mentions Me', value: 'mentioned' }, + ]; @query('#subscription-gate', true) private subscriptionEl!: FeatureGate; @@ -18,10 +28,10 @@ export class GlFocusApp extends LitElement { private subScriptionBadgeEl!: FeatureGateBadge; @state() - private focusFilter?: string; + private selectedTabFilter?: string; @state() - private loading = true; + private searchText?: string; @property({ type: Object }) state?: State; @@ -38,44 +48,80 @@ export class GlFocusApp extends LitElement { return this.state?.access.allowed === true && !(this.state?.repos?.some(r => r.isConnected) ?? false); } - get filteredItems() { - const items: { isPullrequest: boolean; rank: number; state: Record<string, any> }[] = []; + get items() { + if (this.isLoading) { + return []; + } + + const items: { isPullrequest: boolean; rank: number; state: Record<string, any>; reasons: string[] }[] = []; let rank = 0; this.state?.pullRequests?.forEach( - ({ pullRequest, reasons, isCurrentBranch, isCurrentWorktree, hasWorktree, hasLocalBranch }, i) => { - if (this.focusFilter == null || this.focusFilter === '' || reasons.includes(this.focusFilter)) { - items.push({ - isPullrequest: true, - state: { - pullRequest: pullRequest, - // reasons: reasons, - isCurrentBranch: isCurrentBranch, - isCurrentWorktree: isCurrentWorktree, - hasWorktree: hasWorktree, - hasLocalBranch: hasLocalBranch, - }, - rank: ++rank, - }); - } - }, - ); - - this.state?.issues?.forEach(({ issue, reasons }) => { - if (this.focusFilter == null || this.focusFilter === '' || reasons.includes(this.focusFilter)) { + ({ pullRequest, reasons, isCurrentBranch, isCurrentWorktree, hasWorktree, hasLocalBranch }) => { items.push({ - isPullrequest: false, - rank: ++rank, + isPullrequest: true, state: { - issue: issue, + pullRequest: pullRequest, + isCurrentBranch: isCurrentBranch, + isCurrentWorktree: isCurrentWorktree, + hasWorktree: hasWorktree, + hasLocalBranch: hasLocalBranch, }, + rank: ++rank, + reasons: reasons, }); - } + }, + ); + + this.state?.issues?.forEach(({ issue, reasons }) => { + items.push({ + isPullrequest: false, + rank: ++rank, + state: { + issue: issue, + }, + reasons: reasons, + }); }); return items; } + get filteredItems() { + if (this.items.length === 0) { + return this.items; + } + + const hasSearch = this.searchText != null && this.searchText !== ''; + const hasTabFilter = this.selectedTabFilter != null && this.selectedTabFilter !== ''; + if (!hasSearch && !hasTabFilter) { + return this.items; + } + + const searchText = this.searchText?.toLowerCase(); + return this.items.filter(i => { + if (hasTabFilter && !i.reasons.includes(this.selectedTabFilter!)) { + return false; + } + + if (hasSearch) { + if (i.state.issue && !i.state.issue.title.toLowerCase().includes(searchText)) { + return false; + } + + if (i.state.pullRequest && !i.state.pullRequest.title.toLowerCase().includes(searchText)) { + return false; + } + } + + return true; + }); + } + + get isLoading() { + return this.state?.pullRequests == null || this.state?.issues == null; + } + override render() { if (this.state == null) { return undefined; @@ -123,19 +169,25 @@ export class GlFocusApp extends LitElement { <header class="focus-section__header"> <div class="focus-section__header-group"> <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> + ${map( + this.tabFilterOptions, + ({ label, value }, i) => html` + <button + class="tab-filter__tab ${( + this.selectedTabFilter + ? value === this.selectedTabFilter + : i === 0 + ) + ? 'is-active' + : ''}" + type="button" + data-tab="${value}" + @click=${() => (this.selectedTabFilter = value)} + > + ${label} + </button> + `, + )} </nav> </div> <div class="focus-section__header-group"> @@ -144,6 +196,7 @@ export class GlFocusApp extends LitElement { label="Search field" label-visibility="sr-only" placeholder="Search" + @input=${debounce(this.onSearchInput.bind(this), 200)} > <code-icon slot="prefix" icon="search"></code-icon> </gk-input> @@ -152,42 +205,46 @@ export class GlFocusApp extends LitElement { <div class="focus-section__content"> <gk-focus-container id="list-focus-items"> ${when( - this.filteredItems.length > 0, - () => html` - ${repeat( - this.filteredItems, - item => item.rank, - ({ isPullrequest, rank, state }) => - when( - isPullrequest, - () => - html`<gk-pull-request-row - .rank=${rank} - .pullRequest=${state.pullRequest} - ></gk-pull-request-row>`, - () => - html`<gk-issue-row - .rank=${rank} - .issue=${state.issue} - ></gk-issue-row>`, - ), - )} - `, + this.isLoading, () => html` <div class="alert"> - <span class="alert__content">None found</span> + <span class="alert__content" + ><code-icon modifier="spin" icon="loading"></code-icon> + Loading</span + > </div> `, + () => + when( + this.filteredItems.length > 0, + () => html` + ${repeat( + this.filteredItems, + item => item.rank, + ({ isPullrequest, rank, state }) => + when( + isPullrequest, + () => + html`<gk-pull-request-row + .rank=${rank} + .pullRequest=${state.pullRequest} + ></gk-pull-request-row>`, + () => + html`<gk-issue-row + .rank=${rank} + .issue=${state.issue} + ></gk-issue-row>`, + ), + )} + `, + () => html` + <div class="alert"> + <span class="alert__content">None found</span> + </div> + `, + ), )} </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> </main> @@ -196,6 +253,18 @@ export class GlFocusApp extends LitElement { `; } + onSearchInput(e: Event) { + const input = e.target as HTMLInputElement; + const value = input.value; + + if (value === '' || value.length < 3) { + this.searchText = undefined; + return; + } + + this.searchText = value; + } + protected override createRenderRoot() { return this; } 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 index c88ea2b..b5a99eb 100644 --- a/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts +++ b/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts @@ -67,6 +67,19 @@ export class GkPullRequestRow extends LitElement { .actions a code-icon { font-size: 1.6rem; } + + .indicator-info { + color: var(--vscode-problemsInfoIcon-foreground); + } + .indicator-warning { + color: var(--vscode-problemsWarningIcon-foreground); + } + .indicator-error { + color: var(--vscode-problemsErrorIcon-foreground); + } + .indicator-neutral { + color: var(--color-alert-neutralBorder); + } `, ]; @@ -105,6 +118,20 @@ export class GkPullRequestRow extends LitElement { return assignees; } + get indicator() { + if (this.pullRequest == null) return ''; + + if (this.pullRequest.reviewDecision === 'ChangesRequested') { + return 'changes'; + } else if (this.pullRequest.reviewDecision === 'Approved' && this.pullRequest.mergeableState === 'Mergeable') { + return 'ready'; + } else if (this.pullRequest.mergeableState === 'Conflicting') { + return 'conflicting'; + } + + return ''; + } + get dateStyle() { return `indicator-${fromDateRange(this.lastUpdatedDate).status}`; } @@ -114,7 +141,36 @@ export class GkPullRequestRow extends LitElement { return html` <gk-focus-row> - <span slot="rank">${this.rank}</span> + <span slot="rank"> + ${this.rank} + ${when( + this.indicator === 'changes', + () => + html`<code-icon + class="indicator-error" + icon="request-changes" + title="changes requested" + ></code-icon>`, + )} + ${when( + this.indicator === 'ready', + () => + html`<code-icon + class="indicator-info" + icon="pass" + title="approved and ready to merge" + ></code-icon>`, + )} + ${when( + this.indicator === 'conflicting', + () => + html`<code-icon + class="indicator-error" + icon="bracket-error" + title="cannot be merged due to merge conflicts" + ></code-icon>`, + )} + </span> <gk-focus-item> <span slot="type"><code-icon icon="git-pull-request"></code-icon></span> <p> @@ -190,6 +246,21 @@ export class GkPullRequestRow extends LitElement { `; } - public onOpenWorktreeClick(e: MouseEvent) {} - public onSwitchBranchClick(e: MouseEvent) {} + onOpenWorktreeClick(e: Event) { + if (this.isCurrentWorktree) { + e.preventDefault(); + e.stopImmediatePropagation(); + return; + } + this.dispatchEvent(new CustomEvent('open-worktree', { detail: this.pullRequest! })); + } + + onSwitchBranchClick(e: Event) { + if (this.isCurrentBranch) { + e.preventDefault(); + e.stopImmediatePropagation(); + return; + } + this.dispatchEvent(new CustomEvent('switch-branch', { detail: this.pullRequest! })); + } } diff --git a/src/webviews/apps/plus/focus/focus.scss b/src/webviews/apps/plus/focus/focus.scss index 5c50751..574d5c4 100644 --- a/src/webviews/apps/plus/focus/focus.scss +++ b/src/webviews/apps/plus/focus/focus.scss @@ -25,6 +25,8 @@ body { --gk-input-background-color: var(--vscode-input-background); --gk-input-border-color: var(--vscode-input-border); --gk-input-color: var(--vscode-input-foreground); + --gk-text-secondary-color: var(--color-foreground--65); + --gk-button-ghost-color: var(--color-foreground--50); } .vscode-high-contrast, diff --git a/src/webviews/apps/plus/focus/focus.ts b/src/webviews/apps/plus/focus/focus.ts index bd3291a..23a7322 100644 --- a/src/webviews/apps/plus/focus/focus.ts +++ b/src/webviews/apps/plus/focus/focus.ts @@ -44,8 +44,6 @@ export class FocusApp extends App<State> { private _issueFilter?: string; override onInitialize() { - this.renderContent(); - this.attachState(); } @@ -53,37 +51,31 @@ export class FocusApp extends App<State> { const disposables = super.onBind?.() ?? []; disposables.push( - // DOM.on('#pr-filter [data-tab]', 'click', e => - // this.onSelectTab(e, val => { - // this._prFilter = val; - // this.renderPullRequests(); - // }), - // ), - // DOM.on('#issue-filter [data-tab]', 'click', e => - // this.onSelectTab(e, val => { - // this._issueFilter = val; - // this.renderIssues(); - // }), - // ), - DOM.on('#filter-focus-items [data-tab]', 'click', e => - this.onSelectTab(e, val => { - this._focusFilter = val; - this.renderFocusList(); - }), + DOM.on<GkPullRequestRow, PullRequestShape>( + 'gk-pull-request-row', + 'open-worktree', + (e, target: HTMLElement) => this.onOpenWorktree(e, target), + ), + DOM.on<GkPullRequestRow, PullRequestShape>( + 'gk-pull-request-row', + 'switch-branch', + (e, target: HTMLElement) => this.onSwitchBranch(e, target), ), - // DOM.on<PullRequestRow, PullRequestShape>('pull-request-row', 'open-worktree', (e, target: HTMLElement) => - // this.onOpenWorktree(e, target), - // ), - // DOM.on<PullRequestRow, PullRequestShape>('pull-request-row', 'switch-branch', (e, target: HTMLElement) => - // this.onSwitchBranch(e, target), - // ), ); return disposables; } + private _component?: GlFocusApp; + private get component() { + if (this._component == null) { + this._component = (document.getElementById('app') as GlFocusApp)!; + } + return this._component; + } + attachState() { - (document.getElementById('app') as GlFocusApp)!.state = this.state; + this.component.state = this.state; } private onSwitchBranch(e: CustomEvent<PullRequestShape>, _target: HTMLElement) { @@ -105,181 +97,12 @@ export class FocusApp extends App<State> { onIpc(DidChangeNotificationType, msg, params => { this.state = params.state; this.setState(this.state); - this.renderContent(); + // this.renderContent(); + this.attachState(); }); break; } } - - renderContent() { - // let $gate = document.getElementById('subscription-gate')! as FeatureGate; - // if ($gate != null) { - // $gate.state = this.state.access.subscription.current.state; - // $gate.visible = this.state.access.allowed !== true; - // } - - // $gate = document.getElementById('connection-gate')! as FeatureGate; - // if ($gate != null) { - // $gate.visible = - // this.state.access.allowed === true && !(this.state.repos?.some(r => r.isConnected) ?? false); - // } - - // const $badge = document.getElementById('subscription-gate-badge')! as FeatureGateBadge; - // $badge.subscription = this.state.access.subscription.current; - - // 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()); - - // const noneEl = document.getElementById('no-pull-requests')!; - // const loadingEl = document.getElementById('loading-pull-requests')!; - // if (this.state.pullRequests == null || this.state.access.allowed !== true) { - // noneEl.setAttribute('hidden', 'true'); - // loadingEl.removeAttribute('hidden'); - // } else if (this.state.pullRequests.length === 0) { - // noneEl.removeAttribute('hidden'); - // loadingEl.setAttribute('hidden', 'true'); - // } else { - // noneEl.setAttribute('hidden', 'true'); - // loadingEl.setAttribute('hidden', 'true'); - // tableEl2.innerHTML = ''; - // this.state.pullRequests.forEach( - // ({ 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; - // rowEl.reasons = reasons; - // rowEl.isCurrentBranch = isCurrentBranch; - // rowEl.isCurrentWorktree = isCurrentWorktree; - // rowEl.hasWorktree = hasWorktree; - // 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); - // } - // }, - // ); - // } - // } - - // renderIssues() { - // const tableEl = document.getElementById('issues')!; - - // 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')!; - // if (this.state.issues == null || this.state.access.allowed !== true) { - // noneEl.setAttribute('hidden', 'true'); - // loadingEl.removeAttribute('hidden'); - // } else if (this.state.issues.length === 0) { - // noneEl.removeAttribute('hidden'); - // loadingEl.setAttribute('hidden', 'true'); - // } else { - // noneEl.setAttribute('hidden', 'true'); - // loadingEl.setAttribute('hidden', 'true'); - // this.state.issues.forEach(({ issue, reasons }) => { - // if (this._issueFilter == null || this._issueFilter === '' || reasons.includes(this._issueFilter)) { - // const rowEl = document.createElement('issue-row') as IssueRow; - // rowEl.issue = issue; - // rowEl.reasons = reasons; - - // tableEl.append(rowEl); - - // const rowEl2 = document.createElement('gk-issue-row') as GkIssueRow; - // rowEl2.issue = issue; - - // tableEl2.append(rowEl2); - // } - // }); - // } - // } - - onSelectTab(e: Event, callback?: (val?: string) => void) { - const thisEl = e.target as HTMLElement; - const tab = thisEl.dataset.tab!; - - thisEl.parentElement?.querySelectorAll('[data-tab]')?.forEach(el => { - if ((el as HTMLElement).dataset.tab !== tab) { - el.classList.remove('is-active'); - } else { - el.classList.add('is-active'); - } - }); - - callback?.(tab); - } } // customElements.define(FocusView.tag, FocusView);