Skip to content

Commit 8781f91

Browse files
nzaytsevd13
authored andcommitted
Adds threshold filter to the new home view
1 parent 2a09090 commit 8781f91

File tree

8 files changed

+195
-28
lines changed

8 files changed

+195
-28
lines changed

src/system/iterable.ts

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export function* chunk<T>(source: T[], size: number): Iterable<T[]> {
1616
}
1717
}
1818

19+
export const IterUtils = {
20+
notNull: function <T>(x: T | null | undefined): x is T {
21+
return Boolean(x);
22+
},
23+
};
24+
1925
export function* chunkByStringLength(source: string[], maxLength: number): Iterable<string[]> {
2026
let chunk: string[] = [];
2127

src/webviews/apps/home/home.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { html } from 'lit';
55
import { customElement, query } from 'lit/decorators.js';
66
import { when } from 'lit/directives/when.js';
77
import type { State } from '../../home/protocol';
8-
import { DidFocusAccount } from '../../home/protocol';
8+
import { DidChangeOverviewFilter, DidFocusAccount } from '../../home/protocol';
99
import { OverviewState, overviewStateContext } from '../plus/home/components/overviewState';
1010
import type { GLHomeAccountContent } from '../plus/shared/components/home-account-content';
1111
import { GlApp } from '../shared/app';
@@ -51,6 +51,11 @@ export class GlHomeApp extends GlApp<State> {
5151
case DidFocusAccount.is(msg):
5252
this.accountContentEl.show();
5353
break;
54+
case DidChangeOverviewFilter.is(msg):
55+
this._overviewState.filter.recent = msg.params.filter.recent;
56+
this._overviewState.filter.stale = msg.params.filter.stale;
57+
this._overviewState.run(true);
58+
break;
5459
}
5560
});
5661
}

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export const sectionHeadingStyles = css`
2121
margin-block: 0 0.8rem;
2222
text-transform: uppercase;
2323
}
24+
.section-heading.with-actions {
25+
display: flex;
26+
justify-content: space-between;
27+
gap: 8px;
28+
}
2429
`;
2530

2631
@customElement('gl-branch-section')
@@ -40,7 +45,9 @@ export class GlBranchSection extends LitElement {
4045
override render() {
4146
return html`
4247
<div class="section">
43-
<h3 class="section-heading">${this.label}</h3>
48+
<h3 class="section-heading with-actions">
49+
<span>${this.label}</span><slot name="heading-actions"></slot>
50+
</h3>
4451
<slot></slot>
4552
${this.branches.map(branch => html`<gl-branch-card .branch=${branch}></gl-branch-card>`)}
4653
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { css, html } from 'lit';
2+
import { customElement, property } from 'lit/decorators.js';
3+
import { repeat } from 'lit/directives/repeat.js';
4+
import type { OverviewRecentThreshold, OverviewStaleThreshold } from '../../../../home/protocol';
5+
import '../../../shared/components/checkbox/checkbox';
6+
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+
];
39+
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);
47+
}
48+
49+
override render() {
50+
if (!this.options) {
51+
return;
52+
}
53+
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+
)}
68+
</select>
69+
`;
70+
}
71+
}

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

+33-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import { consume } from '@lit/context';
22
import { SignalWatcher } from '@lit-labs/signals';
33
import { css, html, LitElement, nothing } from 'lit';
44
import { customElement } from 'lit/decorators.js';
5-
import type { GetOverviewResponse } from '../../../../home/protocol';
5+
import type { GetOverviewResponse, OverviewRecentThreshold } from '../../../../home/protocol';
6+
import { SetOverviewFilter } from '../../../../home/protocol';
7+
import { ipcContext } from '../../../shared/context';
8+
import type { HostIpc } from '../../../shared/ipc';
69
import { sectionHeadingStyles } from './branch-section';
710
import type { OverviewState } from './overviewState';
811
import { overviewStateContext } from './overviewState';
912
import '../../../shared/components/skeleton-loader';
13+
import './branch-threshold-filter';
1014

1115
type Overview = GetOverviewResponse;
1216

@@ -51,16 +55,42 @@ export class GlOverview extends SignalWatcher(LitElement) {
5155
`;
5256
}
5357

58+
@consume({ context: ipcContext })
59+
private readonly _ipc!: HostIpc;
60+
61+
private onChangeRecentThresholdFilter(e: CustomEvent<{ threshold: OverviewRecentThreshold }>) {
62+
if (!this._overviewState.filter.stale || !this._overviewState.filter.recent) {
63+
return;
64+
}
65+
this._ipc.sendCommand(SetOverviewFilter, {
66+
stale: this._overviewState.filter.stale,
67+
recent: { ...this._overviewState.filter.recent, threshold: e.detail.threshold },
68+
});
69+
}
70+
5471
private renderComplete(overview: Overview) {
5572
if (overview == null) return nothing;
56-
5773
const { repository } = overview;
5874
return html`
5975
<div class="repository">
6076
<gl-branch-section
6177
label="Recent (${repository.branches.recent.length})"
6278
.branches=${repository.branches.recent}
63-
></gl-branch-section>
79+
>
80+
<gl-branch-threshold-filter
81+
slot="heading-actions"
82+
@gl-change=${this.onChangeRecentThresholdFilter.bind(this)}
83+
.options=${[
84+
{ value: 'OneDay', label: '1 day' },
85+
{ value: 'OneWeek', label: '1 week' },
86+
{ value: 'OneMonth', label: '1 month' },
87+
] satisfies {
88+
value: OverviewRecentThreshold;
89+
label: string;
90+
}[]}
91+
.value=${this._overviewState.filter.recent?.threshold}
92+
></gl-branch-threshold-filter>
93+
</gl-branch-section>
6494
<gl-branch-section
6595
hidden
6696
label="Stale (${repository.branches.stale.length})"

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { createContext } from '@lit/context';
2-
import type { GetOverviewResponse } from '../../../../home/protocol';
3-
import { DidChangeRepositoryWip, GetOverview } from '../../../../home/protocol';
2+
import { signalObject } from 'signal-utils/object';
3+
import type { GetOverviewResponse, OverviewFilters } from '../../../../home/protocol';
4+
import { DidChangeRepositoryWip, GetOverview, GetOverviewFilterState } from '../../../../home/protocol';
45
import { AsyncComputedState } from '../../../shared/components/signal-utils';
56
import type { Disposable } from '../../../shared/events';
67
import type { HostIpc } from '../../../shared/ipc';
78

89
export type Overview = GetOverviewResponse;
910

1011
export class OverviewState extends AsyncComputedState<Overview> {
11-
private _disposable: Disposable | undefined;
12+
private readonly _disposable: Disposable | undefined;
1213

1314
constructor(
14-
private _ipc: HostIpc,
15+
private readonly _ipc: HostIpc,
1516
options?: {
1617
runImmediately?: boolean;
1718
initial?: Overview;
@@ -30,11 +31,17 @@ export class OverviewState extends AsyncComputedState<Overview> {
3031
break;
3132
}
3233
});
34+
void this._ipc.sendRequest(GetOverviewFilterState, undefined).then(rsp => {
35+
this.filter.recent = rsp.recent;
36+
this.filter.stale = rsp.stale;
37+
});
3338
}
3439

3540
dispose() {
3641
this._disposable?.dispose();
3742
}
43+
44+
filter = signalObject<Partial<OverviewFilters>>({});
3845
}
3946

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

src/webviews/home/homeWebview.ts

+37-19
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ import type {
3030
GetOverviewBranch,
3131
GetOverviewBranches,
3232
GetOverviewResponse,
33+
OverviewFilters,
34+
OverviewRecentThreshold,
35+
OverviewStaleThreshold,
3336
State,
3437
} from './protocol';
3538
import {
3639
CollapseSectionCommand,
3740
DidChangeIntegrationsConnections,
3841
DidChangeOrgSettings,
42+
DidChangeOverviewFilter,
3943
DidChangePreviewEnabled,
4044
DidChangeRepositories,
4145
DidChangeRepositoryWip,
@@ -45,6 +49,8 @@ import {
4549
DismissWalkthroughSection,
4650
GetLaunchpadSummary,
4751
GetOverview,
52+
GetOverviewFilterState,
53+
SetOverviewFilter,
4854
} from './protocol';
4955
import type { HomeWebviewShowingArgs } from './registration';
5056

@@ -87,6 +93,16 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
8793
};
8894
}
8995

96+
private _overviewBranchFilter: OverviewFilters = {
97+
recent: {
98+
threshold: 'OneWeek',
99+
},
100+
stale: {
101+
threshold: 'OneYear',
102+
show: false,
103+
},
104+
};
105+
90106
onShowing(
91107
loading: boolean,
92108
_options?: WebviewShowOptions,
@@ -133,6 +149,11 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
133149
];
134150
}
135151

152+
private setOverviewFilter(value: OverviewFilters) {
153+
this._overviewBranchFilter = value;
154+
void this.host.notify(DidChangeOverviewFilter, { filter: this._overviewBranchFilter });
155+
}
156+
136157
async onMessageReceived(e: IpcMessage) {
137158
switch (true) {
138159
case CollapseSectionCommand.is(e):
@@ -141,12 +162,18 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
141162
case DismissWalkthroughSection.is(e):
142163
this.dismissWalkthrough();
143164
break;
165+
case SetOverviewFilter.is(e):
166+
this.setOverviewFilter(e.params);
167+
break;
144168
case GetLaunchpadSummary.is(e):
145169
void this.host.respond(GetLaunchpadSummary, e, await getLaunchpadSummary(this.container));
146170
break;
147171
case GetOverview.is(e):
148172
void this.host.respond(GetOverview, e, await this.getBranchOverview());
149173
break;
174+
case GetOverviewFilterState.is(e):
175+
void this.host.respond(GetOverviewFilterState, e, this._overviewBranchFilter);
176+
break;
150177
}
151178
}
152179

@@ -202,7 +229,6 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
202229
}
203230

204231
private getWalkthroughDismissed() {
205-
console.log({ test: this.container.storage.get('home:walkthrough:dismissed') });
206232
return Boolean(this.container.storage.get('home:walkthrough:dismissed'));
207233
}
208234

@@ -276,7 +302,7 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
276302
branchesAndWorktrees?.branches,
277303
branchesAndWorktrees?.worktrees,
278304
this.container,
279-
// TODO: add filters
305+
this._overviewBranchFilter,
280306
);
281307
if (overviewBranches == null) return undefined;
282308

@@ -427,26 +453,18 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
427453
}
428454
}
429455

430-
const branchOverviewDefaults = Object.freeze({
431-
recent: { threshold: 1000 * 60 * 60 * 24 * 14 },
432-
stale: { threshold: 1000 * 60 * 60 * 24 * 365 },
433-
});
434-
435-
interface BranchOverviewOptions {
436-
recent?: {
437-
threshold: number;
438-
};
439-
stale?: {
440-
show?: boolean;
441-
threshold?: number;
442-
};
443-
}
456+
const thresholdValues: Record<OverviewStaleThreshold | OverviewRecentThreshold, number> = {
457+
OneDay: 1000 * 60 * 60 * 24 * 1,
458+
OneWeek: 1000 * 60 * 60 * 24 * 7,
459+
OneMonth: 1000 * 60 * 60 * 24 * 30,
460+
OneYear: 1000 * 60 * 60 * 24 * 365,
461+
};
444462

445463
async function getOverviewBranches(
446464
branches: GitBranch[],
447465
worktrees: GitWorktree[],
448466
container: Container,
449-
options?: BranchOverviewOptions,
467+
options: OverviewFilters,
450468
): Promise<GetOverviewBranches | undefined> {
451469
if (branches.length === 0) return undefined;
452470

@@ -469,7 +487,7 @@ async function getOverviewBranches(
469487
const contributorPromises = new Map<string, Promise<BranchContributorOverview | undefined>>();
470488

471489
const now = Date.now();
472-
const recentThreshold = now - (options?.recent?.threshold ?? branchOverviewDefaults.recent.threshold);
490+
const recentThreshold = now - thresholdValues[options.recent.threshold];
473491

474492
for (const branch of branches) {
475493
const wt = worktreesByBranch.get(branch.id);
@@ -520,7 +538,7 @@ async function getOverviewBranches(
520538
}
521539

522540
if (options?.stale?.show === true) {
523-
const staleThreshold = now - (options?.stale?.threshold ?? branchOverviewDefaults.stale.threshold);
541+
const staleThreshold = now - thresholdValues[options.stale.threshold];
524542
sortBranches(branches, {
525543
missingUpstream: true,
526544
orderBy: 'date:asc',

0 commit comments

Comments
 (0)