Skip to content

Commit 8a9a0c2

Browse files
committed
Adds repo switching to home
1 parent 0c52080 commit 8a9a0c2

File tree

7 files changed

+258
-63
lines changed

7 files changed

+258
-63
lines changed

src/webviews/apps/plus/home/components/active-work.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { consume } from '@lit/context';
22
import { SignalWatcher } from '@lit-labs/signals';
33
import { css, html, LitElement, nothing } from 'lit';
4-
import { customElement } from 'lit/decorators.js';
4+
import { customElement, property, state } from 'lit/decorators.js';
5+
import { repeat } from 'lit/directives/repeat.js';
56
import { when } from 'lit/directives/when.js';
67
import type { GitTrackingState } from '../../../../../git/models/branch';
7-
import type { GetOverviewBranch } from '../../../../home/protocol';
8+
import type { GetOverviewBranch, RepositoryChoice, State } from '../../../../home/protocol';
9+
import { stateContext } from '../../../home/context';
10+
import { GlElement } from '../../../shared/components/element';
811
import { branchCardStyles, sectionHeadingStyles } from './branch-section';
912
import type { Overview, OverviewState } from './overviewState';
1013
import { overviewStateContext } from './overviewState';
@@ -13,6 +16,8 @@ import '../../../shared/components/code-icon';
1316
import '../../../shared/components/skeleton-loader';
1417
import '../../../shared/components/card/card';
1518
import '../../../shared/components/commit/commit-stats';
19+
import '../../../shared/components/menu/menu-item';
20+
import '../../../shared/components/overlays/popover';
1621
import '../../../shared/components/pills/tracking';
1722
import '../../../shared/components/rich/pr-icon';
1823

@@ -31,16 +36,26 @@ export class GlActiveWork extends SignalWatcher(LitElement) {
3136
sectionHeadingStyles,
3237
];
3338

39+
@consume<State>({ context: stateContext, subscribe: true })
40+
@state()
41+
private _homeState!: State;
42+
3443
@consume({ context: overviewStateContext })
3544
private _overviewState!: OverviewState;
3645

3746
override connectedCallback() {
3847
super.connectedCallback();
3948

40-
this._overviewState.run();
49+
if (this._homeState.repositories.openCount > 0) {
50+
this._overviewState.run();
51+
}
4152
}
4253

4354
override render() {
55+
if (this._homeState.repositories.openCount === 0) {
56+
return nothing;
57+
}
58+
4459
return this._overviewState.render({
4560
pending: () => this.renderPending(),
4661
complete: overview => this.renderComplete(overview),
@@ -63,7 +78,10 @@ export class GlActiveWork extends SignalWatcher(LitElement) {
6378
if (activeBranches == null) return html`<span>None</span>`;
6479

6580
return html`
66-
<h3 class="section-heading">Active (${activeBranches.length})</h3>
81+
<h3 class="section-heading section-heading--actions">
82+
<span>Active (${activeBranches.length})</span>
83+
${when(overview!.choices, () => html`<gl-change-repo .options=${overview!.choices}></gl-change-repo>`)}
84+
</h3>
6785
${activeBranches.map(branch => this.renderRepoBranchCard(overview!.repository.name, branch))}
6886
`;
6987
}
@@ -201,8 +219,75 @@ export class GlActiveWork extends SignalWatcher(LitElement) {
201219
}
202220
}
203221

222+
@customElement('gl-change-repo')
223+
export class GlChangeRepo extends GlElement {
224+
static override styles = [
225+
css`
226+
.popover::part(body) {
227+
padding: 0;
228+
font-size: var(--vscode-font-size);
229+
background-color: var(--vscode-menu-background);
230+
}
231+
.trigger {
232+
--button-padding: 0.1rem 0.2rem 0;
233+
margin-block: -1rem;
234+
}
235+
236+
.option {
237+
max-width: 20rem;
238+
white-space: nowrap;
239+
overflow: hidden;
240+
text-overflow: ellipsis;
241+
}
242+
243+
.option code-icon {
244+
color: var(--color-foreground--50);
245+
}
246+
`,
247+
];
248+
249+
@consume({ context: overviewStateContext })
250+
private _overviewState!: OverviewState;
251+
252+
@property({ type: Array }) options?: RepositoryChoice[];
253+
254+
protected getLabel(option: RepositoryChoice) {
255+
return option.name;
256+
}
257+
258+
protected onChange(option: RepositoryChoice) {
259+
void this.querySelector('gl-popover')?.hide();
260+
void this._overviewState.changeRepository(option.path);
261+
}
262+
263+
override render() {
264+
if (!this.options) {
265+
return;
266+
}
267+
return html`
268+
<gl-popover class="popover" placement="bottom-end" trigger="focus" .distance=${4} .arrow=${false}>
269+
<gl-button class="trigger" appearance="toolbar" slot="anchor"
270+
><code-icon icon="chevron-down"></code-icon
271+
></gl-button>
272+
<div slot="content">
273+
${repeat(this.options, item => {
274+
if (item.selected) {
275+
return nothing;
276+
}
277+
const label = this.getLabel(item);
278+
return html`<menu-item class="option" @click=${() => this.onChange(item)}
279+
><code-icon icon="repo"></code-icon> ${label}</menu-item
280+
>`;
281+
})}
282+
</div>
283+
</gl-popover>
284+
`;
285+
}
286+
}
287+
204288
declare global {
205289
interface HTMLElementTagNameMap {
206290
[activeWorkTagName]: GlActiveWork;
291+
'gl-change-repo': GlChangeRepo;
207292
}
208293
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const sectionHeadingStyles = css`
2121
margin-block: 0 0.8rem;
2222
text-transform: uppercase;
2323
}
24-
.section-heading.with-actions {
24+
.section-heading--actions {
2525
display: flex;
2626
justify-content: space-between;
2727
gap: 8px;
@@ -45,7 +45,7 @@ export class GlBranchSection extends LitElement {
4545
override render() {
4646
return html`
4747
<div class="section">
48-
<h3 class="section-heading with-actions">
48+
<h3 class="section-heading section-heading--actions">
4949
<span>${this.label}</span><slot name="heading-actions"></slot>
5050
</h3>
5151
<slot></slot>

src/webviews/apps/plus/home/components/branch-threshold-filter.ts

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,76 @@ import { css, html } from 'lit';
22
import { customElement, property } from 'lit/decorators.js';
33
import { repeat } from 'lit/directives/repeat.js';
44
import type { OverviewRecentThreshold, OverviewStaleThreshold } from '../../../../home/protocol';
5+
import { GlElement } from '../../../shared/components/element';
56
import '../../../shared/components/checkbox/checkbox';
67
import '../../../shared/components/code-icon';
7-
import { GlElement } from '../../../shared/components/element';
8-
import '../../../shared/components/menu/index';
9-
import '../../../shared/components/menu/menu-item';
10-
import '../../../shared/components/menu/menu-list';
11-
import '../../../shared/components/overlays/popover';
12-
13-
@customElement('gl-branch-threshold-filter')
14-
export class GlBranchThresholdFilter extends GlElement {
15-
static override readonly styles = [
16-
css`
17-
.date-select {
18-
background: none;
19-
outline: none;
20-
border: none;
21-
cursor: pointer;
22-
color: var(--vscode-disabledForeground);
23-
text-decoration: none !important;
24-
font-weight: 500;
25-
}
26-
.date-select option {
27-
color: var(--vscode-foreground);
28-
background-color: var(--vscode-dropdown-background);
29-
}
30-
.date-select:focus {
31-
outline: 1px solid var(--vscode-disabledForeground);
32-
}
33-
.date-select:hover {
34-
color: var(--vscode-foreground);
35-
text-decoration: underline !important;
36-
}
37-
`,
38-
];
398

40-
@property({ type: String }) value: OverviewRecentThreshold | OverviewStaleThreshold | undefined;
41-
@property({ type: Array }) options!: { value: OverviewRecentThreshold | OverviewStaleThreshold; label: string }[];
42-
private selectDateFilter(threshold: OverviewRecentThreshold | OverviewStaleThreshold) {
43-
const event = new CustomEvent('gl-change', {
44-
detail: { threshold: threshold },
45-
});
46-
this.dispatchEvent(event);
9+
export const selectStyles = css`
10+
.select {
11+
background: none;
12+
outline: none;
13+
border: none;
14+
cursor: pointer;
15+
color: var(--color-foreground--50);
16+
text-decoration: none !important;
17+
font-weight: 500;
18+
}
19+
.select option {
20+
color: var(--vscode-foreground);
21+
background-color: var(--vscode-dropdown-background);
22+
}
23+
.select:focus {
24+
outline: 1px solid var(--color-focus-border);
4725
}
26+
.select:hover {
27+
color: var(--vscode-foreground);
28+
text-decoration: underline !important;
29+
}
30+
`;
31+
32+
export abstract class GlObjectSelect<T, L = T[keyof T], V = T[keyof T]> extends GlElement {
33+
static override readonly styles = [selectStyles];
34+
35+
@property({ type: String }) value?: V;
36+
@property({ type: Array }) options?: T[];
37+
38+
protected abstract getValue(option: T): V;
39+
protected abstract getLabel(option: T): L;
40+
protected abstract onChange?(e: InputEvent): unknown;
4841

4942
override render() {
5043
if (!this.options) {
5144
return;
5245
}
5346
return html`
54-
<select
55-
class="date-select"
56-
@change=${(e: Event) =>
57-
this.selectDateFilter(
58-
(e.target as HTMLSelectElement).value as OverviewRecentThreshold | OverviewStaleThreshold,
59-
)}
60-
>
61-
${repeat(
62-
this.options,
63-
item =>
64-
html`<option value="${item.value}" ?selected=${this.value === item.value}>
65-
${item.label}
66-
</option>`,
67-
)}
47+
<select class="select" @change=${(e: InputEvent) => this.onChange?.(e)}>
48+
${repeat(this.options, item => {
49+
const value = this.getValue(item);
50+
const label = this.getLabel(item);
51+
return html`<option .value="${value}" ?selected=${this.value === value}>${label}</option>`;
52+
})}
6853
</select>
6954
`;
7055
}
7156
}
57+
58+
@customElement('gl-branch-threshold-filter')
59+
export class GlBranchThresholdFilter extends GlObjectSelect<{
60+
value: OverviewRecentThreshold | OverviewStaleThreshold;
61+
label: string;
62+
}> {
63+
protected getValue(option: { value: OverviewRecentThreshold | OverviewStaleThreshold }) {
64+
return option.value;
65+
}
66+
protected getLabel(option: { label: string }) {
67+
return option.label;
68+
}
69+
protected onChange(e: InputEvent) {
70+
const event = new CustomEvent('gl-change', {
71+
detail: {
72+
threshold: (e.target as HTMLSelectElement).value as OverviewRecentThreshold | OverviewStaleThreshold,
73+
},
74+
});
75+
this.dispatchEvent(event);
76+
}
77+
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { consume } from '@lit/context';
22
import { SignalWatcher } from '@lit-labs/signals';
33
import { css, html, LitElement, nothing } from 'lit';
4-
import { customElement } from 'lit/decorators.js';
5-
import type { GetOverviewResponse, OverviewRecentThreshold } from '../../../../home/protocol';
4+
import { customElement, state } from 'lit/decorators.js';
5+
import type { GetOverviewResponse, OverviewRecentThreshold, State } from '../../../../home/protocol';
66
import { SetOverviewFilter } from '../../../../home/protocol';
7+
import { stateContext } from '../../../home/context';
78
import { ipcContext } from '../../../shared/context';
89
import type { HostIpc } from '../../../shared/ipc';
910
import { sectionHeadingStyles } from './branch-section';
@@ -31,16 +32,26 @@ export class GlOverview extends SignalWatcher(LitElement) {
3132
`,
3233
];
3334

35+
@consume<State>({ context: stateContext, subscribe: true })
36+
@state()
37+
private _homeState!: State;
38+
3439
@consume({ context: overviewStateContext })
3540
private _overviewState!: OverviewState;
3641

3742
override connectedCallback() {
3843
super.connectedCallback();
3944

40-
this._overviewState.run();
45+
if (this._homeState.repositories.openCount > 0) {
46+
this._overviewState.run();
47+
}
4148
}
4249

4350
override render() {
51+
if (this._homeState.repositories.openCount === 0) {
52+
return nothing;
53+
}
54+
4455
return this._overviewState.render({
4556
pending: () => this.renderPending(),
4657
complete: summary => this.renderComplete(summary),

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { createContext } from '@lit/context';
22
import { signalObject } from 'signal-utils/object';
33
import type { GetOverviewResponse, OverviewFilters } from '../../../../home/protocol';
44
import {
5+
ChangeOverviewRepository,
56
DidChangeOverviewFilter,
67
DidChangeRepositoryWip,
8+
DidCompleteDiscoveringRepositories,
79
GetOverview,
810
GetOverviewFilterState,
911
} from '../../../../home/protocol';
@@ -31,6 +33,7 @@ export class OverviewState extends AsyncComputedState<Overview> {
3133

3234
this._disposable = this._ipc.onReceiveMessage(msg => {
3335
switch (true) {
36+
case DidCompleteDiscoveringRepositories.is(msg):
3437
case DidChangeRepositoryWip.is(msg):
3538
this.run(true);
3639
break;
@@ -52,6 +55,11 @@ export class OverviewState extends AsyncComputedState<Overview> {
5255
}
5356

5457
filter = signalObject<Partial<OverviewFilters>>({});
58+
59+
async changeRepository(repo: string) {
60+
await this._ipc.sendRequest(ChangeOverviewRepository, repo);
61+
this.run(true);
62+
}
5563
}
5664

5765
export const overviewStateContext = createContext<Overview>('overviewState');

0 commit comments

Comments
 (0)