Skip to content

Commit 8aff579

Browse files
committed
experiments on action-list and branches overview
1 parent 2a9e0b8 commit 8aff579

File tree

4 files changed

+271
-51
lines changed

4 files changed

+271
-51
lines changed

src/webviews/apps/home/home.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<body
2020
class="preload"
2121
data-placement="#{placement}"
22-
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
22+
data-vscode-context='{ "preventDefaultContextMenuItems": true, "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
2323
>
2424
<gl-home-app name="HomeView" placement="#{placement}" state="#{state}"></gl-home-app>
2525
</body>

src/webviews/apps/plus/home/components/branch-section.ts

Lines changed: 119 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { when } from 'lit/directives/when.js';
44
import type { Commands } from '../../../../../constants.commands';
55
import type { GitTrackingState } from '../../../../../git/models/branch';
66
import type { GetOverviewBranch, OpenInGraphParams } from '../../../../home/protocol';
7+
import type { ActionItemProps } from '../../../shared/components/actions/action-list';
78
import { srOnlyStyles } from '../../../shared/components/styles/lit/a11y.css';
89
import { linkStyles } from '../../shared/components/vscode.css';
910
import '../../../shared/components/code-icon';
@@ -15,6 +16,7 @@ import '../../../shared/components/commit/commit-stats';
1516
import '../../../shared/components/formatted-date';
1617
import '../../../shared/components/pills/tracking';
1718
import '../../../shared/components/rich/pr-icon';
19+
import '../../../shared/components/actions/action-list';
1820
import '../../../shared/components/actions/action-item';
1921
import '../../../shared/components/actions/action-nav';
2022

@@ -101,7 +103,35 @@ export class GlBranchSection extends LitElement {
101103
this.branches.length > 0,
102104
() =>
103105
this.branches.map(
104-
branch => html`<gl-branch-card .repo=${this.repo} .branch=${branch}></gl-branch-card>`,
106+
branch =>
107+
html`<gl-branch-card
108+
@open-actions-menu=${e => {
109+
const evt = new CustomEvent<{ branch: GetOverviewBranch }>(
110+
'branch-context-opened',
111+
{
112+
detail: {
113+
branch: branch,
114+
},
115+
},
116+
);
117+
this.dispatchEvent(evt);
118+
console.log('openVContext', { e: e }, branch);
119+
}}
120+
@close-actions-menu=${e => {
121+
const evt = new CustomEvent<{ branch: GetOverviewBranch }>(
122+
'branch-context-closed',
123+
{
124+
detail: {
125+
branch: branch,
126+
},
127+
},
128+
);
129+
this.dispatchEvent(evt);
130+
console.log('closeVContext', { e: e }, branch);
131+
}}
132+
.repo=${this.repo}
133+
.branch=${branch}
134+
></gl-branch-card>`,
105135
),
106136
() => html`<p>No ${this.label} branches</p>`,
107137
)}
@@ -345,72 +375,112 @@ export class GlBranchCard extends LitElement {
345375
}
346376

347377
private renderActions() {
348-
const actions = [];
378+
const actions: ActionItemProps[] = [];
349379
if (this.branch.pr) {
350380
actions.push(
351-
html`<action-item
352-
label="Open Pull Request Changes"
353-
icon="request-changes"
354-
href=${this.createCommandLink('gitlens.home.openPullRequestChanges')}
355-
></action-item>`,
356-
);
357-
actions.push(
358-
html`<action-item
359-
label="Open Pull Request on Remote"
360-
icon="globe"
361-
href=${this.createCommandLink('gitlens.home.openPullRequestOnRemote')}
362-
></action-item>`,
381+
{
382+
label: 'Open Pull Request Changes',
383+
icon: 'request-changes',
384+
href: this.createCommandLink('gitlens.home.openPullRequestChanges'),
385+
},
386+
{
387+
label: 'Open Pull Request on Remote',
388+
icon: 'globe',
389+
href: this.createCommandLink('gitlens.home.openPullRequestOnRemote'),
390+
},
363391
);
364392
} else if (this.branch.upstream?.missing === false) {
365-
actions.push(
366-
html`<action-item
367-
label="Create Pull Request..."
368-
icon="git-pull-request-create"
369-
href=${this.createCommandLink('gitlens.home.createPullRequest')}
370-
></action-item>`,
371-
);
393+
actions.push({
394+
label: 'Create Pull Request...',
395+
icon: 'git-pull-request-create',
396+
href: this.createCommandLink('gitlens.home.createPullRequest'),
397+
modifiers: [
398+
{
399+
key: 'alt',
400+
label: 'c',
401+
icon: 'globe',
402+
href: '',
403+
},
404+
{
405+
key: 'ctrl',
406+
label: 'c',
407+
icon: 'request-changes',
408+
href: '',
409+
},
410+
],
411+
});
372412
}
373413
if (this.branch.worktree) {
374-
actions.push(
375-
html`<action-item
376-
label="Open Worktree"
377-
icon="browser"
378-
href=${this.createCommandLink('gitlens.home.openWorktree')}
379-
></action-item>`,
380-
);
414+
actions.push({
415+
label: 'Open Worktree',
416+
icon: 'browser',
417+
href: this.createCommandLink('gitlens.home.openWorktree'),
418+
});
381419
} else {
382-
actions.push(
383-
html`<action-item
384-
label="Switch to Branch..."
385-
icon="gl-switch"
386-
href=${this.createCommandLink('gitlens.home.switchToBranch')}
387-
></action-item>`,
388-
);
420+
actions.push({
421+
label: 'Switch to Branch...',
422+
icon: 'gl-switch',
423+
href: this.createCommandLink('gitlens.home.switchToBranch'),
424+
});
389425
}
390426

391427
// branch actions
392428
actions.push(
393-
html`<action-item
394-
label="Fetch"
395-
icon="gl-repo-fetch"
396-
href=${this.createCommandLink('gitlens.home.fetch')}
397-
></action-item>`,
398-
);
399-
actions.push(
400-
html`<action-item
401-
label="Open in Commit Graph"
402-
icon="gl-graph"
403-
href=${createCommandLink('gitlens.home.openInGraph', {
429+
{
430+
label: 'Fetch',
431+
icon: 'gl-repo-fetch',
432+
href: this.createCommandLink('gitlens.home.fetch'),
433+
},
434+
{
435+
label: 'Open in Commit Graph',
436+
icon: 'gl-graph',
437+
href: createCommandLink('gitlens.home.openInGraph', {
404438
...this.branchRefs,
405439
type: 'branch',
406-
} satisfies OpenInGraphParams)}
407-
></action-item>`,
440+
} satisfies OpenInGraphParams),
441+
},
408442
);
409443

410444
if (!actions.length) {
411445
return nothing;
412446
}
413-
return html`<action-nav class="branch-item__actions">${actions}</action-nav>`;
447+
return html`<action-list
448+
limit=${2}
449+
class="branch-item__actions"
450+
@open-actions-menu=${(e: CustomEvent) => {
451+
e.preventDefault();
452+
453+
const ev = new CustomEvent('open-actions-menu');
454+
this.dispatchEvent(ev);
455+
if (ev.defaultPrevented) return;
456+
457+
const element = e.target as HTMLElement;
458+
const ev1 = new PointerEvent('contextmenu', {
459+
bubbles: true,
460+
cancelable: true,
461+
composed: true,
462+
view: window,
463+
button: 2,
464+
buttons: 2,
465+
clientX: element.getBoundingClientRect().right,
466+
clientY: element.getBoundingClientRect().bottom,
467+
});
468+
element.dispatchEvent(ev1);
469+
470+
const _handleClick = () => {
471+
const ev = new CustomEvent('close-actions-menu');
472+
this.dispatchEvent(ev);
473+
window.removeEventListener('keyup', handleClick);
474+
window.removeEventListener('mousedown', handleClick);
475+
window.removeEventListener('focus', handleClick);
476+
};
477+
const handleClick = _handleClick.bind(this);
478+
window.addEventListener('keyup', handleClick);
479+
window.addEventListener('mousedown', handleClick);
480+
window.addEventListener('focus', handleClick);
481+
}}
482+
.items=${actions}
483+
></action-list>`;
414484
}
415485

416486
private createCommandLink(command: string) {

src/webviews/apps/plus/home/components/overview.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { SignalWatcher } from '@lit-labs/signals';
33
import { css, html, LitElement, nothing } from 'lit';
44
import { customElement, state } from 'lit/decorators.js';
55
import { when } from 'lit/directives/when.js';
6-
import type { GetOverviewResponse, OverviewRecentThreshold, State } from '../../../../home/protocol';
6+
import type { GetOverviewBranch, GetOverviewResponse, OverviewRecentThreshold, State } from '../../../../home/protocol';
77
import { SetOverviewFilter } from '../../../../home/protocol';
88
import { stateContext } from '../../../home/context';
99
import { ipcContext } from '../../../shared/context';
@@ -91,6 +91,8 @@ export class GlOverview extends SignalWatcher(LitElement) {
9191
});
9292
}
9393

94+
private prevAttr = JSON.parse(document.body.getAttribute('data-vscode-context') ?? '{}');
95+
9496
private renderComplete(overview: Overview, isFetching = false) {
9597
if (overview == null) return nothing;
9698
const { repository } = overview;
@@ -100,6 +102,16 @@ export class GlOverview extends SignalWatcher(LitElement) {
100102
.isFetching=${isFetching}
101103
.repo=${repository.path}
102104
.branches=${repository.branches.recent}
105+
@branch-context-opened=${(e: CustomEvent<{ branch: GetOverviewBranch }>) => {
106+
this.prevAttr = JSON.parse(document.body.getAttribute('data-vscode-context') ?? '{}');
107+
document.body.setAttribute(
108+
'data-vscode-context',
109+
JSON.stringify({ ...this.prevAttr, webviewItem: 'gitlens:upstreamStatus' }),
110+
);
111+
}}
112+
@branch-context-closed=${() => {
113+
document.body.setAttribute('data-vscode-context', JSON.stringify(this.prevAttr));
114+
}}
103115
>
104116
<gl-branch-threshold-filter
105117
slot="heading-actions"
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// import { isMac } from '@env/platform';
2+
import { css, html, LitElement } from 'lit';
3+
import { customElement, property, queryAssignedElements, state } from 'lit/decorators.js';
4+
import { ifDefined } from 'lit/directives/if-defined.js';
5+
6+
const isMac = true;
7+
import './action-item';
8+
import './action-nav';
9+
import { when } from 'lit/directives/when.js';
10+
// import { isMac } from '@env/platform';
11+
12+
export interface ActionItemProps {
13+
icon: string;
14+
label: string;
15+
href?: string;
16+
modifiers?: { key: 'ctrl' | 'alt'; icon: string; label: string; href?: string }[];
17+
}
18+
19+
@customElement('action-list')
20+
export class ActionList extends LitElement {
21+
// static override styles = css``;
22+
23+
private _slotSubscriptionsDisposer?: () => void;
24+
25+
// override firstUpdated() {
26+
// this.role = 'navigation';
27+
// }
28+
29+
@property({ type: Array })
30+
private items: Array<ActionItemProps> = [];
31+
32+
@property({ type: Number })
33+
private limit: number = 3;
34+
35+
@state()
36+
private modifier: 'ctrl' | 'alt' | undefined;
37+
38+
override connectedCallback(): void {
39+
const handleKeydown = this.handleKeydown.bind(this);
40+
const handleKeyup = this.handleKeyup.bind(this);
41+
const handleOpenMore = this.handleOpenMore.bind(this);
42+
document.addEventListener('keydown', handleKeydown, false);
43+
document.addEventListener('keyup', handleKeyup, false);
44+
this.addEventListener('open-actions-menu', handleOpenMore);
45+
this._slotSubscriptionsDisposer = () => {
46+
document.removeEventListener('keydown', handleKeydown, false);
47+
document.removeEventListener('keyup', handleKeyup, false);
48+
this.removeEventListener('open-actions-menu', handleOpenMore);
49+
};
50+
super.connectedCallback();
51+
}
52+
53+
override disconnectedCallback() {
54+
this._slotSubscriptionsDisposer?.();
55+
super.disconnectedCallback();
56+
}
57+
58+
@state()
59+
private open = false;
60+
61+
private renderMoreOptions() {
62+
return html`
63+
<gl-popover ?open=${this.open} trigger="manual">
64+
<action-item
65+
slot="anchor"
66+
icon="more"
67+
label="more"
68+
@click=${(e: MouseEvent) => {
69+
if (e.button !== 0) {
70+
return;
71+
}
72+
const event = new CustomEvent('open-actions-menu', { cancelable: true });
73+
this.dispatchEvent(event);
74+
if (event.defaultPrevented) {
75+
return;
76+
}
77+
this.open = !this.open;
78+
// const element = e.target as HTMLElement;
79+
// e.preventDefault();
80+
// const ev1 = new PointerEvent('contextmenu', {
81+
// bubbles: true,
82+
// cancelable: true,
83+
// composed: true,
84+
// view: window,
85+
// button: 2,
86+
// buttons: 2,
87+
// clientX: element.getBoundingClientRect().right,
88+
// clientY: element.getBoundingClientRect().bottom,
89+
// });
90+
// element.dispatchEvent(ev1);
91+
}}
92+
>
93+
</action-item>
94+
<menu-list slot="content">
95+
${this.items
96+
.slice(this.limit)
97+
.map(action => html` <menu-item href=${ifDefined(action.href)}>${action.label}</menu-item> `)}
98+
</menu-list>
99+
</gl-popover>
100+
`;
101+
}
102+
103+
override render() {
104+
return html`
105+
<action-nav>
106+
${this.items.slice(0, this.limit).map(({ modifiers, ...originalProps }) => {
107+
const { icon, label, href } = modifiers?.find(x => this.modifier === x.key) ?? originalProps;
108+
return html`<action-item icon=${icon} label=${label} href=${ifDefined(href)}></action-item>`;
109+
})}
110+
${when(this.items.length >= this.limit, this.renderMoreOptions.bind(this))}
111+
</action-nav>
112+
`;
113+
}
114+
115+
private handleOpenMore() {
116+
// this.open = !this.open;
117+
}
118+
119+
private handleKeydown(e: KeyboardEvent) {
120+
if (this.modifier) {
121+
return;
122+
}
123+
if (e.key === 'Alt') {
124+
this.modifier = 'alt';
125+
} else if ((isMac && e.key === 'Meta') || (!isMac && e.key === 'Control')) {
126+
this.modifier = 'ctrl';
127+
}
128+
}
129+
130+
private handleKeyup(e: KeyboardEvent) {
131+
if (!this.modifier) {
132+
return;
133+
}
134+
if (e.key === 'Alt' || (isMac && e.key === 'Meta') || (!isMac && e.key === 'Control')) {
135+
this.modifier = undefined;
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)