From 07a9f342e6ddc4c6d0c584d1e69f6d182f231b01 Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Wed, 20 Nov 2024 16:18:53 +0700 Subject: [PATCH] Implements actions-list component --- contributions.json | 117 +++++++++++++ package.json | 126 ++++++++++++++ src/constants.commands.ts | 2 + src/webviews/apps/home/home.html | 2 +- .../apps/plus/home/components/active-work.ts | 45 ++--- .../apps/plus/home/components/branch-card.ts | 157 ++++++++++------- .../plus/home/components/branch-section.ts | 47 +++++- .../apps/plus/home/components/overview.ts | 28 ++++ .../shared/components/actions/action-item.ts | 44 ++--- .../shared/components/actions/action-list.ts | 158 ++++++++++++++++++ src/webviews/home/homeWebview.ts | 27 ++- 11 files changed, 629 insertions(+), 124 deletions(-) create mode 100644 src/webviews/apps/shared/components/actions/action-list.ts diff --git a/contributions.json b/contributions.json index 484a402985702..de0dd0b51bb8a 100644 --- a/contributions.json +++ b/contributions.json @@ -2905,6 +2905,123 @@ ] } }, + "gitlens.home.createPullRequest": { + "label": "Create Pull Request...", + "icon": "$(git-pull-request-create)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.createPullRequest\\b)/", + "group": "30_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.fetch": { + "label": "Fetch", + "icon": "$(gl-repo-fetch)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.fetch\\b)/", + "group": "60_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.openInGraph": { + "label": "Open in Commit Graph", + "icon": "$(gl-graph)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openInGraph\\b)/", + "group": "80_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.openPullRequestChanges": { + "label": "Open Pull Request Changes", + "icon": "$(request-changes)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openPullRequestChanges\\b)/", + "group": "10_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.openPullRequestOnRemote": { + "label": "Open Pull Request on Remote", + "icon": "$(globe)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openPullRequestOnRemote\\b)/", + "group": "20_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.openWorktree": { + "label": "Open Worktree", + "icon": "$(browser)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openWorktree\\b)/", + "group": "40_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.openWorktreeInNewWindow": { + "label": "Open Worktree in New Window", + "icon": "$(empty-window)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openWorktreeInNewWindow\\b)/", + "group": "40_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.pull": { + "label": "Pull", + "icon": "$(gl-repo-pull)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.pull\\b)/", + "group": "60_gitlens_action", + "order": 1 + } + ] + } + }, + "gitlens.home.switchToBranch": { + "label": "Switch to Branch...", + "icon": "$(gl-switch)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.switchToBranch\\b)/", + "group": "50_gitlens_action", + "order": 1 + } + ] + } + }, "gitlens.inviteToLiveShare": { "label": "Invite to Live Share", "icon": "$(live-share)", diff --git a/package.json b/package.json index bea17442b8f3f..569152270cae4 100644 --- a/package.json +++ b/package.json @@ -6868,6 +6868,51 @@ "icon": "$(discard)", "enablement": "!operationInProgress" }, + { + "command": "gitlens.home.createPullRequest", + "title": "Create Pull Request...", + "icon": "$(git-pull-request-create)" + }, + { + "command": "gitlens.home.fetch", + "title": "Fetch", + "icon": "$(gl-repo-fetch)" + }, + { + "command": "gitlens.home.openInGraph", + "title": "Open in Commit Graph", + "icon": "$(gl-graph)" + }, + { + "command": "gitlens.home.openPullRequestChanges", + "title": "Open Pull Request Changes", + "icon": "$(request-changes)" + }, + { + "command": "gitlens.home.openPullRequestOnRemote", + "title": "Open Pull Request on Remote", + "icon": "$(globe)" + }, + { + "command": "gitlens.home.openWorktree", + "title": "Open Worktree", + "icon": "$(browser)" + }, + { + "command": "gitlens.home.openWorktreeInNewWindow", + "title": "Open Worktree in New Window", + "icon": "$(empty-window)" + }, + { + "command": "gitlens.home.pull", + "title": "Pull", + "icon": "$(gl-repo-pull)" + }, + { + "command": "gitlens.home.switchToBranch", + "title": "Switch to Branch...", + "icon": "$(gl-switch)" + }, { "command": "gitlens.inviteToLiveShare", "title": "Invite to Live Share", @@ -10781,6 +10826,42 @@ "command": "gitlens.graph.undoCommit", "when": "false" }, + { + "command": "gitlens.home.createPullRequest", + "when": "false" + }, + { + "command": "gitlens.home.fetch", + "when": "false" + }, + { + "command": "gitlens.home.openInGraph", + "when": "false" + }, + { + "command": "gitlens.home.openPullRequestChanges", + "when": "false" + }, + { + "command": "gitlens.home.openPullRequestOnRemote", + "when": "false" + }, + { + "command": "gitlens.home.openWorktree", + "when": "false" + }, + { + "command": "gitlens.home.openWorktreeInNewWindow", + "when": "false" + }, + { + "command": "gitlens.home.pull", + "when": "false" + }, + { + "command": "gitlens.home.switchToBranch", + "when": "false" + }, { "command": "gitlens.inviteToLiveShare", "when": "false" @@ -18692,6 +18773,51 @@ "when": "webviewItem =~ /gitlens:graph:(columns|settings)\\b/", "group": "3_columns@2" }, + { + "command": "gitlens.home.openPullRequestChanges", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openPullRequestChanges\\b)/", + "group": "10_gitlens_action@1" + }, + { + "command": "gitlens.home.openPullRequestOnRemote", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openPullRequestOnRemote\\b)/", + "group": "20_gitlens_action@1" + }, + { + "command": "gitlens.home.createPullRequest", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.createPullRequest\\b)/", + "group": "30_gitlens_action@1" + }, + { + "command": "gitlens.home.openWorktree", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openWorktree\\b)/", + "group": "40_gitlens_action@1" + }, + { + "command": "gitlens.home.openWorktreeInNewWindow", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openWorktreeInNewWindow\\b)/", + "group": "40_gitlens_action@1" + }, + { + "command": "gitlens.home.switchToBranch", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.switchToBranch\\b)/", + "group": "50_gitlens_action@1" + }, + { + "command": "gitlens.home.fetch", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.fetch\\b)/", + "group": "60_gitlens_action@1" + }, + { + "command": "gitlens.home.pull", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.pull\\b)/", + "group": "60_gitlens_action@1" + }, + { + "command": "gitlens.home.openInGraph", + "when": "webviewItem =~ /gitlens:home\\b(?=.*?\\b\\+.*gitlens\\.home\\.openInGraph\\b)/", + "group": "80_gitlens_action@1" + }, { "command": "gitlens.graph.openPullRequestChanges", "when": "webviewItem =~ /gitlens:pullrequest\\b(?=.*?\\b\\+refs\\b)/ && config.multiDiffEditor.experimental.enabled", diff --git a/src/constants.commands.ts b/src/constants.commands.ts index ec80afb20dc1c..a2695f9a38202 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -692,8 +692,10 @@ type HomeWebviewCommands = `home.${ | 'openPullRequestDetails' | 'createPullRequest' | 'openWorktree' + | 'openWorktreeInNewWindow' | 'switchToBranch' | 'fetch' + | 'pull' | 'openInGraph' | 'createBranch' | 'mergeIntoCurrent' diff --git a/src/webviews/apps/home/home.html b/src/webviews/apps/home/home.html index 75db0cbb1bbd5..50bc7413ac406 100644 --- a/src/webviews/apps/home/home.html +++ b/src/webviews/apps/home/home.html @@ -19,7 +19,7 @@ diff --git a/src/webviews/apps/plus/home/components/active-work.ts b/src/webviews/apps/plus/home/components/active-work.ts index d56c3e3dcec40..4be1042be2f27 100644 --- a/src/webviews/apps/plus/home/components/active-work.ts +++ b/src/webviews/apps/plus/home/components/active-work.ts @@ -9,14 +9,10 @@ import { createCommandLink } from '../../../../../system/commands'; import { createWebviewCommandLink } from '../../../../../system/webview'; import type { GetOverviewBranch, OpenInGraphParams, State } from '../../../../home/protocol'; import { stateContext } from '../../../home/context'; -import { linkStyles } from '../../shared/components/vscode.css'; -import { branchCardStyles, GlBranchCardBase } from './branch-card'; -import type { Overview, OverviewState } from './overviewState'; -import { overviewStateContext } from './overviewState'; +import type { ActionList } from '../../../shared/components/actions/action-list'; import '../../../shared/components/button'; -import '../../../shared/components/code-icon'; -import '../../../shared/components/skeleton-loader'; import '../../../shared/components/card/card'; +import '../../../shared/components/code-icon'; import '../../../shared/components/commit/commit-stats'; import '../../../shared/components/menu/menu-item'; import '../../../shared/components/overlays/popover'; @@ -24,7 +20,12 @@ import '../../../shared/components/overlays/tooltip'; import '../../../shared/components/pills/tracking'; import '../../../shared/components/rich/issue-icon'; import '../../../shared/components/rich/pr-icon'; +import '../../../shared/components/skeleton-loader'; import '../../shared/components/merge-rebase-status'; +import { linkStyles } from '../../shared/components/vscode.css'; +import { branchCardStyles, GlBranchCardBase } from './branch-card'; +import type { Overview, OverviewState } from './overviewState'; +import { overviewStateContext } from './overviewState'; export const activeWorkTagName = 'gl-active-work'; @@ -376,23 +377,23 @@ export class GlActiveBranchCard extends GlBranchCardBase { return []; } - protected getPrActions() { + protected getPrActions(): (typeof ActionList.ItemProps)[] { return [ - html``, - html``, - html``, + { + label: 'Open Pull Request Changes', + icon: 'request-changes', + href: this.createCommandLink('gitlens.home.openPullRequestChanges'), + }, + { + label: 'Compare Pull Request', + icon: 'git-compare', + href: this.createCommandLink('gitlens.home.openPullRequestComparison'), + }, + { + label: 'Open Pull Request Details', + icon: 'eye', + href: this.createCommandLink('gitlens.home.openPullRequestDetails'), + }, ]; } } diff --git a/src/webviews/apps/plus/home/components/branch-card.ts b/src/webviews/apps/plus/home/components/branch-card.ts index 28c2e68319eec..cfe7df9a7abae 100644 --- a/src/webviews/apps/plus/home/components/branch-card.ts +++ b/src/webviews/apps/plus/home/components/branch-card.ts @@ -16,22 +16,22 @@ import { createCommandLink } from '../../../../../system/commands'; import { fromNow } from '../../../../../system/date'; import { interpolate, pluralize } from '../../../../../system/string'; import type { BranchRef, GetOverviewBranch, OpenInGraphParams } from '../../../../home/protocol'; +import '../../../shared/components/actions/action-list'; +import type { ActionList } from '../../../shared/components/actions/action-list'; +import '../../../shared/components/avatar/avatar'; +import '../../../shared/components/avatar/avatar-list'; +import '../../../shared/components/branch-icon'; import { renderBranchName } from '../../../shared/components/branch-name'; import type { GlCard } from '../../../shared/components/card/card'; -import { GlElement, observe } from '../../../shared/components/element'; -import { srOnlyStyles } from '../../../shared/components/styles/lit/a11y.css'; -import { linkStyles } from '../../shared/components/vscode.css'; import '../../../shared/components/code-icon'; -import '../../../shared/components/avatar/avatar'; -import '../../../shared/components/avatar/avatar-list'; import '../../../shared/components/commit/commit-stats'; +import { GlElement, observe } from '../../../shared/components/element'; import '../../../shared/components/formatted-date'; import '../../../shared/components/pills/tracking'; import '../../../shared/components/rich/issue-icon'; import '../../../shared/components/rich/pr-icon'; -import '../../../shared/components/actions/action-item'; -import '../../../shared/components/actions/action-nav'; -import '../../../shared/components/branch-icon'; +import { srOnlyStyles } from '../../../shared/components/styles/lit/a11y.css'; +import { linkStyles } from '../../shared/components/vscode.css'; import './merge-target-status'; export const branchCardStyles = css` @@ -76,11 +76,6 @@ export const branchCardStyles = css` font-size: 0.9em; } - /* :empty selector doesn't work with lit */ - .branch-item__actions:not(:has(*)) { - display: none; - } - .branch-item__icon { color: var(--vscode-descriptionForeground); flex: none; @@ -412,6 +407,10 @@ export abstract class GlBranchCardBase extends GlElement { this.attachFocusListener(); } + static get OpenContextMenuEvent(): CustomEvent<{ items: (typeof ActionList.ItemProps)[]; branchRefs: BranchRef }> { + throw new Error('type field OpenContextMenuEvent cannot be used as a value'); + } + get branchRef(): BranchRef { return { repoPath: this.repo, @@ -607,20 +606,38 @@ export abstract class GlBranchCardBase extends GlElement { >`; } - protected abstract getBranchActions(): TemplateResult[]; + protected renderActions(actions: (typeof ActionList.ItemProps)[]) { + return html` { + const ev = new CustomEvent('open-actions-menu', { + detail: { items: e.detail.items, branchRefs: this.branchRef }, + }) satisfies typeof GlBranchCard.OpenContextMenuEvent; + this.dispatchEvent(ev); + }} + @close-actions-menu=${() => { + const ev = new CustomEvent('close-actions-menu'); + this.dispatchEvent(ev); + }} + .items=${actions} + >`; + } + + protected abstract getBranchActions(): (typeof ActionList.ItemProps)[]; protected renderBranchActions() { const actions = this.getBranchActions?.(); if (!actions?.length) return nothing; - return html`${actions}`; + return this.renderActions(actions); } - protected abstract getPrActions(): TemplateResult[]; + protected abstract getPrActions(): (typeof ActionList.ItemProps)[]; protected renderPrActions() { const actions = this.getPrActions?.(); if (!actions?.length) return nothing; - return html`${actions}`; + return this.renderActions(actions); } protected createCommandLink(command: Commands, args?: T | any) { @@ -848,65 +865,75 @@ export class GlBranchCard extends GlBranchCardBase { } protected getBranchActions() { - const actions = []; + const actions: (typeof ActionList.ItemProps)[] = []; if (this.branch.worktree) { - actions.push( - html``, - ); + actions.push({ + label: 'Open Worktree', + icon: 'browser', + href: this.createCommandLink('gitlens.home.openWorktree'), + modifiers: [ + { + key: 'alt', + label: 'Open Worktree in New Window', + href: this.createCommandLink('gitlens.home.openWorktreeInNewWindow'), + icon: 'empty-window', + }, + ], + }); } else { - actions.push( - html``, - ); + actions.push({ + label: 'Switch to Branch...', + icon: 'gl-switch', + href: this.createCommandLink('gitlens.home.switchToBranch'), + }); } // branch actions - actions.push( - html``, - ); - actions.push( - html``, - ); + actions.push({ + label: 'Fetch', + icon: 'repo-fetch', + href: this.createCommandLink('gitlens.home.fetch'), + modifiers: this.branch.upstream && [ + { + key: 'alt', + label: 'Pull', + icon: 'repo-pull', + href: this.createCommandLink('gitlens.home.pull'), + }, + ], + }); + actions.push({ + label: 'Open in Commit Graph', + icon: 'gl-graph', + href: createCommandLink('gitlens.home.openInGraph', { + ...this.branchRef, + type: 'branch', + } satisfies OpenInGraphParams), + }); return actions; } - protected getPrActions() { + protected getPrActions(): (typeof ActionList.ItemProps)[] { return [ - html``, - html``, - html``, + { + label: 'Open Pull Request Changes', + icon: 'request-changes', + href: this.createCommandLink('gitlens.home.openPullRequestChanges'), + }, + + { + label: 'Compare Pull Request', + icon: 'git-compare', + href: this.createCommandLink('gitlens.home.openPullRequestComparison'), + }, + + { + label: 'Open Pull Request Details', + icon: 'eye', + href: this.createCommandLink('gitlens.home.openPullRequestDetails'), + }, ]; } diff --git a/src/webviews/apps/plus/home/components/branch-section.ts b/src/webviews/apps/plus/home/components/branch-section.ts index 07b600e48964d..ebcfe6fd2ba3c 100644 --- a/src/webviews/apps/plus/home/components/branch-section.ts +++ b/src/webviews/apps/plus/home/components/branch-section.ts @@ -3,9 +3,22 @@ import { customElement, property, queryAll } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { when } from 'lit/directives/when.js'; import { debounce } from '../../../../../system/function'; -import type { GetOverviewBranch } from '../../../../home/protocol'; -import type { GlBranchCardBase } from './branch-card'; +import type { BranchRef, GetOverviewBranch } from '../../../../home/protocol'; +import '../../../shared/components/actions/action-item'; +import '../../../shared/components/actions/action-list'; +import type { ActionList } from '../../../shared/components/actions/action-list'; +import '../../../shared/components/actions/action-nav'; +import '../../../shared/components/avatar/avatar'; +import '../../../shared/components/avatar/avatar-list'; +import '../../../shared/components/card/card'; +import '../../../shared/components/code-icon'; +import '../../../shared/components/commit/commit-stats'; +import '../../../shared/components/formatted-date'; +import '../../../shared/components/pills/tracking'; import '../../../shared/components/progress'; +import '../../../shared/components/rich/issue-icon'; +import '../../../shared/components/rich/pr-icon'; +import type { GlBranchCard, GlBranchCardBase } from './branch-card'; @customElement('gl-section') export class GlSection extends LitElement { @@ -70,6 +83,10 @@ export class GlSection extends LitElement { @customElement('gl-branch-section') export class GlBranchSection extends LitElement { + static get OpenContextMenuEvent(): CustomEvent<{ items: (typeof ActionList.ItemProps)[]; branchRefs: BranchRef }> { + throw new Error('type field OpenContextMenuEvent cannot be used as a value'); + } + @property({ type: String }) label!: string; @property() repo!: string; @property({ type: Array }) branches!: GetOverviewBranch[]; @@ -124,7 +141,31 @@ export class GlBranchSection extends LitElement { () => this.branches.map( branch => - html``, + html` { + const evt = new CustomEvent('branch-context-opened', { + detail: { + branchRefs: e.detail.branchRefs, + items: e.detail.items, + }, + }) satisfies typeof GlBranchSection.OpenContextMenuEvent; + this.dispatchEvent(evt); + }} + @close-actions-menu=${(e: CustomEvent) => { + const evt = new CustomEvent<{ + branch: GetOverviewBranch; + }>('branch-context-closed', { + detail: { + branch: branch, + }, + }); + this.dispatchEvent(evt); + console.log('closeVContext', { e: e }, branch); + }} + .repo=${this.repo} + .branch=${branch} + >`, ), () => html`

No ${this.label} branches

`, )} diff --git a/src/webviews/apps/plus/home/components/overview.ts b/src/webviews/apps/plus/home/components/overview.ts index e43ddad0353df..d4f696ea059e6 100644 --- a/src/webviews/apps/plus/home/components/overview.ts +++ b/src/webviews/apps/plus/home/components/overview.ts @@ -9,6 +9,7 @@ import { stateContext } from '../../../home/context'; import { ipcContext } from '../../../shared/context'; import type { HostIpc } from '../../../shared/ipc'; import { linkStyles } from '../../shared/components/vscode.css'; +import type { GlBranchSection } from './branch-section'; import type { OverviewState } from './overviewState'; import { overviewStateContext } from './overviewState'; import '../../../shared/components/skeleton-loader'; @@ -91,6 +92,32 @@ export class GlOverview extends SignalWatcher(LitElement) { }); } + // TODO: can be moved to a separate function (maybe for home scope only) + private applyContext(context: object) { + const prevContext = JSON.parse(document.body.getAttribute('data-vscode-context') ?? '{}'); + document.body.setAttribute( + 'data-vscode-context', + JSON.stringify({ + ...prevContext, + ...context, + }), + ); + // clear context immediatelly after the contextmenu is opened to avoid randomly clicked contextmenu being filled + setTimeout(() => { + document.body.setAttribute('data-vscode-context', JSON.stringify(prevContext)); + }); + } + + private handleBranchContext(e: typeof GlBranchSection.OpenContextMenuEvent) { + let context = 'gitlens:home'; + e.detail.items.forEach(x => { + if (x.href) { + context += `+${x.href}`; + } + }); + this.applyContext({ webviewItem: context, ...e.detail.branchRefs, type: 'branch' }); + } + private renderComplete(overview: Overview, isFetching = false) { if (overview == null) return nothing; const { repository } = overview; @@ -100,6 +127,7 @@ export class GlOverview extends SignalWatcher(LitElement) { .isFetching=${isFetching} .repo=${repository.path} .branches=${repository.branches.recent} + @branch-context-opened=${this.handleBranchContext} > + + `; + } + override render() { - return html` - - - - - - `; + if (this.selected) { + return this.renderButtonContent(); + } + return html` ${this.renderButtonContent()} `; } override focus(options?: FocusOptions) { diff --git a/src/webviews/apps/shared/components/actions/action-list.ts b/src/webviews/apps/shared/components/actions/action-list.ts new file mode 100644 index 0000000000000..cb541f19b4748 --- /dev/null +++ b/src/webviews/apps/shared/components/actions/action-list.ts @@ -0,0 +1,158 @@ +import { html, LitElement } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { when } from 'lit/directives/when.js'; +import { isMac } from '@env/platform'; +import './action-item'; +import './action-nav'; + +interface ActionItemProps { + icon: string; + label: string; + href?: string; + modifiers?: { key: 'ctrl' | 'alt'; icon: string; label: string; href?: string }[]; +} + +@customElement('action-list') +export class ActionList extends LitElement { + static get ItemProps(): ActionItemProps { + throw new Error('type field ItemProps cannot be used as a value'); + } + static get OpenContextMenuEvent(): CustomEvent<{ items: ActionItemProps[] }> { + throw new Error('type field OpenContextMenuEvent cannot be used as a value'); + } + private _slotSubscriptionsDisposer?: () => void; + @property({ type: Array }) + private items: Array = []; + + @property({ type: Number }) + private limit: number = 3; + + @state() + private modifier: 'ctrl' | 'alt' | undefined; + + override connectedCallback(): void { + const handleKeydown = this.handleKeydown.bind(this); + const handleKeyup = this.handleKeyup.bind(this); + window.addEventListener('keydown', handleKeydown, false); + window.addEventListener('keyup', handleKeyup, false); + this._slotSubscriptionsDisposer = () => { + window.removeEventListener('keydown', handleKeydown, false); + window.removeEventListener('keyup', handleKeyup, false); + }; + super.connectedCallback(); + } + + override disconnectedCallback() { + this._slotSubscriptionsDisposer?.(); + super.disconnectedCallback(); + } + + /** is used to remove tooltip under the context menu */ + @state() + private open = false; + + private handleMoreActions(from: number, e: MouseEvent) { + if (e.button !== 0) { + return; + } + e.preventDefault(); + + this.open = true; + const event = new CustomEvent('open-actions-menu', { + detail: { + items: this.items + .slice(from) + .map((item): ActionItemProps[] => [item, ...(item.modifiers ?? [])]) + .flat(), + }, + }) satisfies typeof ActionList.OpenContextMenuEvent; + this.dispatchEvent(event); + + const contextMenuEvent = new PointerEvent('contextmenu', { + bubbles: true, + cancelable: true, + composed: true, + view: window, + button: 2, + buttons: 2, + clientX: this.getBoundingClientRect().right, + clientY: this.getBoundingClientRect().bottom, + }); + this.dispatchEvent(contextMenuEvent); + this.modifier = undefined; + + const handleClick = () => { + this.open = false; + window.removeEventListener('keyup', handleClick); + window.removeEventListener('mousedown', handleClick); + window.removeEventListener('mousemove', handleClick); + window.removeEventListener('blur', handleClick); + }; + setTimeout(() => { + window.addEventListener('keyup', handleClick); + window.addEventListener('mousedown', handleClick); + window.addEventListener('mousemove', handleClick); + window.addEventListener('blur', handleClick); + }); + } + + private renderMoreOptions(from: number) { + return html` + + + `; + } + + override render() { + const hasMore = this.items.length > this.limit; + const splitValue = hasMore ? this.limit - 1 : this.items.length; + return html` + + ${this.items.slice(0, splitValue).map(({ modifiers, ...originalProps }) => { + const { icon, label, href } = modifiers?.find(x => this.modifier === x.key) ?? originalProps; + return html` { + // finish event handling and clear modifier in next macrotask + setTimeout(() => { + this.modifier = undefined; + }); + }} + label=${label} + href=${ifDefined(href)} + >`; + })} + ${when(hasMore, this.renderMoreOptions.bind(this, splitValue))} + + `; + } + + // TODO it would be fine to think about hover-to-focus behavior for all places that use this component + private handleKeydown(e: KeyboardEvent) { + if (this.modifier) { + return; + } + if (e.key === 'Alt') { + this.modifier = 'alt'; + } else if ((isMac && e.key === 'Meta') || (!isMac && e.key === 'Control')) { + this.modifier = 'ctrl'; + } + } + + private handleKeyup(e: KeyboardEvent) { + if (!this.modifier) { + return; + } + if (e.key === 'Alt' || (isMac && e.key === 'Meta') || (!isMac && e.key === 'Control')) { + this.modifier = undefined; + } + } +} diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 1d39b9072dd4e..2fa746176ff95 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -253,17 +253,6 @@ export class HomeWebviewProvider implements WebviewProvider b.id === ref.branchId); + if (branch == null) return; + + void RepoActions.pull(repo!.repo, getReferenceFromBranch(branch)); + } + private findBranch(ref: BranchRef): GitBranch | undefined { const branches = this._repositoryBranches.get(ref.repoPath)?.branches; return branches?.find(b => b.id === ref.branchId);