mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
chore(svelte): Hide cody nav entries and upsell banner when cody isn't enabled (#63463)
Contributes to srch-529 This commit refactors the main navigation and search home page code to make it more configurable. In particular we now only show navigation entries for features that are enabled (as determined by `window.context`) and only show the cody upsell banner when cody is enabled. I extended the dev HTML template and .env files to support this. ## Test plan Manual testing.
This commit is contained in:
parent
40dc6965e8
commit
d4548b2be5
@ -1 +1,5 @@
|
||||
PUBLIC_DOTCOM=
|
||||
PUBLIC_CODY_ENABLED_ON_INSTANCE=true
|
||||
PUBLIC_CODY_ENABLED_FOR_CURRENT_USER=true
|
||||
PUBLIC_BATCH_CHANGES_ENABLED=true
|
||||
PUBLIC_CODE_INSIGHTS_ENABLED=true
|
||||
|
||||
@ -1 +1,5 @@
|
||||
PUBLIC_DOTCOM=true
|
||||
PUBLIC_CODY_ENABLED_ON_INSTANCE=true
|
||||
PUBLIC_CODY_ENABLED_FOR_CURRENT_USER=true
|
||||
PUBLIC_BATCH_CHANGES_ENABLED=
|
||||
PUBLIC_CODE_INSIGHTS_ENABLED=
|
||||
|
||||
@ -324,6 +324,10 @@ svelte_check.svelte_check_test(
|
||||
args = [
|
||||
"--tsconfig",
|
||||
"tsconfig.json",
|
||||
"--compiler-warnings",
|
||||
# missing-declaration is raised for our icon components. The Svelte compiler
|
||||
# does not take into account ambient declarations (will be fixed in Svelte 5).
|
||||
"missing-declaration:ignore",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = SRCS + BUILD_DEPS + CONFIGS + [
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"build:watch": "vite build --watch",
|
||||
"preview": "vite preview",
|
||||
"install:browsers": "playwright install",
|
||||
"test": "playwright test",
|
||||
"test": "DISABLE_APP_ASSETS_MOCKING=true playwright test",
|
||||
"test:dev": "DISABLE_APP_ASSETS_MOCKING=true PORT=5173 playwright test --ui",
|
||||
"test:svelte": "vitest --run",
|
||||
"sync": "svelte-kit sync",
|
||||
|
||||
@ -5,6 +5,14 @@ const PORT = process.env.PORT ? Number(process.env.PORT) : 4173
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testMatch: 'src/**/*.spec.ts',
|
||||
// For local testing
|
||||
webServer: process.env.DISABLE_APP_ASSETS_MOCKING
|
||||
? {
|
||||
command: 'pnpm build:preview && pnpm preview',
|
||||
port: PORT,
|
||||
reuseExistingServer: true,
|
||||
}
|
||||
: undefined,
|
||||
reporter: 'list',
|
||||
// note: if you proxy into a locally running vite preview, you may have to raise this to 60 seconds
|
||||
timeout: 5_000,
|
||||
|
||||
@ -19,7 +19,14 @@
|
||||
},
|
||||
// Local standalone dev server for dotcom can be started with
|
||||
// pnpm dev:dotcom
|
||||
sourcegraphDotComMode: '%sveltekit.env.PUBLIC_DOTCOM%' ? true : false,
|
||||
sourcegraphDotComMode: !!'%sveltekit.env.PUBLIC_DOTCOM%',
|
||||
codyEnabledOnInstance: !!'%sveltekit.env.PUBLIC_CODY_ENABLED_ON_INSTANCE%',
|
||||
codyEnabledForCurrentUser: !!'%sveltekit.env.PUBLIC_CODY_ENABLED_FOR_CURRENT_USER%',
|
||||
batchChangesEnabled: !!'%sveltekit.env.PUBLIC_BATCH_CHANGES_ENABLED%',
|
||||
codeInsightsEnabled: !!'%sveltekit.env.PUBLIC_CODE_INSIGHTS_ENABLED%',
|
||||
|
||||
// The following are used to mock context in playwright tests
|
||||
...(typeof window.context === 'object' ? window.context : {}),
|
||||
}
|
||||
window.pageError = undefined
|
||||
</script>
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
type FlipOptions,
|
||||
} from '@floating-ui/dom'
|
||||
import { tick } from 'svelte'
|
||||
import type { ActionReturn, Action } from 'svelte/action'
|
||||
import type { Action } from 'svelte/action'
|
||||
import * as uuid from 'uuid'
|
||||
|
||||
import { highlightNode } from '$lib/common'
|
||||
@ -96,18 +96,29 @@ export function uniqueID(prefix = ''): string {
|
||||
* An action that dispatches a custom 'click-outside' event when the user clicks
|
||||
* outside the attached element.
|
||||
*/
|
||||
export function onClickOutside(
|
||||
node: HTMLElement
|
||||
): ActionReturn<void, { 'on:click-outside': (event: CustomEvent<HTMLElement>) => void }> {
|
||||
export const onClickOutside: Action<
|
||||
HTMLElement,
|
||||
{ enabled?: boolean } | undefined,
|
||||
{ 'on:click-outside': (event: CustomEvent<HTMLElement>) => void }
|
||||
> = (node, { enabled } = { enabled: true }) => {
|
||||
function handler(event: MouseEvent): void {
|
||||
if (event.target && !node.contains(event.target as HTMLElement)) {
|
||||
node.dispatchEvent(new CustomEvent('click-outside', { detail: event.target }))
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousedown', handler)
|
||||
if (enabled) {
|
||||
window.addEventListener('mousedown', handler)
|
||||
}
|
||||
|
||||
return {
|
||||
update({ enabled } = { enabled: true }) {
|
||||
if (enabled) {
|
||||
window.addEventListener('mousedown', handler)
|
||||
} else {
|
||||
window.removeEventListener('mousedown', handler)
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
window.removeEventListener('mousedown', handler)
|
||||
},
|
||||
|
||||
@ -14,57 +14,114 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { page } from '$app/stores'
|
||||
import { onClickOutside } from '$lib/dom'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { mark } from '$lib/images'
|
||||
import GlobalSidebarNavigation from '$lib/navigation/GlobalSidebarNavigation.svelte'
|
||||
import MainNavigationEntry from '$lib/navigation/MainNavigationEntry.svelte'
|
||||
import MainNavigationLink from '$lib/navigation/MainNavigationLink.svelte'
|
||||
import Popover from '$lib/Popover.svelte'
|
||||
import SourcegraphLogo from '$lib/SourcegraphLogo.svelte'
|
||||
import { isViewportMediumDown } from '$lib/stores'
|
||||
import { Badge, Button } from '$lib/wildcard'
|
||||
|
||||
import { GlobalNavigation_User } from './GlobalNavigation.gql'
|
||||
import type { NavigationEntry, NavigationMenu } from './mainNavigation'
|
||||
import { type NavigationEntry, type NavigationMenu, isNavigationMenu, isCurrent } from './mainNavigation'
|
||||
import UserMenu from './UserMenu.svelte'
|
||||
|
||||
export let authenticatedUser: GlobalNavigation_User | null | undefined
|
||||
export let handleOptOut: (() => Promise<void>) | undefined
|
||||
export let entries: (NavigationEntry | NavigationMenu)[]
|
||||
|
||||
let isSidebarNavigationOpen: boolean = false
|
||||
|
||||
const isDevOrS2 =
|
||||
(browser && window.location.hostname === 'localhost') ||
|
||||
window.location.hostname === 'sourcegraph.sourcegraph.com'
|
||||
|
||||
let sidebarNavigationOpen: boolean = false
|
||||
let closeMenuTimer: number = 0
|
||||
let openedMenu: string = ''
|
||||
|
||||
$: withCustomContent = $navigationModeStore === NavigationMode.WithCustomContent
|
||||
$: sidebarMode = withCustomContent || $isViewportMediumDown
|
||||
|
||||
function openMenu(menu: string) {
|
||||
openedMenu = menu
|
||||
clearTimeout(closeMenuTimer)
|
||||
}
|
||||
function closeMenu() {
|
||||
// We use a delay to close the menu to make it easier to navigate (back) to it
|
||||
closeMenuTimer = window.setTimeout(() => {
|
||||
openedMenu = ''
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="root" data-global-header>
|
||||
{#if isSidebarNavigationOpen}
|
||||
<GlobalSidebarNavigation onClose={() => (isSidebarNavigationOpen = false)} {entries} />
|
||||
{/if}
|
||||
|
||||
<div class="logo" class:with-custom-content={withCustomContent}>
|
||||
{#if withCustomContent}
|
||||
<button class="menu-button" on:click={() => (isSidebarNavigationOpen = true)}>
|
||||
<Icon icon={ILucideMenu} aria-label="Navigation menu" />
|
||||
</button>
|
||||
{/if}
|
||||
<header class="root" data-global-header class:withCustomContent class:sidebarMode>
|
||||
<div class="logo">
|
||||
<button class="menu-button" on:click={() => (sidebarNavigationOpen = true)}>
|
||||
<Icon icon={ILucideMenu} aria-label="Navigation menu" />
|
||||
</button>
|
||||
|
||||
<a href="/search">
|
||||
<img src={mark} alt="Sourcegraph" width="25" height="25" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="plain-navigation" bind:this={$extensionElement}>
|
||||
{#if !withCustomContent}
|
||||
<ul class="plain-navigation-list">
|
||||
<nav aria-label="Main" class:as-sidebar={sidebarMode} class:open={sidebarNavigationOpen}>
|
||||
<!-- Additional wrapper needed to handle sidebar navigation mode -->
|
||||
<div
|
||||
class="content"
|
||||
use:onClickOutside={{ enabled: sidebarNavigationOpen }}
|
||||
on:click-outside={() => (sidebarNavigationOpen = false)}
|
||||
>
|
||||
<div class="sidebar-navigation-header">
|
||||
<button class="close-button" on:click={() => (sidebarNavigationOpen = false)}>
|
||||
<Icon icon={ILucideX} aria-label="Close sidebar navigation" />
|
||||
</button>
|
||||
|
||||
<a href="/search" class="logo-link">
|
||||
<SourcegraphLogo width="9.1rem" />
|
||||
</a>
|
||||
</div>
|
||||
<ul class="top-navigation">
|
||||
{#each entries as entry (entry.label)}
|
||||
<MainNavigationEntry {entry} />
|
||||
{@const open = openedMenu === entry.label}
|
||||
<li class:open on:mouseenter={() => openMenu(entry.label)} on:mouseleave={closeMenu}>
|
||||
{#if isNavigationMenu(entry)}
|
||||
<span>
|
||||
<MainNavigationLink {entry} />
|
||||
<Button
|
||||
variant="icon"
|
||||
on:click={() => (openedMenu = open ? '' : entry.label)}
|
||||
aria-label="{open ? 'Close' : 'Open'} '{entry.label}' submenu"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<Icon icon={ILucideChevronDown} inline aria-hidden />
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
<ul class="sub-navigation">
|
||||
{#each entry.children as subEntry (subEntry.label)}
|
||||
<li>
|
||||
<MainNavigationLink
|
||||
entry={subEntry}
|
||||
aria-current={isCurrent(subEntry, $page) ? 'page' : 'false'}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<span>
|
||||
<MainNavigationLink {entry} aria-current={isCurrent(entry, $page) ? 'page' : 'false'} />
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="global-portal" bind:this={$extensionElement} />
|
||||
|
||||
<Popover let:registerTrigger showOnHover hoverDelay={100} hoverCloseDelay={50}>
|
||||
<span class="web-next-badge" use:registerTrigger>
|
||||
<Badge variant="warning">Experimental</Badge>
|
||||
@ -121,13 +178,11 @@
|
||||
margin-left: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.with-custom-content {
|
||||
.sidebarMode & {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo img {
|
||||
&:hover {
|
||||
img:hover {
|
||||
@keyframes spin {
|
||||
50% {
|
||||
transform: rotate(180deg) scale(1.2);
|
||||
@ -143,21 +198,216 @@
|
||||
}
|
||||
}
|
||||
|
||||
.plain-navigation {
|
||||
.global-portal {
|
||||
display: none;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
|
||||
&-list {
|
||||
.withCustomContent & {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal navigation style, only used on search home page
|
||||
nav:not(.as-sidebar) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-navigation-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-navigation {
|
||||
--icon-color: var(--header-icon-color);
|
||||
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
margin: -0.5rem 0 -0.5rem 0;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
background-size: contain;
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
border-color: transparent;
|
||||
|
||||
&.open,
|
||||
&:hover {
|
||||
border-color: var(--border-color-2);
|
||||
}
|
||||
|
||||
&.open .sub-navigation {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
height: 100%;
|
||||
border-bottom: 2px solid;
|
||||
border-color: inherit;
|
||||
|
||||
:global(a) {
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(a[aria-current='page']) {
|
||||
border-color: var(--brand-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-navigation {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(100% + 3px);
|
||||
|
||||
min-width: 10rem;
|
||||
background-clip: padding-box;
|
||||
background-color: var(--dropdown-bg);
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
border-radius: var(--popover-border-radius);
|
||||
color: var(--body-color);
|
||||
box-shadow: var(--dropdown-shadow);
|
||||
padding: 0.25rem 0;
|
||||
// This seems necessary to make the dropdown render above other elements
|
||||
// and keep it open when moving the mouse into it.
|
||||
z-index: 2;
|
||||
|
||||
> li {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--dropdown-item-padding);
|
||||
white-space: nowrap;
|
||||
color: var(--dropdown-link-hover-color);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--dropdown-link-hover-bg);
|
||||
color: var(--dropdown-link-hover-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar navigation style
|
||||
nav.as-sidebar {
|
||||
display: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
// Fixed overlay color TODO: find a better design token for it
|
||||
background-color: rgba(172, 182, 192, 0.2);
|
||||
// Ensures that the sidebar navigation is all other elements
|
||||
z-index: 2;
|
||||
|
||||
.content {
|
||||
width: 18.75rem;
|
||||
background-color: var(--color-bg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-navigation-header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
// Original menu navigation has 50px - 1px bottom border
|
||||
// To ensure that there are no jumps between closed/open states
|
||||
// we set height here to repeat menu and icon buttons positions.
|
||||
min-height: 49px;
|
||||
background-color: var(--color-bg-1);
|
||||
|
||||
.close-button {
|
||||
border: none;
|
||||
padding: 0.35rem 0.35rem;
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
|
||||
--icon-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.top-navigation {
|
||||
overflow-y: auto;
|
||||
max-width: 100vw;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 18.75rem;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--color-bg-1);
|
||||
|
||||
:global(button) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(a) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-navigation {
|
||||
:global(a) {
|
||||
padding-left: 3.7rem;
|
||||
}
|
||||
:global(a[aria-current='page']) {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +422,7 @@
|
||||
.web-next-badge {
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.web-next-content {
|
||||
@ -185,13 +436,17 @@
|
||||
|
||||
// Custom menu with sidebar navigation controls styles
|
||||
.menu-button {
|
||||
display: flex;
|
||||
display: none;
|
||||
padding: 0.35rem;
|
||||
align-items: center;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.sidebarMode & {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
|
||||
@ -1,149 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { NavigationEntry, NavigationMenu } from './mainNavigation'
|
||||
|
||||
function isNavigationMenu(entry: NavigationEntry | NavigationMenu): entry is NavigationMenu {
|
||||
return entry && 'children' in entry
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { onClickOutside, portal } from '$lib/dom'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import SourcegraphLogo from '$lib/SourcegraphLogo.svelte'
|
||||
|
||||
import { isCurrent } from './mainNavigation'
|
||||
import MainNavigationLink from './MainNavigationLink.svelte'
|
||||
|
||||
export let onClose: () => void
|
||||
export let entries: (NavigationEntry | NavigationMenu)[]
|
||||
</script>
|
||||
|
||||
<div class="root" use:portal>
|
||||
<div class="content" use:onClickOutside on:click-outside={onClose}>
|
||||
<header>
|
||||
<button class="close-button" on:click={onClose}>
|
||||
<Icon icon={ILucideX} aria-label="Close sidebar navigation" />
|
||||
</button>
|
||||
|
||||
<a href="/search" class="logo-link">
|
||||
<SourcegraphLogo width="9.1rem" />
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<ul class="list">
|
||||
{#each entries as entry (entry.label)}
|
||||
<li>
|
||||
<MainNavigationLink {entry} />
|
||||
{#if isNavigationMenu(entry) && entry.children.length > 0}
|
||||
<ul>
|
||||
{#each entry.children as subEntry (subEntry.label)}
|
||||
<li aria-current={isCurrent(subEntry, $page) ? 'page' : 'false'}>
|
||||
<MainNavigationLink entry={subEntry} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
// Fixed overlay color TODO: find a better design token for it
|
||||
background-color: rgba(172, 182, 192, 0.2);
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 18.75rem;
|
||||
height: 100%;
|
||||
transform: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
border: none;
|
||||
padding: 0.35rem 0.35rem;
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
|
||||
--icon-size: 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
// Original menu navigation has 50px - 1px bottom border
|
||||
// To ensure that there are no jumps between closed/open states
|
||||
// we set height here to repeat menu and icon buttons positions.
|
||||
min-height: 49px;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
flex-grow: 1;
|
||||
|
||||
li[aria-current='page'] {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
|
||||
:global(a) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-2);
|
||||
}
|
||||
}
|
||||
|
||||
& ul {
|
||||
:global(a) {
|
||||
padding-left: 3.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,117 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { createDropdownMenu } from '@melt-ui/svelte'
|
||||
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
|
||||
import MainNavigationLink from './MainNavigationLink.svelte'
|
||||
import { isCurrent, type NavigationEntry, type NavigationMenu } from './mainNavigation'
|
||||
|
||||
export let entry: NavigationEntry | NavigationMenu
|
||||
|
||||
function isNavigationEntry(entry: NavigationEntry | NavigationMenu): entry is NavigationEntry {
|
||||
return entry && 'href' in entry
|
||||
}
|
||||
|
||||
const {
|
||||
elements: { menu, item, trigger },
|
||||
states: { open },
|
||||
} = createDropdownMenu({
|
||||
positioning: {
|
||||
placement: 'bottom-start',
|
||||
gutter: 0,
|
||||
},
|
||||
})
|
||||
$: current = isNavigationEntry(entry) ? isCurrent(entry, $page) : entry.isCurrent($page)
|
||||
</script>
|
||||
|
||||
<li class="toplevel-naventry" aria-current={current ? 'page' : 'false'}>
|
||||
{#if isNavigationEntry(entry)}
|
||||
<MainNavigationLink {entry} />
|
||||
{:else}
|
||||
<button {...$trigger} use:trigger>
|
||||
{#if typeof entry.icon === 'string'}
|
||||
<Icon icon={entry.icon} aria-hidden="true" inline />
|
||||
{:else if entry.icon}
|
||||
<span class="icon"><svelte:component this={entry.icon} /></span>
|
||||
{/if}
|
||||
{entry.label}
|
||||
<Icon icon={$open ? ILucideChevronUp : ILucideChevronDown} inline aria-hidden />
|
||||
</button>
|
||||
<ul {...$menu} use:menu>
|
||||
{#each entry.children as subEntry (subEntry.label)}
|
||||
<li {...$item} use:item>
|
||||
<MainNavigationLink entry={subEntry} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
li.toplevel-naventry {
|
||||
--icon-color: var(--header-icon-color);
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
white-space: nowrap;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-color-2);
|
||||
}
|
||||
|
||||
&[aria-current='page'] {
|
||||
border-color: var(--brand-secondary);
|
||||
}
|
||||
|
||||
> button,
|
||||
:global(a) {
|
||||
border-bottom: 2px solid;
|
||||
border-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
// Since this button is part navigation links blocks
|
||||
// we should override focus ring with inset to avoid
|
||||
// visual cropping with parent border.
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--primary-2) inset;
|
||||
}
|
||||
}
|
||||
|
||||
[role='menu'] {
|
||||
font-size: 0.875rem;
|
||||
min-width: 10rem;
|
||||
background-clip: padding-box;
|
||||
background-color: var(--dropdown-bg);
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
border-radius: var(--popover-border-radius);
|
||||
color: var(--body-color);
|
||||
box-shadow: var(--dropdown-shadow);
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
[role^='menuitem'] {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--dropdown-item-padding);
|
||||
white-space: nowrap;
|
||||
color: var(--dropdown-link-hover-color);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--dropdown-link-hover-bg);
|
||||
color: var(--dropdown-link-hover-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,13 +1,18 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { Badge } from '$lib/wildcard'
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements'
|
||||
|
||||
import { type NavigationEntry, Status } from './mainNavigation'
|
||||
|
||||
type $$Props = {
|
||||
entry: NavigationEntry
|
||||
} & HTMLAnchorAttributes
|
||||
|
||||
export let entry: NavigationEntry
|
||||
</script>
|
||||
|
||||
<a href={entry.href}>
|
||||
<a href={entry.href} {...$$restProps}>
|
||||
{#if entry.icon}
|
||||
<Icon icon={entry.icon} aria-hidden="true" inline />
|
||||
{/if}
|
||||
@ -20,8 +25,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
a {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--body-color);
|
||||
@ -31,13 +35,4 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
--icon-color: var(--header-icon-color);
|
||||
width: var(--icon-inline-size);
|
||||
height: var(--icon-inline-size);
|
||||
color: var(--header-icon-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -36,7 +36,7 @@ export interface NavigationEntry {
|
||||
* A navigation menu is a collection of navigation entries.
|
||||
* Currently, it will be rendered as a dropdown in the navigation bar.
|
||||
*/
|
||||
export interface NavigationMenu {
|
||||
export interface NavigationMenu<T extends NavigationEntry = NavigationEntry> {
|
||||
/**
|
||||
* The label of the navigation menu.
|
||||
*/
|
||||
@ -44,11 +44,10 @@ export interface NavigationMenu {
|
||||
/**
|
||||
* The navigation entries that are part of the menu.
|
||||
*/
|
||||
children: NavigationEntry[]
|
||||
children: T[]
|
||||
|
||||
/**
|
||||
* NavigationMenu item can be rendered as plain link in side navigation mode
|
||||
* This fallbackURL will be used to set URL to this link
|
||||
* Target URL to navigate to when the menu is clicked.
|
||||
*/
|
||||
href: string
|
||||
|
||||
@ -56,11 +55,6 @@ export interface NavigationMenu {
|
||||
* An optional icon to display next to the label.
|
||||
*/
|
||||
icon?: IconComponent
|
||||
/**
|
||||
* A function to determine if current page is part of the menu.
|
||||
* This is used to mark the menu as "current" in the UI.
|
||||
*/
|
||||
isCurrent(page: Page): boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,3 +64,7 @@ export interface NavigationMenu {
|
||||
export function isCurrent(entry: NavigationEntry, page: Page): boolean {
|
||||
return page.url.pathname === entry.href
|
||||
}
|
||||
|
||||
export function isNavigationMenu(entry: NavigationEntry | NavigationMenu): entry is NavigationMenu {
|
||||
return entry && 'children' in entry && entry.children.length > 0
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import { readable, writable, type Readable, type Writable } from 'svelte/store'
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import type { Settings, TemporarySettingsStorage } from '$lib/shared'
|
||||
|
||||
import type { AuthenticatedUser, FeatureFlag } from '../routes/layout.gql'
|
||||
@ -81,3 +82,17 @@ export function createLocalWritable<T>(localStorageKey: string, defaultValue: T)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Media query store that updates when the media query matches.
|
||||
*/
|
||||
export function mediaQuery(query: string): Readable<boolean> {
|
||||
const mediaQuery = window.matchMedia(query)
|
||||
return readable(mediaQuery.matches, set => {
|
||||
const listener = () => set(mediaQuery.matches)
|
||||
mediaQuery.addEventListener('change', listener)
|
||||
return () => mediaQuery.removeEventListener('change', listener)
|
||||
})
|
||||
}
|
||||
|
||||
export const isViewportMediumDown = browser ? mediaQuery('(max-width: 768px)') : readable(false)
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
EditSettings,
|
||||
LatestSettingsQuery,
|
||||
} from './layout.gql'
|
||||
import { dotcomMainNavigation, mainNavigation } from './navigation'
|
||||
import { getMainNavigationEntries, Mode } from './navigation'
|
||||
|
||||
// Disable server side rendering for the whole app
|
||||
export const ssr = false
|
||||
@ -47,11 +47,20 @@ export const load: LayoutLoad = async ({ fetch }) => {
|
||||
}
|
||||
|
||||
return {
|
||||
navigationEntries: getMainNavigationEntries(
|
||||
(window.context.sourcegraphDotComMode ? Mode.DOTCOM : Mode.ENTERPRISE) |
|
||||
(window.context.codyEnabledOnInstance ? Mode.CODY_INSTANCE_ENABLED : 0) |
|
||||
(window.context.codyEnabledForCurrentUser ? Mode.CODY_USER_ENABLED : 0) |
|
||||
(window.context.batchChangesEnabled ? Mode.BATCH_CHANGES_ENABLED : 0) |
|
||||
(window.context.codeInsightsEnabled ? Mode.CODE_INSIGHTS_ENABLED : 0) |
|
||||
(result.data.currentUser ? Mode.AUTHENTICATED : Mode.UNAUTHENTICATED)
|
||||
),
|
||||
|
||||
// User data
|
||||
user: result.data.currentUser,
|
||||
navigationEntries: window.context.sourcegraphDotComMode ? dotcomMainNavigation : mainNavigation,
|
||||
// Initial user settings
|
||||
settings,
|
||||
featureFlags: result.data.evaluatedFeatureFlags,
|
||||
|
||||
globalSiteAlerts: globalSiteAlerts.then(result => result.data?.site),
|
||||
fetchEvaluatedFeatureFlags: async () => {
|
||||
const result = await client.query(EvaluatedFeatureFlagsQuery, {}, { requestPolicy: 'network-only', fetch })
|
||||
|
||||
@ -30,6 +30,82 @@ test('has user menu', async ({ sg, page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Signed in as @test' })).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('cody top level navigation', () => {
|
||||
const topNavName = 'Cody'
|
||||
|
||||
;[
|
||||
{
|
||||
name: 'sourcegraph.com, signed out',
|
||||
context: { sourcegraphDotComMode: true },
|
||||
signedIn: false,
|
||||
expectedTopNav: '/cody',
|
||||
expectedSubNav: false,
|
||||
},
|
||||
{
|
||||
name: 'sourcegraph.com, signed in',
|
||||
context: { sourcegraphDotComMode: true },
|
||||
signedIn: true,
|
||||
expectedTopNav: '/cody/chat',
|
||||
expectedSubNav: {
|
||||
'Web Chat': '/cody/chat',
|
||||
Dashboard: '/cody/manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'enterprise, no user cody',
|
||||
context: { sourcegraphDotComMode: false, codyEnabledOnInstance: true, codyEnabledForCurrentUser: false },
|
||||
signedIn: true,
|
||||
expectedTopNav: '/cody/dashboard',
|
||||
expectedSubNav: false,
|
||||
},
|
||||
{
|
||||
name: 'enterprise, user cody',
|
||||
context: { sourcegraphDotComMode: false, codyEnabledOnInstance: true, codyEnabledForCurrentUser: true },
|
||||
signedIn: true,
|
||||
expectedTopNav: '/cody/chat',
|
||||
expectedSubNav: {
|
||||
'Web Chat': '/cody/chat',
|
||||
Dashboard: '/cody/dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'enterprise, no cody',
|
||||
context: { sourcegraphDotComMode: false, codyEnabledOnInstance: false, codyEnabledForCurrentUser: false },
|
||||
signedIn: true,
|
||||
expectedTopNav: false,
|
||||
},
|
||||
].forEach(({ name, context, signedIn, expectedTopNav, expectedSubNav }) => {
|
||||
test(name, async ({ sg, page }) => {
|
||||
const mainNav = page.getByLabel('Main')
|
||||
const topNavCodyEntry = mainNav.getByRole('link', { name: topNavName })
|
||||
const menuToggleButton = mainNav.getByLabel("Open 'Cody' submenu")
|
||||
await sg.setWindowContext(context)
|
||||
|
||||
if (signedIn) {
|
||||
sg.signIn({ username: 'test' })
|
||||
}
|
||||
|
||||
await page.goto('/')
|
||||
await expect(mainNav).toBeVisible()
|
||||
|
||||
if (typeof expectedTopNav === 'string') {
|
||||
await expect(topNavCodyEntry).toHaveAttribute('href', expectedTopNav)
|
||||
} else {
|
||||
await expect(topNavCodyEntry).not.toBeAttached()
|
||||
}
|
||||
|
||||
if (typeof expectedSubNav === 'object') {
|
||||
await expect(menuToggleButton).toBeVisible()
|
||||
for (const [name, href] of Object.entries(expectedSubNav)) {
|
||||
await expect(page.getByRole('link', { name, includeHidden: true })).toHaveAttribute('href', href)
|
||||
}
|
||||
} else if (expectedTopNav) {
|
||||
await expect(menuToggleButton).not.toBeAttached()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('has global notifications', async ({ sg, page }) => {
|
||||
sg.mockTypes({
|
||||
Query: () => ({
|
||||
|
||||
@ -1,10 +1,68 @@
|
||||
import { isRepoRoute } from '$lib/navigation'
|
||||
import { Status, isCurrent, type NavigationEntry, type NavigationMenu } from '$lib/navigation/mainNavigation'
|
||||
import { Status, type NavigationEntry, type NavigationMenu } from '$lib/navigation/mainNavigation'
|
||||
|
||||
/**
|
||||
* The main navigation of the application.
|
||||
*/
|
||||
export const mainNavigation: (NavigationMenu | NavigationEntry)[] = [
|
||||
export const enum Mode {
|
||||
ENTERPRISE = 1 << 0,
|
||||
DOTCOM = 1 << 1,
|
||||
CODY_INSTANCE_ENABLED = 1 << 2,
|
||||
CODY_USER_ENABLED = 1 << 3,
|
||||
BATCH_CHANGES_ENABLED = 1 << 4,
|
||||
CODE_INSIGHTS_ENABLED = 1 << 5,
|
||||
AUTHENTICATED = 1 << 6,
|
||||
UNAUTHENTICATED = 1 << 7,
|
||||
}
|
||||
|
||||
interface NavigationEntryDefinition extends Omit<NavigationEntry, 'href'> {
|
||||
href: string | [Mode, string][]
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
interface NavigationMenuDefinition extends Omit<NavigationMenu, 'children' | 'href'> {
|
||||
href: string | [Mode, string][]
|
||||
children: NavigationEntryDefinition[]
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
function matchesMode(entry: NavigationEntryDefinition, mode: Mode): boolean {
|
||||
return entry.mode === undefined || (entry.mode & mode) === entry.mode
|
||||
}
|
||||
|
||||
function toEntry(entry: NavigationEntryDefinition, mode: Mode): NavigationEntry {
|
||||
return {
|
||||
...entry,
|
||||
href: matchHref(entry.href, mode),
|
||||
}
|
||||
}
|
||||
|
||||
function matchHref(href: NavigationEntryDefinition['href'], mode: Mode): string {
|
||||
if (typeof href === 'string') {
|
||||
return href
|
||||
}
|
||||
|
||||
for (const [key, value] of href) {
|
||||
if ((mode & +key) === +key) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getMainNavigationEntries(mode: Mode): (NavigationMenu | NavigationEntry)[] {
|
||||
return navigationEntries
|
||||
.filter(entry => matchesMode(entry, mode))
|
||||
.map(definition => {
|
||||
const entry = toEntry(definition, mode)
|
||||
return 'children' in definition
|
||||
? {
|
||||
...entry,
|
||||
children: definition.children
|
||||
.filter(child => matchesMode(child, mode))
|
||||
.map(child => toEntry(child, mode)),
|
||||
}
|
||||
: entry
|
||||
})
|
||||
}
|
||||
|
||||
const navigationEntries: (NavigationMenuDefinition | NavigationEntryDefinition)[] = [
|
||||
{
|
||||
label: 'Code Search',
|
||||
icon: ILucideSearch,
|
||||
@ -36,71 +94,72 @@ export const mainNavigation: (NavigationMenu | NavigationEntry)[] = [
|
||||
status: Status.BETA,
|
||||
},
|
||||
],
|
||||
isCurrent(this: NavigationMenu, page) {
|
||||
// This is a special case of the code search menu: It is marked as "current" if the
|
||||
// current page is a repository route.
|
||||
return isRepoRoute(page.route?.id) || this.children.some(entry => isCurrent(entry, page))
|
||||
},
|
||||
mode: Mode.ENTERPRISE,
|
||||
},
|
||||
{
|
||||
label: 'Code Search',
|
||||
icon: ILucideSearch,
|
||||
href: '/search',
|
||||
mode: Mode.DOTCOM,
|
||||
},
|
||||
{
|
||||
label: 'Cody',
|
||||
icon: ISgCody,
|
||||
href: '/cody',
|
||||
isCurrent(this: NavigationMenu, page) {
|
||||
return this.children.some(entry => isCurrent(entry, page))
|
||||
},
|
||||
mode: Mode.DOTCOM | Mode.UNAUTHENTICATED,
|
||||
},
|
||||
{
|
||||
label: 'Cody',
|
||||
icon: ISgCody,
|
||||
href: '/cody/chat',
|
||||
children: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/cody',
|
||||
},
|
||||
{
|
||||
label: 'Web Chat',
|
||||
href: '/cody/chat',
|
||||
},
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/cody/manage',
|
||||
},
|
||||
],
|
||||
mode: Mode.DOTCOM | Mode.AUTHENTICATED,
|
||||
},
|
||||
{
|
||||
label: 'Cody',
|
||||
icon: ISgCody,
|
||||
href: [
|
||||
[Mode.CODY_USER_ENABLED, '/cody/chat'],
|
||||
[Mode.CODY_INSTANCE_ENABLED, '/cody/dashboard'],
|
||||
],
|
||||
children: [
|
||||
{
|
||||
label: 'Web Chat',
|
||||
href: '/cody/chat',
|
||||
mode: Mode.CODY_USER_ENABLED,
|
||||
},
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/cody/dashboard',
|
||||
mode: Mode.CODY_USER_ENABLED,
|
||||
},
|
||||
],
|
||||
mode: Mode.ENTERPRISE | Mode.CODY_INSTANCE_ENABLED,
|
||||
},
|
||||
{
|
||||
label: 'Batch Changes',
|
||||
icon: ISgBatchChanges,
|
||||
href: '/batch-changes',
|
||||
mode: Mode.BATCH_CHANGES_ENABLED,
|
||||
},
|
||||
{
|
||||
label: 'Insights',
|
||||
icon: ILucideBarChartBig,
|
||||
href: '/insights',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* The main navigation for sourcegraph.com
|
||||
*/
|
||||
export const dotcomMainNavigation: (NavigationMenu | NavigationEntry)[] = [
|
||||
{
|
||||
label: 'Code Search',
|
||||
icon: ILucideSearch,
|
||||
href: '/search',
|
||||
},
|
||||
{
|
||||
label: 'Cody',
|
||||
icon: ISgCody,
|
||||
href: '/cody',
|
||||
children: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/cody',
|
||||
},
|
||||
{
|
||||
label: 'Web Chat',
|
||||
href: '/cody/chat',
|
||||
},
|
||||
],
|
||||
isCurrent(this: NavigationMenu, page) {
|
||||
return this.children.some(entry => isCurrent(entry, page))
|
||||
},
|
||||
mode: Mode.CODE_INSIGHTS_ENABLED,
|
||||
},
|
||||
{
|
||||
label: 'About Sourcegraph',
|
||||
href: '/',
|
||||
mode: Mode.DOTCOM,
|
||||
},
|
||||
]
|
||||
|
||||
@ -41,5 +41,7 @@
|
||||
selectedFilters={data.queryFilters}
|
||||
/>
|
||||
{:else}
|
||||
<SearchHome {queryState} codyHref={data.codyHref} showDotcomStuff={data.showDotcomFooterLinks} />
|
||||
<SearchHome {queryState}>
|
||||
<svelte:component this={data.footer} />
|
||||
</SearchHome>
|
||||
{/if}
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from '$lib/shared'
|
||||
|
||||
import type { PageLoad } from './$types'
|
||||
import DotcomFooterLinks from './DotcomFooterLinks.svelte'
|
||||
|
||||
type SearchStreamCacheEntry = Observable<AggregateStreamingSearchResults>
|
||||
|
||||
@ -142,7 +143,7 @@ export const load: PageLoad = async ({ parent, url, depends }) => {
|
||||
|
||||
return {
|
||||
codyHref,
|
||||
showDotcomFooterLinks: window.context.sourcegraphDotComMode,
|
||||
footer: window.context.sourcegraphDotComMode ? DotcomFooterLinks : null,
|
||||
searchStream,
|
||||
queryFilters,
|
||||
queryFromURL: query,
|
||||
@ -157,7 +158,7 @@ export const load: PageLoad = async ({ parent, url, depends }) => {
|
||||
}
|
||||
return {
|
||||
codyHref,
|
||||
showDotcomFooterLinks: window.context.sourcegraphDotComMode,
|
||||
footer: window.context.sourcegraphDotComMode ? DotcomFooterLinks : null,
|
||||
queryOptions: {
|
||||
query: '',
|
||||
},
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { TELEMETRY_RECORDER } from '$lib/telemetry'
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'Docs',
|
||||
@ -31,7 +29,9 @@
|
||||
]
|
||||
|
||||
function handleLinkClick(telemetryType: number): void {
|
||||
TELEMETRY_RECORDER.recordEvent('home.footer.CTA', 'click', { metadata: { type: telemetryType } })
|
||||
import('$lib/telemetry').then(({ TELEMETRY_RECORDER }) => {
|
||||
TELEMETRY_RECORDER.recordEvent('home.footer.CTA', 'click', { metadata: { type: telemetryType } })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -9,12 +9,9 @@
|
||||
import { isLightTheme } from '$lib/stores'
|
||||
import { TELEMETRY_RECORDER } from '$lib/telemetry'
|
||||
|
||||
import DotcomFooterLinks from './DotcomFooterLinks.svelte'
|
||||
import SearchHomeNotifications from './SearchHomeNotifications.svelte'
|
||||
|
||||
export let queryState: QueryStateStore
|
||||
export let codyHref: string = '/cody'
|
||||
export let showDotcomStuff: boolean = false
|
||||
|
||||
setContext<SearchPageContext>('search-context', {
|
||||
setQuery(newQuery) {
|
||||
@ -40,9 +37,7 @@
|
||||
<SearchInput {queryState} autoFocus onSubmit={handleSubmit} />
|
||||
<SearchHomeNotifications />
|
||||
</div>
|
||||
{#if showDotcomStuff}
|
||||
<DotcomFooterLinks />
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -231,6 +231,16 @@ class Sourcegraph {
|
||||
this.graphqlMock.addFixtures(fixtures)
|
||||
}
|
||||
|
||||
public setWindowContext(context: Partial<Window['context']>): Promise<void> {
|
||||
return this.page.addInitScript(context => {
|
||||
if (!window.context) {
|
||||
// @ts-expect-error - Unclear how to type this correctly
|
||||
window.context = {}
|
||||
}
|
||||
Object.assign(window.context, context)
|
||||
}, context)
|
||||
}
|
||||
|
||||
public signIn(userMock: UserMock = {}): void {
|
||||
this.mockTypes({
|
||||
Query: () => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user