Skip to content

Commit 46bef5e

Browse files
committed
Adds Create/Start menus and Launchpad indicator to Graph header
Elevates the "start new" actions in the Commit Graph header so they are always available regardless of selection or working-tree state (#5390): - Replaces the "Create Branch" button with a Create menu (Create Worktree, Create Branch, Apply / Pop Stash) - Adds a Start menu (Start Work on an Issue, Start Review on a PR) - Replaces the Launchpad and Home buttons with a Launchpad indicator that shows per-group counts and opens a summary popover Extracts the WIP details Launchpad summary into a shared gl-launchpad-summary component, and hoists its fetch into a shared, gl-graph-app-owned store that reuses the LaunchpadService RPC with a single onLaunchpadChanged subscription, so the header indicator and the WIP empty pane share one source of truth.
1 parent 1ca090d commit 46bef5e

10 files changed

Lines changed: 756 additions & 342 deletions

src/webviews/apps/plus/graph/components/detailsState.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';
2727
import type { GitCommitSearchContext } from '@gitlens/git/models/search.js';
2828
import type { GitCommitReachability } from '@gitlens/git/providers/commits.js';
2929
import type { Autolink } from '../../../../../autolinks/models/autolinks.js';
30-
import type { LaunchpadSummaryResult } from '../../../../../plus/launchpad/launchpadIndicator.js';
3130
import type { CommitDetails, CommitSignatureShape, Preferences, Wip } from '../../../../plus/graph/detailsProtocol.js';
3231
import type {
3332
BranchCommitEntry,
@@ -232,9 +231,6 @@ function createDurableState() {
232231
const hasRemotes = signal(false);
233232
const aiModel = signal<AiModelInfo | undefined>(undefined);
234233

235-
const launchpadSummary = signal<LaunchpadSummaryResult | { error: Error } | undefined>(undefined);
236-
const launchpadSummaryLoading = signal(false);
237-
238234
return {
239235
commit: commit,
240236
wip: wip,
@@ -310,9 +306,6 @@ function createDurableState() {
310306
hasRemotes: hasRemotes,
311307
aiModel: aiModel,
312308

313-
launchpadSummary: launchpadSummary,
314-
launchpadSummaryLoading: launchpadSummaryLoading,
315-
316309
resetAll: resetAll,
317310
};
318311
}

src/webviews/apps/plus/graph/components/gl-details-wip-empty-pane.css.ts

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -104,79 +104,4 @@ export const detailsWipEmptyPaneStyles = css`
104104
.start-new gl-button code-icon {
105105
margin-right: var(--gl-space-4);
106106
}
107-
108-
.launchpad-items {
109-
display: flex;
110-
flex-direction: column;
111-
gap: var(--gl-space-4);
112-
113-
/* Match the left inset of Next-step rows so the launchpad items line up with the
114-
Next-steps content column rather than sitting flush with the section heading. */
115-
padding-inline-start: var(--gl-space-6);
116-
117-
/* Matches the start-new top padding so the Launchpad heading-to-content gap reads the
118-
same as the other sections — first launchpad row sits flush with where the first row
119-
of Next-steps and the first button of Start-new sit. */
120-
margin-block: var(--gl-space-8) var(--gl-space-6);
121-
list-style: none;
122-
}
123-
124-
.launchpad-items--loading {
125-
gap: var(--gl-space-4);
126-
}
127-
128-
.launchpad-item {
129-
display: flex;
130-
gap: var(--gl-space-6);
131-
align-items: center;
132-
font-size: var(--gl-font-md);
133-
color: inherit;
134-
text-decoration: none;
135-
}
136-
137-
.launchpad-item__icon {
138-
color: var(--gl-launchpad-item-color, inherit);
139-
}
140-
141-
.launchpad-item--link {
142-
cursor: pointer;
143-
}
144-
145-
.launchpad-item--link:hover {
146-
text-decoration: none;
147-
}
148-
149-
.launchpad-item--link:hover span {
150-
text-decoration: underline;
151-
}
152-
153-
.launchpad-item--link:hover .launchpad-item__icon {
154-
color: var(--gl-launchpad-item-hover-color, var(--gl-launchpad-item-color, inherit));
155-
}
156-
157-
.launchpad-item--link:focus-visible {
158-
outline: var(--gl-border-width) solid var(--vscode-focusBorder);
159-
outline-offset: 2px;
160-
border-radius: var(--gl-radius-xs);
161-
}
162-
163-
.launchpad-item--muted {
164-
font-style: italic;
165-
color: var(--color-foreground--65);
166-
}
167-
168-
.launchpad-item--mergeable {
169-
--gl-launchpad-item-color: var(--vscode-gitlens-launchpadIndicatorMergeableColor);
170-
--gl-launchpad-item-hover-color: var(--vscode-gitlens-launchpadIndicatorMergeableHoverColor);
171-
}
172-
173-
.launchpad-item--blocked {
174-
--gl-launchpad-item-color: var(--vscode-gitlens-launchpadIndicatorBlockedColor);
175-
--gl-launchpad-item-hover-color: var(--vscode-gitlens-launchpadIndicatorBlockedHoverColor);
176-
}
177-
178-
.launchpad-item--attention {
179-
--gl-launchpad-item-color: var(--vscode-gitlens-launchpadIndicatorAttentionColor);
180-
--gl-launchpad-item-hover-color: var(--vscode-gitlens-launchpadIndicatorAttentionHoverColor);
181-
}
182107
`;

src/webviews/apps/plus/graph/components/gl-details-wip-empty-pane.ts

Lines changed: 7 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { consume } from '@lit/context';
2-
import type { TemplateResult } from 'lit';
32
import { html, LitElement, nothing } from 'lit';
43
import { customElement, property } from 'lit/decorators.js';
54
import { ifDefined } from 'lit/directives/if-defined.js';
65
import { pluralize } from '@gitlens/utils/string.js';
7-
import type { ConnectCloudIntegrationsCommandArgs } from '../../../../../commands/cloudIntegrations.js';
8-
import type { LaunchpadCommandArgs } from '../../../../../plus/launchpad/launchpad.js';
96
import type { LaunchpadSummaryResult } from '../../../../../plus/launchpad/launchpadIndicator.js';
10-
import { createCommandLink } from '../../../../../system/commands.js';
117
import type { GitBranchShape, Wip } from '../../../../plus/graph/detailsProtocol.js';
128
import type { BranchMergeTargetStatus } from '../../../../rpc/services/branches.js';
139
import type { BranchRef } from '../../../../shared/branchRefs.js';
@@ -18,7 +14,7 @@ import { detailsWipEmptyPaneStyles } from './gl-details-wip-empty-pane.css.js';
1814
import '../../../shared/components/button.js';
1915
import '../../../shared/components/button-container.js';
2016
import '../../../shared/components/code-icon.js';
21-
import '../../../shared/components/skeleton-loader.js';
17+
import './gl-launchpad-summary.js';
2218

2319
type NextStepAction = {
2420
actionLabel: string;
@@ -100,7 +96,7 @@ export class GlDetailsWipEmptyPane extends LitElement {
10096

10197
// Launchpad renders from initial mount (when `showLaunchpad`) — the summary content is
10298
// branch-agnostic (PRs across the user's connected integrations) and the inner
103-
// `renderLaunchpadSummary` handles its own loading/empty/unconnected states with a
99+
// `gl-launchpad-summary` handles its own loading/empty/unconnected states with a
104100
// stable footprint. Gating on `branch != null` here would cause the section to pop into
105101
// existence the moment WIP arrived, shifting `Start New` down — the very layout flip
106102
// this scaffold was reshaped to avoid.
@@ -131,7 +127,11 @@ export class GlDetailsWipEmptyPane extends LitElement {
131127
<code-icon icon="refresh"></code-icon>
132128
</gl-button>
133129
</header>
134-
${this.renderLaunchpadSummary()}
130+
<gl-launchpad-summary
131+
.summary=${this.launchpadSummary}
132+
?has-integrations-connected=${this.hasIntegrationsConnected}
133+
source="graph-details"
134+
></gl-launchpad-summary>
135135
</section>`;
136136
}
137137

@@ -267,195 +267,6 @@ export class GlDetailsWipEmptyPane extends LitElement {
267267
</section>`;
268268
}
269269

270-
private renderLaunchpadSummary(): TemplateResult {
271-
if (!this.hasIntegrationsConnected) {
272-
return html`<ul class="launchpad-items">
273-
<li>
274-
<a
275-
class="launchpad-item launchpad-item--link"
276-
href=${createCommandLink<ConnectCloudIntegrationsCommandArgs>(
277-
'gitlens.plus.cloudIntegrations.connect',
278-
{ source: { source: 'graph' } },
279-
)}
280-
>
281-
<code-icon class="launchpad-item__icon" icon="plug"></code-icon>
282-
<span>Connect to see PRs here</span>
283-
</a>
284-
</li>
285-
</ul>`;
286-
}
287-
288-
const summary = this.launchpadSummary;
289-
if (summary == null) {
290-
// Single skeleton line matches the most common landed content — "You are all caught
291-
// up!" or a single group summary. Two lines was nearly always over-tall, causing a
292-
// downward shift when content landed.
293-
return html`<div class="launchpad-items launchpad-items--loading">
294-
<skeleton-loader lines="1"></skeleton-loader>
295-
</div>`;
296-
}
297-
298-
if (!('total' in summary)) {
299-
return html`<ul class="launchpad-items">
300-
<li class="launchpad-item launchpad-item--muted">Unable to load items</li>
301-
</ul>`;
302-
}
303-
304-
const items: TemplateResult[] = [];
305-
306-
if (summary.error != null) {
307-
items.push(
308-
html`<li>
309-
<span class="launchpad-item launchpad-item--muted">
310-
<code-icon class="launchpad-item__icon" icon="warning"></code-icon>
311-
<span>Some integrations failed to load</span>
312-
</span>
313-
</li>`,
314-
);
315-
}
316-
317-
if (summary.total === 0) {
318-
items.push(html`<li class="launchpad-item launchpad-item--muted">You are all caught up!</li>`);
319-
return html`<ul class="launchpad-items">
320-
${items}
321-
</ul>`;
322-
}
323-
324-
if (!summary.hasGroupedItems) {
325-
items.push(
326-
html`<li class="launchpad-item launchpad-item--muted">No pull requests need your attention</li>
327-
<li class="launchpad-item launchpad-item--muted">(${summary.total} other pull requests)</li>`,
328-
);
329-
return html`<ul class="launchpad-items">
330-
${items}
331-
</ul>`;
332-
}
333-
334-
for (const group of summary.groups) {
335-
switch (group) {
336-
case 'mergeable': {
337-
const total = summary.mergeable?.total ?? 0;
338-
if (total === 0) continue;
339-
340-
items.push(
341-
html`<li>
342-
<a
343-
class="launchpad-item launchpad-item--link launchpad-item--mergeable"
344-
href=${this.createShowLaunchpadLink('mergeable')}
345-
>
346-
<code-icon class="launchpad-item__icon" icon="rocket"></code-icon>
347-
<span>${pluralize('pull request', total)} can be merged</span>
348-
</a>
349-
</li>`,
350-
);
351-
break;
352-
}
353-
case 'blocked': {
354-
const total = summary.blocked?.total ?? 0;
355-
if (total === 0) continue;
356-
357-
const messages: { count: number; message: string }[] = [];
358-
if (summary.blocked!.unassignedReviewers) {
359-
messages.push({
360-
count: summary.blocked!.unassignedReviewers,
361-
message: `${summary.blocked!.unassignedReviewers > 1 ? 'need' : 'needs'} reviewers`,
362-
});
363-
}
364-
if (summary.blocked!.failedChecks) {
365-
messages.push({
366-
count: summary.blocked!.failedChecks,
367-
message: `${summary.blocked!.failedChecks > 1 ? 'have' : 'has'} failed CI checks`,
368-
});
369-
}
370-
if (summary.blocked!.conflicts) {
371-
messages.push({
372-
count: summary.blocked!.conflicts,
373-
message: `${summary.blocked!.conflicts > 1 ? 'have' : 'has'} conflicts`,
374-
});
375-
}
376-
377-
const href = this.createShowLaunchpadLink('blocked');
378-
if (messages.length === 1) {
379-
items.push(
380-
html`<li>
381-
<a class="launchpad-item launchpad-item--link launchpad-item--blocked" href=${href}>
382-
<code-icon class="launchpad-item__icon" icon="error"></code-icon>
383-
<span>${pluralize('pull request', total)} ${messages[0].message}</span>
384-
</a>
385-
</li>`,
386-
);
387-
} else {
388-
items.push(
389-
html`<li>
390-
<a class="launchpad-item launchpad-item--link launchpad-item--blocked" href=${href}>
391-
<code-icon class="launchpad-item__icon" icon="error"></code-icon>
392-
<span
393-
>${pluralize('pull request', total)} ${total > 1 ? 'are' : 'is'} blocked
394-
(${messages.map(m => `${m.count} ${m.message}`).join(', ')})</span
395-
>
396-
</a>
397-
</li>`,
398-
);
399-
}
400-
break;
401-
}
402-
case 'follow-up': {
403-
const total = summary.followUp?.total ?? 0;
404-
if (total === 0) continue;
405-
406-
items.push(
407-
html`<li>
408-
<a
409-
class="launchpad-item launchpad-item--link launchpad-item--attention"
410-
href=${this.createShowLaunchpadLink('follow-up')}
411-
>
412-
<code-icon class="launchpad-item__icon" icon="report"></code-icon>
413-
<span
414-
>${pluralize('pull request', total)} ${total > 1 ? 'require' : 'requires'}
415-
follow-up</span
416-
>
417-
</a>
418-
</li>`,
419-
);
420-
break;
421-
}
422-
case 'needs-review': {
423-
const total = summary.needsReview?.total ?? 0;
424-
if (total === 0) continue;
425-
426-
items.push(
427-
html`<li>
428-
<a
429-
class="launchpad-item launchpad-item--link launchpad-item--attention"
430-
href=${this.createShowLaunchpadLink('needs-review')}
431-
>
432-
<code-icon class="launchpad-item__icon" icon="comment-unresolved"></code-icon>
433-
<span
434-
>${pluralize('pull request', total)} ${total > 1 ? 'need' : 'needs'} your
435-
review</span
436-
>
437-
</a>
438-
</li>`,
439-
);
440-
break;
441-
}
442-
}
443-
}
444-
445-
return html`<ul class="launchpad-items">
446-
${items}
447-
</ul>`;
448-
}
449-
450-
private createShowLaunchpadLink(group: NonNullable<LaunchpadCommandArgs['state']>['initialGroup']): string {
451-
return `command:gitlens.showLaunchpad?${encodeURIComponent(
452-
JSON.stringify({
453-
source: 'graph-details',
454-
state: { initialGroup: group },
455-
} satisfies Omit<LaunchpadCommandArgs, 'command'>),
456-
)}`;
457-
}
458-
459270
private computeNextSteps(branch: GitBranchShape): NextStep[] {
460271
const ahead = branch.tracking?.ahead ?? 0;
461272
const behind = branch.tracking?.behind ?? 0;

0 commit comments

Comments
 (0)