Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 7228b49

Browse files
authored
Svelte: add repo header dropdown menu (#63257)
Adds a dropdown menu when clicking the repo name for common repo-level actions.
1 parent fadcaa2 commit 7228b49

File tree

9 files changed

+235
-52
lines changed

9 files changed

+235
-52
lines changed

client/web-sveltekit/src/auto-imports.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ declare global {
2929
const ILucideCircleHelp: typeof import('~icons/lucide/circle-help')['default']
3030
const ILucideCircleX: typeof import('~icons/lucide/circle-x')['default']
3131
const ILucideCode: typeof import('~icons/lucide/code')['default']
32+
const ILucideCodesandbox: typeof import('~icons/lucide/codesandbox')['default']
3233
const ILucideCopy: typeof import('~icons/lucide/copy')['default']
3334
const ILucideCornerRightDown: typeof import('~icons/lucide/corner-right-down')['default']
3435
const ILucideCornerRightUp: typeof import('~icons/lucide/corner-right-up')['default']
@@ -55,6 +56,7 @@ declare global {
5556
const ILucideFullscreen: typeof import('~icons/lucide/fullscreen')['default']
5657
const ILucideGitBranch: typeof import('~icons/lucide/git-branch')['default']
5758
const ILucideGitCommitVertical: typeof import('~icons/lucide/git-commit-vertical')['default']
59+
const ILucideGitCompare: typeof import('~icons/lucide/git-compare')['default']
5860
const ILucideGitCompareArrows: typeof import('~icons/lucide/git-compare-arrows')['default']
5961
const ILucideGitFork: typeof import('~icons/lucide/git-fork')['default']
6062
const ILucideGitMerge: typeof import('~icons/lucide/git-merge')['default']
@@ -70,6 +72,7 @@ declare global {
7072
const ILucidePanelLeftOpen: typeof import('~icons/lucide/panel-left-open')['default']
7173
const ILucidePencil: typeof import('~icons/lucide/pencil')['default']
7274
const ILucideRegex: typeof import('~icons/lucide/regex')['default']
75+
const ILucideRepeat: typeof import('~icons/lucide/repeat')['default']
7376
const ILucideSearch: typeof import('~icons/lucide/search')['default']
7477
const ILucideSearchX: typeof import('~icons/lucide/search-x')['default']
7578
const ILucideSettings: typeof import('~icons/lucide/settings')['default']

client/web-sveltekit/src/lib/styles/dropdown.scss

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
:root {
22
--dropdown-inner-border-radius: 0.1875rem;
3-
--dropdown-padding-y: 0.5rem;
4-
--dropdown-item-padding-y: 0.25rem;
5-
--dropdown-item-padding-x: 0.5rem;
3+
--dropdown-padding-y: 0.375rem;
4+
--dropdown-item-padding-y: 0.375rem;
5+
--dropdown-item-padding-x: 0.75rem;
66
--dropdown-item-padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x);
77
--dropdown-min-width: 10rem;
88
--dropdown-spacer: 0.125rem;
@@ -25,7 +25,8 @@
2525
--dropdown-link-active-bg: var(--primary);
2626
--dropdown-link-active-color: var(--white);
2727
--dropdown-link-disabled-color: var(--text-muted);
28-
--dropdown-shadow: 0 4px 16px -6px rgba(36, 41, 54, 0.2);
28+
--dropdown-shadow: 0 193px 54px 0 rgba(0, 0, 0, 0), 0 123px 49px 0 rgba(0, 0, 0, 0.01),
29+
0 69px 42px 0 rgba(0, 0, 0, 0.04), 0 31px 31px 0 rgba(0, 0, 0, 0.07), 0 8px 17px 0 rgba(0, 0, 0, 0.08);
2930
}
3031

3132
.theme-dark {
@@ -39,5 +40,6 @@
3940
--dropdown-link-active-bg: var(--primary);
4041
--dropdown-link-active-color: var(--white);
4142
--dropdown-link-disabled-color: var(--text-muted);
42-
--dropdown-shadow: 0 4px 16px -6px rgba(11, 12, 15, 0.8);
43+
--dropdown-shadow: 0 309px 87px 0 rgba(0, 0, 0, 0.01), 0 198px 79px 0 rgba(0, 0, 0, 0.07),
44+
0 111px 67px 0 rgba(0, 0, 0, 0.24), 0 49px 49px 0 rgba(0, 0, 0, 0.41), 0 12px 27px 0 rgba(0, 0, 0, 0.47);
4345
}

client/web-sveltekit/src/lib/wildcard/menu/DropdownMenu.svelte

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts" context="module">
22
import { createContextAccessors } from '$lib/utils/context'
3+
34
type DropdownMenu = ReturnType<typeof createDropdownMenu>
45
56
interface DropdownMenuContext {
@@ -48,14 +49,14 @@
4849
div :global([role='menu']) {
4950
isolation: isolate;
5051
min-width: 12rem;
51-
font-size: 0.875rem;
52+
font-size: var(--font-size-small);
5253
background-clip: padding-box;
5354
background-color: var(--dropdown-bg);
5455
border: 1px solid var(--dropdown-border-color);
5556
border-radius: var(--popover-border-radius);
5657
color: var(--body-color);
5758
box-shadow: var(--dropdown-shadow);
58-
padding: 0.25rem 0;
59+
padding: var(--dropdown-padding-y) 0;
5960
6061
:global([role^='menuitem']) {
6162
all: unset;

client/web-sveltekit/src/lib/wildcard/menu/MenuSeparator.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<style lang="scss">
1010
div {
1111
height: 0;
12-
margin: 0.25rem 0;
12+
margin: var(--dropdown-item-padding-y) 0;
1313
overflow: hidden;
1414
border-top: 1px solid var(--dropdown--separator-color, var(--border-color));
1515
}

client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts

+21
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,27 @@ test.describe('file header', () => {
266266
})
267267
})
268268

269+
test.describe('repo menu', () => {
270+
test('click go to root', async ({ page }) => {
271+
const url = `/${repoName}/-/blob/src/large-file-1.js`
272+
await page.goto(url)
273+
274+
await page.getByRole('heading', { name: 'sourcegraph/sourcegraph' }).click()
275+
await page.getByRole('menuitem', { name: 'Go to repository root' }).click()
276+
await page.waitForURL(`/${repoName}`)
277+
})
278+
279+
test('keyboard shortcut go to root', async ({ page }) => {
280+
const url = `/${repoName}/-/blob/src/large-file-1.js`
281+
await page.goto(url)
282+
// Focus _something_ on the page. Use both mac and linux shortcuts so this works
283+
// both locally and in CI.
284+
await page.getByRole('link', { name: 'Sourcegraph' }).press('Meta+Backspace')
285+
await page.getByRole('link', { name: 'Sourcegraph' }).press('Control+Backspace')
286+
await page.waitForURL(`/${repoName}`)
287+
})
288+
})
289+
269290
test.describe('scroll behavior', () => {
270291
const url = `/${repoName}/-/blob/src/large-file-1.js`
271292

client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte

+31-43
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
import type { ComponentProps } from 'svelte'
33
import { writable } from 'svelte/store'
44
5+
import { getButtonClassName } from '@sourcegraph/wildcard'
6+
7+
import { goto } from '$app/navigation'
58
import { page } from '$app/stores'
69
import { sizeToFit } from '$lib/dom'
10+
import { registerHotkey } from '$lib/Hotkey'
711
import Icon from '$lib/Icon.svelte'
812
import GlobalHeaderPortal from '$lib/navigation/GlobalHeaderPortal.svelte'
9-
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
1013
import { createScopeSuggestions } from '$lib/search/codemirror/suggestions'
1114
import SearchInput from '$lib/search/input/SearchInput.svelte'
1215
import { queryStateStore } from '$lib/search/state'
@@ -15,10 +18,10 @@
1518
import { default as TabsHeader } from '$lib/TabsHeader.svelte'
1619
import { TELEMETRY_RECORDER } from '$lib/telemetry'
1720
import { DropdownMenu, MenuLink } from '$lib/wildcard'
18-
import { getButtonClassName } from '$lib/wildcard/Button'
1921
2022
import type { LayoutData } from './$types'
2123
import { setRepositoryPageContext, type RepositoryPageContext } from './context'
24+
import RepoMenu from './RepoMenu.svelte'
2225
2326
interface MenuEntry {
2427
/**
@@ -50,10 +53,10 @@
5053
{ path: '/-/stats/contributors', icon: ILucideUsers, label: 'Contributors', visibility: 'user' },
5154
]
5255
const menuEntries: MenuEntry[] = [
53-
{ path: '/-/compare', icon: ILucideHistory, label: 'Compare', visibility: 'user' },
56+
{ path: '/-/compare', icon: ILucideGitCompare, label: 'Compare', visibility: 'user' },
5457
{ path: '/-/own', icon: ILucideUsers, label: 'Ownership', visibility: 'admin' },
5558
{ path: '/-/embeddings', icon: ILucideSpline, label: 'Embeddings', visibility: 'admin' },
56-
{ path: '/-/code-graph', icon: ILucideBrainCircuit, label: 'Code graph data', visibility: 'admin' },
59+
{ path: '/-/code-graph', icon: ILucideCodesandbox, label: 'Code graph data', visibility: 'admin' },
5760
{ path: '/-/batch-changes', icon: ISgBatchChanges, label: 'Batch changes', visibility: 'admin' },
5861
{ path: '/-/settings', icon: ILucideSettings, label: 'Settings', visibility: 'admin' },
5962
]
@@ -97,14 +100,25 @@
97100
}))
98101
$: selectedTab = tabs.findIndex(tab => isActive(tab.href, $page.url))
99102
100-
$: ({ repoName, displayRepoName, revision, resolvedRevision } = data)
103+
$: ({ repoName, revision } = data)
101104
$: query = `repo:${repositoryInsertText({ repository: repoName })}${revision ? `@${revision}` : ''} `
102105
$: queryState = queryStateStore({ query }, $settings)
103106
function handleSearchSubmit(): void {
104107
TELEMETRY_RECORDER.recordEvent('search', 'submit', {
105108
metadata: { source: TELEMETRY_SEARCH_SOURCE_TYPE['repo'] },
106109
})
107110
}
111+
112+
registerHotkey({
113+
keys: {
114+
key: 'ctrl+backspace',
115+
mac: 'cmd+backspace',
116+
},
117+
ignoreInputFields: false,
118+
handler: () => {
119+
goto(data.repoURL)
120+
},
121+
})
108122
</script>
109123

110124
<GlobalHeaderPortal>
@@ -126,10 +140,13 @@
126140
},
127141
}}
128142
>
129-
<a href={data.repoURL}>
130-
<CodeHostIcon repository={repoName} codeHost={resolvedRevision?.repo?.externalRepository?.serviceType} />
131-
<h1>{displayRepoName}</h1>
132-
</a>
143+
<RepoMenu
144+
repoName={data.repoName}
145+
displayRepoName={data.displayRepoName}
146+
repoURL={data.repoURL}
147+
externalURL={data.resolvedRevision?.repo?.externalURLs?.[0].url}
148+
externalServiceKind={data.resolvedRevision?.repo?.externalURLs?.[0].serviceKind ?? undefined}
149+
/>
133150

134151
<TabsHeader id="repoheader" {tabs} selected={selectedTab} />
135152

@@ -145,12 +162,12 @@
145162
{#if entry.visibility === 'user' || (entry.visibility === 'admin' && data.user?.siteAdmin)}
146163
{@const href = data.repoURL + entry.path}
147164
<MenuLink {href}>
148-
<span class="overflow-entry" class:active={isActive(href, $page.url)}>
165+
<div class="overflow-entry">
149166
{#if entry.icon}
150167
<Icon icon={entry.icon} inline aria-hidden />
151168
{/if}
152169
<span>{entry.label}</span>
153-
</span>
170+
</div>
154171
</MenuLink>
155172
{/if}
156173
{/each}
@@ -176,40 +193,11 @@
176193
overflow: hidden;
177194
border-bottom: 1px solid var(--border-color);
178195
background-color: var(--color-bg-1);
179-
180-
a {
181-
all: unset;
182-
183-
display: flex;
184-
align-items: center;
185-
gap: 0.5rem;
186-
padding: 0 1rem;
187-
cursor: pointer;
188-
&:hover {
189-
background-color: var(--color-bg-2);
190-
}
191-
192-
h1 {
193-
display: contents;
194-
font-size: 1rem;
195-
white-space: nowrap;
196-
color: var(--text-title);
197-
font-weight: normal;
198-
}
199-
}
200-
201-
:global([data-dropdown-trigger]) {
202-
height: 100%;
203-
align-self: stretch;
204-
padding: 0.5rem;
205-
--icon-fill-color: var(--text-muted);
206-
}
207196
}
208197
209198
.overflow-entry {
210-
width: 100%;
211-
display: inline-block;
212-
padding: 0 0.25rem;
213-
border-radius: var(--border-radius);
199+
display: flex;
200+
gap: 0.5rem;
201+
align-items: center;
214202
}
215203
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<script lang="ts">
2+
import { openFuzzyFinder } from '$lib/fuzzyfinder/FuzzyFinderContainer.svelte'
3+
import { reposHotkey } from '$lib/fuzzyfinder/keys'
4+
import Icon from '$lib/Icon.svelte'
5+
import KeyboardShortcut from '$lib/KeyboardShortcut.svelte'
6+
import { getHumanNameForCodeHost } from '$lib/repo/shared/codehost'
7+
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
8+
import { getButtonClassName } from '$lib/wildcard/Button'
9+
import DropdownMenu from '$lib/wildcard/menu/DropdownMenu.svelte'
10+
import MenuButton from '$lib/wildcard/menu/MenuButton.svelte'
11+
import MenuLink from '$lib/wildcard/menu/MenuLink.svelte'
12+
import MenuSeparator from '$lib/wildcard/menu/MenuSeparator.svelte'
13+
14+
export let repoName: string
15+
export let displayRepoName: string
16+
export let repoURL: string
17+
18+
export let externalURL: string | undefined
19+
export let externalServiceKind: string | undefined
20+
</script>
21+
22+
<DropdownMenu triggerButtonClass={getButtonClassName({ variant: 'text' })}>
23+
<svelte:fragment slot="trigger">
24+
<div class="trigger">
25+
<CodeHostIcon repository={repoName} codeHost={externalServiceKind} />
26+
<h2>
27+
{#each displayRepoName.split('/') as segment, i}
28+
{#if i > 0}<span class="slash">/</span>{/if}{segment}
29+
{/each}
30+
</h2>
31+
</div>
32+
</svelte:fragment>
33+
34+
<MenuLink href={repoURL}>
35+
<div class="menu-item">
36+
<Icon icon={ILucideHome} inline />
37+
<span>Go to repository root</span>
38+
<KeyboardShortcut shortcut={{ key: 'ctrl+backspace', mac: 'cmd+backspace' }} />
39+
</div>
40+
</MenuLink>
41+
<MenuButton on:click={() => openFuzzyFinder('repos')}>
42+
<div class="menu-item">
43+
<Icon icon={ILucideRepeat} inline />
44+
<span>Switch repo</span>
45+
<KeyboardShortcut shortcut={reposHotkey} />
46+
</div>
47+
</MenuButton>
48+
<MenuLink href="{repoURL}/-/settings">
49+
<div class="menu-item">
50+
<Icon icon={ILucideSettings} inline />
51+
<span>Settings</span>
52+
</div>
53+
</MenuLink>
54+
{#if externalURL}
55+
<MenuSeparator />
56+
<MenuLink href={externalURL} target="_blank" rel="noreferrer noopener">
57+
<div class="code-host-item">
58+
<small>
59+
{#if externalServiceKind}
60+
Hosted on {getHumanNameForCodeHost(externalServiceKind)}
61+
{:else}
62+
View on code host
63+
{/if}
64+
</small>
65+
<div>
66+
<CodeHostIcon repository={repoName} codeHost={externalServiceKind} />
67+
<span>{displayRepoName}</span>
68+
</div>
69+
</div>
70+
</MenuLink>
71+
{/if}
72+
</DropdownMenu>
73+
74+
<style lang="scss">
75+
.trigger {
76+
display: flex;
77+
align-items: center;
78+
gap: 0.5rem;
79+
white-space: nowrap;
80+
81+
h2 {
82+
font-size: var(--font-size-large);
83+
font-weight: 500;
84+
margin: 0;
85+
86+
.slash {
87+
font-weight: 400;
88+
color: var(--text-muted);
89+
margin: 0.25em;
90+
letter-spacing: -0.25px;
91+
}
92+
}
93+
}
94+
95+
.menu-item {
96+
display: flex;
97+
gap: 0.5rem;
98+
min-width: 20rem;
99+
align-items: center;
100+
color: var(--color-text);
101+
--icon-color: currentColor;
102+
103+
:global(kbd) {
104+
margin-left: auto;
105+
}
106+
}
107+
108+
.code-host-item {
109+
display: flex;
110+
flex-direction: column;
111+
gap: 0.25rem;
112+
113+
small {
114+
color: var(--text-muted);
115+
}
116+
117+
div {
118+
display: flex;
119+
gap: 0.5em;
120+
align-items: center;
121+
}
122+
}
123+
</style>

client/web-sveltekit/src/routes/[...repo=reporev]/layout.gql

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ fragment ResolvedRepository on Repository {
2828
defaultBranch {
2929
abbrevName
3030
}
31-
31+
externalURLs {
32+
url
33+
serviceKind
34+
}
3235
...RepoPage_ResolvedRevision
3336
...BlobPage_ResolvedRevision
3437
}

0 commit comments

Comments
 (0)