Skip to content

Commit ca08d96

Browse files
committed
Adds threshold filter to the new home view
1 parent ccf72bb commit ca08d96

File tree

10 files changed

+410
-29
lines changed

10 files changed

+410
-29
lines changed

src/system/iterable.ts

Lines changed: 6 additions & 0 deletions
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

Lines changed: 6 additions & 1 deletion
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
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// unused now
2+
import { consume } from '@lit/context';
3+
import { Task } from '@lit/task';
4+
import { css, html } from 'lit';
5+
import { customElement, property, state } from 'lit/decorators.js';
6+
import { repeat } from 'lit/directives/repeat.js';
7+
import { when } from 'lit/directives/when.js';
8+
import { IterUtils } from '../../../../../system/iterable';
9+
import { pluralize } from '../../../../../system/string';
10+
import type { RepoOwner } from '../../../../home/protocol';
11+
import { GetRepoOwners } from '../../../../home/protocol';
12+
import '../../../shared/components/checkbox/checkbox';
13+
import '../../../shared/components/code-icon';
14+
import { GlElement } from '../../../shared/components/element';
15+
import '../../../shared/components/menu/index';
16+
import '../../../shared/components/menu/menu-item';
17+
import '../../../shared/components/menu/menu-list';
18+
import '../../../shared/components/overlays/popover';
19+
import { ipcContext } from '../../../shared/context';
20+
import type { HostIpc } from '../../../shared/ipc';
21+
22+
@customElement('gl-branch-owner-filter')
23+
export class GlBranchOwnerFilter extends GlElement {
24+
static override readonly styles = [
25+
// should be shared with other filters
26+
css`
27+
.owner-filter:focus {
28+
outline: 1px solid;
29+
}
30+
.owner-filter {
31+
background: none;
32+
outline: none;
33+
border: none;
34+
cursor: pointer;
35+
color: var(--vscode-disabledForeground);
36+
text-decoration: none !important;
37+
font-weight: 500;
38+
}
39+
.owner-filter:hover {
40+
color: var(--vscode-foreground);
41+
text-decoration: underline !important;
42+
}
43+
.owner-item {
44+
display: flex;
45+
flex-direction: column;
46+
gap: 2px;
47+
}
48+
.owner-label {
49+
display: flex;
50+
gap: 4px;
51+
align-items: center;
52+
}
53+
.owner-email {
54+
color: var(--vscode-disabledForeground);
55+
}
56+
.current {
57+
display: inline-block;
58+
padding: 0px 2px;
59+
background: var(--vscode-disabledForeground);
60+
border-radius: 2px;
61+
}
62+
`,
63+
];
64+
65+
@property({ type: Array }) filter: RepoOwner[] | undefined;
66+
@consume({ context: ipcContext })
67+
private readonly _ipc!: HostIpc;
68+
69+
private renderOwnerFilterLabel() {
70+
console.log('test', this.filter);
71+
if (!this.filter?.length) {
72+
return 'By all users';
73+
}
74+
75+
const additionalLabel = this.filter.length > 1 ? ` and ${pluralize('other', this.filter.length - 1)}` : '';
76+
if (this.filter.some(x => x.current)) {
77+
return `By me${additionalLabel}`;
78+
}
79+
return `By ${this.filter[0].label}${additionalLabel}`;
80+
}
81+
private readonly _getOwners = new Task(this, {
82+
task: async () => {
83+
return this._ipc.sendRequest(GetRepoOwners, undefined);
84+
},
85+
});
86+
87+
private renderOwnerItem(owner: RepoOwner) {
88+
return html`<div class="owner-item">
89+
<div class="owner-label">
90+
${when(owner.avatarSrc, src => html`<gl-avatar src=${src}></gl-avatar>`)}</gl-avatar>
91+
<span>${owner.label}</span>
92+
${when(owner.current, () => html`<span class="current">current</span>`)}
93+
</div>
94+
${when(owner.email, email => html`<span class="owner-email">${email}</span>`)}</gl-avatar>
95+
</div>`;
96+
}
97+
98+
@state()
99+
ownerQuery: string = '';
100+
101+
private filterState: Record<string, boolean> = {};
102+
private renderOwnerList(ownerList: undefined | RepoOwner[]) {
103+
if (!ownerList) {
104+
return html`<p>No available owners</p>`;
105+
}
106+
107+
return html` <input
108+
value=${this.ownerQuery}
109+
@input=${(e: Event) => {
110+
this.ownerQuery = (e.target as HTMLInputElement | null)?.value ?? '';
111+
}}
112+
/>
113+
${repeat(
114+
ownerList.filter(
115+
x => !this.ownerQuery || x.label.includes(this.ownerQuery) || x.email?.includes(this.ownerQuery),
116+
),
117+
item =>
118+
html`<menu-item role="none"
119+
><gl-checkbox
120+
@gl-change-value=${() => {
121+
if (!item.email) {
122+
return;
123+
}
124+
this.filterState[item.email] = !this.filterState[item.email];
125+
}}
126+
?disabled=${!item.email}
127+
?checked=${this.filter?.some(x => x.email === item.email)}
128+
>${this.renderOwnerItem(item)}</gl-checkbox
129+
></menu-item
130+
>`,
131+
)}
132+
<menu-item
133+
@click=${() => {
134+
this.setOwnersFilter(
135+
Object.keys(this.filterState)
136+
.filter(x => this.filterState[x])
137+
.map(email => this._getOwners.value?.find(x => x.email === email))
138+
.filter(IterUtils.notNull),
139+
);
140+
}}
141+
>Apply</menu-item
142+
>
143+
<menu-item
144+
@click=${() => {
145+
this.setOwnersFilter([]);
146+
}}
147+
>Clear</menu-item
148+
>`;
149+
}
150+
151+
private setOwnersFilter(ownersList: RepoOwner[]) {
152+
const event = new CustomEvent('owners-filter-change', {
153+
detail: { ownersList: ownersList },
154+
});
155+
this.dispatchEvent(event);
156+
}
157+
158+
override render() {
159+
return html`
160+
<gl-popover
161+
placement="bottom-start"
162+
trigger="focus"
163+
@gl-popover-show=${() => {
164+
this.ownerQuery = '';
165+
this.filterState = {};
166+
this.filter?.forEach(x => x.email && (this.filterState[x.email] = true));
167+
void this._getOwners.run();
168+
}}
169+
?arrow=${false}
170+
>
171+
<button type="button" slot="anchor" class="owner-filter">
172+
${this.renderOwnerFilterLabel()}<code-icon icon="chevron-down"></code-icon>
173+
</button>
174+
175+
<div slot="content">
176+
${this._getOwners.render({
177+
initial: () => html`<menu-item>Waiting to get owners</menu-item>`,
178+
pending: () => html`<p>Getting repo owners</p>`,
179+
complete: this.renderOwnerList.bind(this),
180+
error: error => html`<p>Oops, something went wrong: ${error}</p>`,
181+
})}
182+
</div>
183+
</gl-popover>
184+
`;
185+
}
186+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export const sectionHeadingStyles = css`
2020
font-weight: normal;
2121
margin-block: 0 0.8rem;
2222
text-transform: uppercase;
23+
display: flex;
24+
justify-content: space-between;
25+
gap: 8px;
2326
}
2427
`;
2528

@@ -40,8 +43,7 @@ export class GlBranchSection extends LitElement {
4043
override render() {
4144
return html`
4245
<div class="section">
43-
<h3 class="section-heading">${this.label}</h3>
44-
<slot></slot>
46+
<h3 class="section-heading"><span>${this.label}</span><slot></slot></h3>
4547
${this.branches.map(branch => html`<gl-branch-card .branch=${branch}></gl-branch-card>`)}
4648
</div>
4749
`;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { css, html } from 'lit';
2+
import { customElement, property } from 'lit/decorators.js';
3+
import { repeat } from 'lit/directives/repeat.js';
4+
import { pluralize } from '../../../../../system/string';
5+
import { OverviewRecentThreshold, OverviewStaleThreshold } from '../../../../home/overviewThreshold';
6+
import '../../../shared/components/checkbox/checkbox';
7+
import '../../../shared/components/code-icon';
8+
import { GlElement } from '../../../shared/components/element';
9+
import '../../../shared/components/menu/index';
10+
import '../../../shared/components/menu/menu-item';
11+
import '../../../shared/components/menu/menu-list';
12+
import '../../../shared/components/overlays/popover';
13+
14+
@customElement('gl-branch-threshold-filter')
15+
export class GlBranchThresholdFilter extends GlElement {
16+
static override readonly styles = [
17+
css`
18+
.date-select {
19+
background: none;
20+
outline: none;
21+
border: none;
22+
cursor: pointer;
23+
color: var(--vscode-disabledForeground);
24+
text-decoration: none !important;
25+
font-weight: 500;
26+
}
27+
.date-select:focus {
28+
outline: 1px solid var(--vscode-disabledForeground);
29+
}
30+
.date-select:hover {
31+
color: var(--vscode-foreground);
32+
text-decoration: underline !important;
33+
}
34+
`,
35+
];
36+
37+
@property({ type: Number }) value: OverviewRecentThreshold | OverviewStaleThreshold | undefined;
38+
@property({ type: Array }) options: (OverviewRecentThreshold | OverviewStaleThreshold)[] | undefined;
39+
private selectDateFilter(threshold: number) {
40+
const event = new CustomEvent('gl-change', {
41+
detail: { threshold: threshold },
42+
});
43+
this.dispatchEvent(event);
44+
}
45+
46+
private renderOption(option: OverviewRecentThreshold | OverviewStaleThreshold) {
47+
switch (option) {
48+
case OverviewRecentThreshold.OneDay:
49+
return '1 day';
50+
case OverviewRecentThreshold.OneWeek:
51+
return '1 week';
52+
case OverviewRecentThreshold.OneMonth:
53+
return '1 month';
54+
case OverviewStaleThreshold.OneYear:
55+
return '1 year';
56+
default:
57+
return pluralize('day', option / OverviewRecentThreshold.OneDay);
58+
}
59+
}
60+
override render() {
61+
if (!this.options) {
62+
return;
63+
}
64+
console.log({ options: this.options, value: this.value });
65+
return html`
66+
<select
67+
class="date-select"
68+
@change=${(e: Event) => this.selectDateFilter(parseInt((e.target as HTMLSelectElement).value))}
69+
>
70+
${repeat(
71+
this.options,
72+
item =>
73+
html`<option value="${item}" ?selected=${this.value === item}>
74+
${this.renderOption(item)}
75+
</option>`,
76+
)}
77+
</select>
78+
`;
79+
}
80+
}

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ 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 { OverviewRecentThreshold } from '../../../../home/overviewThreshold';
56
import type { GetOverviewResponse } from '../../../../home/protocol';
7+
import { SetOverviewFilter } from '../../../../home/protocol';
8+
import { ipcContext } from '../../../shared/context';
9+
import type { HostIpc } from '../../../shared/ipc';
610
import { sectionHeadingStyles } from './branch-section';
711
import type { OverviewState } from './overviewState';
812
import { overviewStateContext } from './overviewState';
913
import '../../../shared/components/skeleton-loader';
14+
import './branch-threshold-filter';
1015

1116
type Overview = GetOverviewResponse;
1217

@@ -51,16 +56,36 @@ export class GlOverview extends SignalWatcher(LitElement) {
5156
`;
5257
}
5358

59+
@consume({ context: ipcContext })
60+
private readonly _ipc!: HostIpc;
61+
5462
private renderComplete(overview: Overview) {
5563
if (overview == null) return nothing;
56-
5764
const { repository } = overview;
5865
return html`
5966
<div class="repository">
6067
<gl-branch-section
6168
label="Recent (${repository.branches.recent.length})"
6269
.branches=${repository.branches.recent}
63-
></gl-branch-section>
70+
>
71+
<gl-branch-threshold-filter
72+
@gl-change=${(e: CustomEvent<{ threshold: OverviewRecentThreshold }>) => {
73+
if (!this._overviewState.filter.stale || !this._overviewState.filter.recent) {
74+
return;
75+
}
76+
this._ipc.sendCommand(SetOverviewFilter, {
77+
stale: this._overviewState.filter.stale,
78+
recent: { ...this._overviewState.filter.recent, threshold: e.detail.threshold },
79+
});
80+
}}
81+
.options=${[
82+
OverviewRecentThreshold.OneDay,
83+
OverviewRecentThreshold.OneWeek,
84+
OverviewRecentThreshold.OneMonth,
85+
]}
86+
.value=${this._overviewState.filter.recent?.threshold}
87+
></gl-branch-threshold-filter>
88+
</gl-branch-section>
6489
<gl-branch-section
6590
hidden
6691
label="Stale (${repository.branches.stale.length})"

0 commit comments

Comments
 (0)