Skip to content

Commit f6ea6b0

Browse files
authored
UI branches page wip (#8138)
* v2: remove branch selector in header Looks like no one using it * `SeriesLabel` remove unused prop * branches page wip
1 parent b649bcf commit f6ea6b0

17 files changed

+985
-25
lines changed

apps/desktop/src/components/BranchPreviewHeader.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
<div class="header__wrapper">
7979
<div class="header card">
8080
<div class="header__info">
81-
<SeriesLabelsRow series={stackBranchNames} showRestAmount />
81+
<SeriesLabelsRow series={stackBranchNames} />
8282
<div class="header__remote-branch">
8383
{#if remoteBranch}
8484
<Tooltip text="At least some of your changes have been pushed">

apps/desktop/src/components/CollapsedLane.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
{uncommittedChanges === 1 ? 'change' : 'changes'}
4949
</Badge>
5050
{/if}
51-
<SeriesLabelsRow series={nonArchivedSeries.map((s) => s.name)} showRestAmount />
51+
<SeriesLabelsRow series={nonArchivedSeries.map((s) => s.name)} />
5252
</div>
5353

5454
<div class="collapsed-lane__info__details">

apps/desktop/src/components/SeriesLabels.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
99
interface Props {
1010
series: (PatchSeries | Error)[];
11-
disableSelector?: boolean;
1211
}
1312
14-
const { series, disableSelector }: Props = $props();
13+
const { series }: Props = $props();
1514
1615
const shiftedSeries = $derived(series.slice(1));
1716
const seriesTypes = $derived(
@@ -30,7 +29,7 @@
3029
</script>
3130

3231
<div class="stack-series-row">
33-
<SeriesLabelsRow series={series.map((s) => s.name)} showRestAmount={disableSelector} />
32+
<SeriesLabelsRow series={series.map((s) => s.name)} />
3433

3534
<!-- SERIES SELECTOR -->
3635
{#if series.length > 1}

apps/desktop/src/components/v3/HeaderMetaSection.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<script lang="ts">
22
import BranchLaneContextMenu from '$components/BranchLaneContextMenu.svelte';
3-
import SeriesRowLabels from '$components/SeriesLabels.svelte';
43
import { PatchSeries } from '$lib/branches/branch';
54
import Button from '@gitbutler/ui/Button.svelte';
65
import ContextMenu from '@gitbutler/ui/ContextMenu.svelte';
6+
import SeriesLabelsRow from '@gitbutler/ui/SeriesLabelsRow.svelte';
77
88
interface Props {
99
projectId: string;
@@ -21,7 +21,7 @@
2121

2222
<div class="stack-meta">
2323
<div class="stack-meta-top">
24-
<SeriesRowLabels {series} />
24+
<SeriesLabelsRow series={series.map((s) => s.name)} />
2525

2626
<Button
2727
bind:el={kebabButtonEl}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
<script lang="ts">
2+
import BranchesCardTemplate from '$components/v3/branchesPage/BranchesCardTemplate.svelte';
3+
import { BranchListingDetails, type BranchListing } from '$lib/branches/branchListing';
4+
import { BranchService } from '$lib/branches/branchService.svelte';
5+
import { GitConfigService } from '$lib/config/gitConfigService';
6+
import { Project } from '$lib/project/project';
7+
import { UserService } from '$lib/user/userService';
8+
import { inject } from '@gitbutler/shared/context';
9+
import ReviewBadge from '@gitbutler/ui/ReviewBadge.svelte';
10+
import SeriesLabelsRow from '@gitbutler/ui/SeriesLabelsRow.svelte';
11+
// import SidebarEntry from '@gitbutler/ui/SidebarEntry.svelte';
12+
import TimeAgo from '@gitbutler/ui/TimeAgo.svelte';
13+
import AvatarGroup from '@gitbutler/ui/avatar/AvatarGroup.svelte';
14+
import { gravatarUrlFromEmail } from '@gitbutler/ui/avatar/gravatar';
15+
import type { PullRequest } from '$lib/forge/interface/types';
16+
import { goto } from '$app/navigation';
17+
import { page } from '$app/state';
18+
19+
interface Props {
20+
projectId: string;
21+
branchListing: BranchListing;
22+
prs: PullRequest[];
23+
}
24+
25+
const { projectId, branchListing, prs }: Props = $props();
26+
27+
const unknownName = 'unknown';
28+
const unknownEmail = '[email protected]';
29+
30+
const [userService, gitConfigService, project, branchService] = inject(
31+
UserService,
32+
GitConfigService,
33+
Project,
34+
BranchService
35+
);
36+
37+
const user = userService.user;
38+
39+
// TODO: Use information from all PRs in a stack?
40+
const pr = $derived(prs.at(0));
41+
42+
let hasBeenSeen = $state(false);
43+
44+
const branchDetailsResult = $derived(
45+
hasBeenSeen ? branchService.get(projectId, branchListing.name) : undefined
46+
);
47+
48+
let lastCommitDetails = $state<{ authorName: string; lastCommitAt?: Date }>();
49+
let branchListingDetails = $derived(branchDetailsResult?.current.data);
50+
51+
// If there are zero commits we should not show the author
52+
const ownedByUser = $derived(branchListingDetails?.numberOfCommits === 0);
53+
54+
function handleClick() {
55+
if (branchListing.stack?.inWorkspace) {
56+
goto(`/${project.id}/board`);
57+
} else {
58+
goto(formatBranchURL(project, branchListing.name));
59+
}
60+
}
61+
62+
const selected = $derived(page.url.pathname === formatBranchURL(project, branchListing.name));
63+
64+
function formatBranchURL(project: Project, name: string) {
65+
return `/${project.id}/branch/${encodeURIComponent(name)}`;
66+
}
67+
68+
$effect(() => {
69+
let canceled = false;
70+
71+
if (ownedByUser) {
72+
gitConfigService.get('user.name').then((userName) => {
73+
if (canceled) return;
74+
75+
if (userName) {
76+
lastCommitDetails = { authorName: userName };
77+
} else {
78+
lastCommitDetails = undefined;
79+
}
80+
});
81+
} else {
82+
lastCommitDetails = {
83+
authorName: branchListing.lastCommiter.name || unknownName,
84+
lastCommitAt: new Date(branchListing.updatedAt)
85+
};
86+
}
87+
});
88+
89+
let avatars = $state<{ name: string; srcUrl: string }[]>([]);
90+
91+
async function setAvatars(ownedByUser: boolean, branchListingDetails?: BranchListingDetails) {
92+
if (ownedByUser) {
93+
const name = (await gitConfigService.get('user.name')) || unknownName;
94+
const email = (await gitConfigService.get('user.email')) || unknownEmail;
95+
const srcUrl =
96+
email.toLowerCase() === $user?.email?.toLowerCase() && $user?.picture
97+
? $user?.picture
98+
: await gravatarUrlFromEmail(email);
99+
100+
avatars = [{ name, srcUrl: srcUrl }];
101+
} else if (branchListingDetails) {
102+
avatars = branchListingDetails.authors
103+
? await Promise.all(
104+
branchListingDetails.authors.map(async (author) => {
105+
return {
106+
name: author.name || unknownName,
107+
srcUrl:
108+
(author.email?.toLowerCase() === $user?.email?.toLowerCase()
109+
? $user?.picture
110+
: author.gravatarUrl) ??
111+
(await gravatarUrlFromEmail(author.email || unknownEmail))
112+
};
113+
})
114+
)
115+
: [];
116+
} else {
117+
avatars = [];
118+
}
119+
}
120+
121+
const stackBranches = $derived(branchListing.stack?.branches);
122+
const filteredStackBranches = $derived(
123+
stackBranches && stackBranches.length > 0 ? stackBranches : [branchListing.name]
124+
);
125+
126+
const pullRequestDetails = $derived(
127+
pr && {
128+
title: pr.title,
129+
draft: pr.draft,
130+
number: pr.number
131+
}
132+
);
133+
134+
$effect(() => {
135+
setAvatars(ownedByUser, branchListingDetails);
136+
});
137+
</script>
138+
139+
<BranchesCardTemplate {selected} onclick={handleClick}>
140+
{#snippet content()}
141+
<div class="sidebar-entry__header">
142+
<SeriesLabelsRow series={filteredStackBranches} />
143+
{#if branchListing.stack?.inWorkspace}
144+
<div class="sidebar-entry__applied-tag">
145+
<span class="text-10 text-semibold">Workspace</span>
146+
</div>
147+
{/if}
148+
</div>
149+
150+
<div class="text-12 sidebar-entry__about">
151+
{#if pullRequestDetails}
152+
<ReviewBadge
153+
prStatus={pullRequestDetails.draft ? 'draft' : 'unknown'}
154+
prTitle={pullRequestDetails.title}
155+
prNumber={pullRequestDetails.number}
156+
/>
157+
<span class="sidebar-entry__divider">•</span>
158+
{/if}
159+
160+
<AvatarGroup {avatars} />
161+
162+
<!-- NEED API -->
163+
{#each branchListing.remotes as remote}
164+
<span class="sidebar-entry__divider">•</span>
165+
<span>{remote}</span>
166+
{/each}
167+
{#if branchListing.hasLocal}
168+
<span class="sidebar-entry__divider">•</span>
169+
<span>local</span>
170+
{/if}
171+
{#if branchListing.remotes.length === 0 && !branchListing.hasLocal}
172+
<span class="sidebar-entry__divider">•</span>
173+
<span>No remotes</span>
174+
{/if}
175+
</div>
176+
{/snippet}
177+
{#snippet details()}
178+
<div class="text-12 sidebar-entry__details">
179+
<span>
180+
{#if lastCommitDetails}
181+
<TimeAgo date={lastCommitDetails.lastCommitAt} addSuffix />
182+
by {lastCommitDetails.authorName}
183+
{/if}
184+
</span>
185+
186+
{#if branchListingDetails}
187+
<div class="sidebar-entry__details-item">
188+
{#if branchListingDetails.linesAdded}
189+
<span>
190+
+{branchListingDetails.linesAdded}
191+
</span>
192+
{/if}
193+
{#if branchListingDetails.linesRemoved}
194+
<span>
195+
-{branchListingDetails.linesRemoved}
196+
</span>
197+
{/if}
198+
199+
<svg
200+
width="14"
201+
height="12"
202+
viewBox="0 0 14 12"
203+
fill="none"
204+
xmlns="http://www.w3.org/2000/svg"
205+
>
206+
<path
207+
d="M10 6C10 7.65685 8.65685 9 7 9C5.34315 9 4 7.65685 4 6M10 6C10 4.34315 8.65685 3 7 3C5.34315 3 4 4.34315 4 6M10 6H14M4 6H0"
208+
stroke="currentColor"
209+
/>
210+
</svg>
211+
212+
<span>{branchListingDetails?.numberOfCommits}</span>
213+
</div>
214+
{/if}
215+
</div>
216+
{/snippet}
217+
</BranchesCardTemplate>
218+
219+
<!-- <SidebarEntry
220+
series={filteredStackBranches}
221+
remotes={branchListing.remotes}
222+
local={branchListing.hasLocal}
223+
applied={branchListing.stack?.inWorkspace}
224+
{lastCommitDetails}
225+
pullRequestDetails={pr && {
226+
title: pr.title,
227+
draft: pr.draft
228+
}}
229+
branchDetails={branchListingDetails && {
230+
commitCount: branchListingDetails.numberOfCommits,
231+
linesAdded: branchListingDetails.linesAdded,
232+
linesRemoved: branchListingDetails.linesRemoved
233+
}}
234+
onFirstSeen={() => (hasBeenSeen = true)}
235+
{onMouseDown}
236+
{selected}
237+
{avatars}
238+
/> -->
239+
240+
<style lang="postcss">
241+
.sidebar-entry__about {
242+
display: flex;
243+
align-items: center;
244+
gap: 6px;
245+
color: var(--clr-text-2);
246+
}
247+
248+
.sidebar-entry__header {
249+
display: flex;
250+
align-items: center;
251+
gap: 10px;
252+
}
253+
254+
.sidebar-entry__divider {
255+
color: var(--clr-text-3);
256+
}
257+
258+
.sidebar-entry__applied-tag {
259+
display: flex;
260+
background-color: var(--clr-scale-ntrl-50);
261+
padding: 2px 4px;
262+
border-radius: 10px;
263+
color: var(--clr-theme-ntrl-on-element);
264+
}
265+
266+
.sidebar-entry__details {
267+
display: flex;
268+
gap: 6px;
269+
align-items: center;
270+
justify-content: space-between;
271+
width: 100%;
272+
}
273+
274+
.sidebar-entry__details-item {
275+
display: flex;
276+
gap: 5px;
277+
align-items: center;
278+
color: var(--clr-text-2);
279+
}
280+
</style>

0 commit comments

Comments
 (0)