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);