diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index c80c3e0..8342049 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -78,7 +78,7 @@ import { arePlusFeaturesEnabled, ensurePlusFeaturesEnabled } from '../../subscri import type { DimMergeCommitsParams, DismissBannerParams, - DoubleClickedRefParams, + DoubleClickedParams, EnsureRowParams, GetMissingAvatarsParams, GetMissingRefsMetadataParams, @@ -125,7 +125,7 @@ import { DidSearchNotificationType, DimMergeCommitsCommandType, DismissBannerCommandType, - DoubleClickedRefCommandType, + DoubleClickedCommandType, EnsureRowCommandType, GetMissingAvatarsCommandType, GetMissingRefsMetadataCommandType, @@ -415,8 +415,8 @@ export class GraphWebview extends WebviewBase { case DismissBannerCommandType.method: onIpc(DismissBannerCommandType, e, params => this.dismissBanner(params)); break; - case DoubleClickedRefCommandType.method: - onIpc(DoubleClickedRefCommandType, e, params => this.onDoubleClickRef(params)); + case DoubleClickedCommandType.method: + onIpc(DoubleClickedCommandType, e, params => this.onDoubleClick(params)); break; case EnsureRowCommandType.method: onIpc(EnsureRowCommandType, e, params => this.onEnsureRow(params, e.completionId)); @@ -624,8 +624,8 @@ export class GraphWebview extends WebviewBase { this.updateExcludedRefs(this._graph, e.refs, e.visible); } - private onDoubleClickRef(e: DoubleClickedRefParams) { - if (e.ref.context) { + private onDoubleClick(e: DoubleClickedParams) { + if (e.type === 'ref' && e.ref.context) { const item = typeof e.ref.context === 'string' ? JSON.parse(e.ref.context) : e.ref.context; if (!('webview' in item)) { item.webview = this.id; @@ -643,6 +643,9 @@ export class GraphWebview extends WebviewBase { configuration.isUnset('gitCommands.skipConfirmations') ? true : undefined, ); } + } else if (e.type === 'row' && e.row) { + const commit = this.getRevisionReference(this.repository?.path, e.row.id, e.row.type); + if (commit != null) return GitActions.Commit.showDetailsView(commit, { preserveFocus: e.preserveFocus }); } return Promise.resolve(); @@ -931,22 +934,8 @@ export class GraphWebview extends WebviewBase { private fireSelectionChanged(id: string | undefined, type: GitGraphRowType | undefined) { if (this.repository == null) return; - let commits: GitRevisionReference[] | undefined; - if (id != null) { - if (type === GitGraphRowType.Stash) { - commits = [ - GitReference.create(id, this.repository.path, { - refType: 'stash', - name: id, - number: undefined, - }), - ]; - } else if (type === GitGraphRowType.Working) { - commits = [GitReference.create(GitRevision.uncommitted, this.repository.path, { refType: 'revision' })]; - } else { - commits = [GitReference.create(id, this.repository.path, { refType: 'revision' })]; - } - } + const commit = this.getRevisionReference(this.repository.path, id, type); + const commits = commit != null ? [commit] : undefined; this._selection = commits; this._onDidChangeSelection.fire({ selection: commits ?? [] }); @@ -965,6 +954,29 @@ export class GraphWebview extends WebviewBase { private _notifyDidChangeStateDebounced: Deferrable | undefined = undefined; + private getRevisionReference( + repoPath: string | undefined, + id: string | undefined, + type: GitGraphRowType | undefined, + ) { + if (repoPath == null || id == null) return undefined; + + switch (type) { + case GitGraphRowType.Stash: + return GitReference.create(id, repoPath, { + refType: 'stash', + name: id, + number: undefined, + }); + + case GitGraphRowType.Working: + return GitReference.create(GitRevision.uncommitted, repoPath, { refType: 'revision' }); + + default: + return GitReference.create(id, repoPath, { refType: 'revision' }); + } + } + @debug() private updateState(immediate: boolean = false) { this._pendingIpcNotifications.clear(); diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index 7645bd6..6898b97 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -154,10 +154,17 @@ export interface DismissBannerParams { } export const DismissBannerCommandType = new IpcCommandType('graph/dismissBanner'); -export interface DoubleClickedRefParams { - ref: GraphRef; -} -export const DoubleClickedRefCommandType = new IpcCommandType('graph/ref/doubleclick'); +export type DoubleClickedParams = + | { + type: 'ref'; + ref: GraphRef; + } + | { + type: 'row'; + row: { id: string; type: GitGraphRowType }; + preserveFocus?: boolean; + }; +export const DoubleClickedCommandType = new IpcCommandType('graph/dblclick'); export interface EnsureRowParams { id: string; @@ -228,6 +235,7 @@ export interface UpdateSelectionParams { export const UpdateSelectionCommandType = new IpcCommandType('graph/selection/update'); // Notifications + export interface DidChangeParams { state: State; } diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 2c107ac..838a5d1 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -1,4 +1,4 @@ -import GraphContainer from '@gitkraken/gitkraken-components'; +import GraphContainer, { GRAPH_ZONE_TYPE, REF_ZONE_TYPE } from '@gitkraken/gitkraken-components'; import type { GraphColumnSetting, GraphColumnsSettings, @@ -8,6 +8,7 @@ import type { GraphRefGroup, GraphRefOptData, GraphRow, + GraphZoneType, OnFormatCommitDateTime, } from '@gitkraken/gitkraken-components'; import { VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup } from '@vscode/webview-ui-toolkit/react'; @@ -69,6 +70,7 @@ export interface GraphWrapperProps { onColumnsChange?: (colsSettings: GraphColumnsConfig) => void; onDimMergeCommits?: (dim: boolean) => void; onDoubleClickRef?: (ref: GraphRef) => void; + onDoubleClickRow?: (row: GraphRow, preserveFocus?: boolean) => void; onMissingAvatars?: (emails: { [email: string]: string }) => void; onMissingRefsMetadata?: (metadata: GraphMissingRefsMetadata) => void; onMoreRows?: (id?: string) => void; @@ -144,6 +146,7 @@ export function GraphWrapper({ onColumnsChange, onDimMergeCommits, onDoubleClickRef, + onDoubleClickRow, onEnsureRowPromise, onMissingAvatars, onMissingRefsMetadata, @@ -309,6 +312,30 @@ export function GraphWrapper({ useEffect(() => subscriber?.(updateState), []); useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [activeRow]); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + const sha = getActiveRowInfo(activeRow ?? state.activeRow)?.id; + if (sha == null) return; + + // TODO@eamodio a bit of a hack since the graph container ref isn't exposed in the types + const graph = (graphRef.current as any)?.graphContainerRef.current; + if (!e.composedPath().some(el => el === graph)) return; + + const row = rows.find(r => r.sha === sha); + if (row == null) return; + + onDoubleClickRow?.(row, e.key !== 'Enter'); + } + }; + + useEffect(() => { if (searchResultsError != null || searchResults == null || searchResults.count === 0 || searchQuery == null) { return; } @@ -573,14 +600,25 @@ export function GraphWrapper({ }; const handleOnDoubleClickRef = ( - event: React.MouseEvent, + _event: React.MouseEvent, refGroup: GraphRefGroup, + _row: GraphRow, ) => { if (refGroup.length > 0) { onDoubleClickRef?.(refGroup[0]); } }; + const handleOnDoubleClickRow = ( + _event: React.MouseEvent, + graphZoneType: GraphZoneType, + row: GraphRow, + ) => { + if (graphZoneType === REF_ZONE_TYPE || graphZoneType === GRAPH_ZONE_TYPE) return; + + onDoubleClickRow?.(row, true); + }; + const handleSelectGraphRows = (rows: GraphRow[]) => { const active = rows[0]; const activeKey = active != null ? `${active.sha}|${active.date}` : undefined; @@ -986,6 +1024,7 @@ export function GraphWrapper({ isSelectedBySha={selectedRows} nonce={nonce} onColumnResized={handleOnColumnResized} + onDoubleClickGraphRow={handleOnDoubleClickRow} onDoubleClickGraphRef={handleOnDoubleClickRef} onGraphColumnsReOrdered={handleOnGraphColumnsReOrdered} onSelectGraphRows={handleSelectGraphRows} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index 8c2bef2..c7f2972 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -33,7 +33,7 @@ import { DidSearchNotificationType, DimMergeCommitsCommandType, DismissBannerCommandType, - DoubleClickedRefCommandType, + DoubleClickedCommandType, EnsureRowCommandType, GetMissingAvatarsCommandType, GetMissingRefsMetadataCommandType, @@ -76,6 +76,7 @@ export class GraphApp extends App { protected override onBind() { const disposables = super.onBind?.() ?? []; + // disposables.push(DOM.on(window, 'keyup', e => this.onKeyUp(e))); this.log(`${this.appName}.onBind`); @@ -98,6 +99,7 @@ export class GraphApp extends App { } onChooseRepository={debounce(() => this.onChooseRepository(), 250)} onDoubleClickRef={ref => this.onDoubleClickRef(ref)} + onDoubleClickRow={(row, preserveFocus) => this.onDoubleClickRow(row, preserveFocus)} onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)} onMissingRefsMetadata={(...params) => this.onGetMissingRefsMetadata(...params)} onMoreRows={(...params) => this.onGetMoreRows(...params)} @@ -123,6 +125,15 @@ export class GraphApp extends App { return disposables; } + // private onKeyUp(e: KeyboardEvent) { + // if (e.key === 'Enter' || e.key === ' ') { + // const inputFocused = e.composedPath().some(el => (el as HTMLElement).tagName === 'INPUT'); + // if (!inputFocused) return; + + // const $target = e.target as HTMLElement; + // } + // } + protected override onMessageReceived(e: MessageEvent) { const msg = e.data as IpcMessage; this.log(`${this.appName}.onMessageReceived(${msg.id}): name=${msg.method}`); @@ -422,11 +433,20 @@ export class GraphApp extends App { } private onDoubleClickRef(ref: GraphRef) { - this.sendCommand(DoubleClickedRefCommandType, { + this.sendCommand(DoubleClickedCommandType, { + type: 'ref', ref: ref, }); } + private onDoubleClickRow(row: GraphRow, preserveFocus?: boolean) { + this.sendCommand(DoubleClickedCommandType, { + type: 'row', + row: { id: row.sha, type: row.type as GitGraphRowType }, + preserveFocus: preserveFocus, + }); + } + private onGetMissingAvatars(emails: GraphAvatars) { this.sendCommand(GetMissingAvatarsCommandType, { emails: emails }); }