Svelte: add welcome introduction when enabling svelte for the first time (#64163)

This implements a welcome dialog and some additional messaging around
the beta rollout.
This commit is contained in:
Camden Cheek 2024-08-01 03:06:37 -06:00 committed by GitHub
parent 78064ba956
commit be17da7305
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 3837 additions and 83 deletions

View File

@ -96,6 +96,9 @@ export interface TemporarySettingsSchema {
/** OpenCodeGraph */
'openCodeGraph.annotations.visible': boolean
'webNext.welcomeOverlay.dismissed': boolean
'webNext.departureMessage.dismissed': boolean
}
/**
@ -161,6 +164,8 @@ const TEMPORARY_SETTINGS: Record<keyof TemporarySettings, null> = {
'simple.search.toggle': null,
'cody.onboarding.completed': null,
'openCodeGraph.annotations.visible': null,
'webNext.welcomeOverlay.dismissed': null,
'webNext.departureMessage.dismissed': null,
}
export const TEMPORARY_SETTINGS_KEYS = Object.keys(TEMPORARY_SETTINGS) as readonly (keyof TemporarySettings)[]

View File

@ -42,6 +42,7 @@ declare global {
const ILucideEye: typeof import('~icons/lucide/eye')['default']
const ILucideFile: typeof import('~icons/lucide/file')['default']
const ILucideFileCode: typeof import('~icons/lucide/file-code')['default']
const ILucideFileDiff: typeof import('~icons/lucide/file-diff')['default']
const ILucideFileJson: typeof import('~icons/lucide/file-json')['default']
const ILucideFileSearch2: typeof import('~icons/lucide/file-search2')['default']
const ILucideFileStack: typeof import('~icons/lucide/file-stack')['default']
@ -61,12 +62,14 @@ declare global {
const ILucideGitCompareArrows: typeof import('~icons/lucide/git-compare-arrows')['default']
const ILucideGitFork: typeof import('~icons/lucide/git-fork')['default']
const ILucideGitMerge: typeof import('~icons/lucide/git-merge')['default']
const ILucideHelp: typeof import('~icons/lucide/help')['default']
const ILucideHistory: typeof import('~icons/lucide/history')['default']
const ILucideHome: typeof import('~icons/lucide/home')['default']
const ILucideInfo: typeof import('~icons/lucide/info')['default']
const ILucideLink: typeof import('~icons/lucide/link')['default']
const ILucideLock: typeof import('~icons/lucide/lock')['default']
const ILucideMenu: typeof import('~icons/lucide/menu')['default']
const ILucideNetwork: typeof import('~icons/lucide/network')['default']
const ILucideOctagonX: typeof import('~icons/lucide/octagon-x')['default']
const ILucidePanelBottomClose: typeof import('~icons/lucide/panel-bottom-close')['default']
const ILucidePanelLeftClose: typeof import('~icons/lucide/panel-left-close')['default']
@ -74,6 +77,7 @@ declare global {
const ILucidePencil: typeof import('~icons/lucide/pencil')['default']
const ILucideRegex: typeof import('~icons/lucide/regex')['default']
const ILucideRepeat: typeof import('~icons/lucide/repeat')['default']
const ILucideScanSearch: typeof import('~icons/lucide/scan-search')['default']
const ILucideSearch: typeof import('~icons/lucide/search')['default']
const ILucideSearchX: typeof import('~icons/lucide/search-x')['default']
const ILucideSettings: typeof import('~icons/lucide/settings')['default']

View File

@ -0,0 +1,13 @@
<script lang="ts" context="module">
import { Story } from '@storybook/addon-svelte-csf'
import FeedbackDialog from './FeedbackDialog.svelte'
export const meta = {
component: FeedbackDialog,
}
</script>
<Story name="Default">
<FeedbackDialog handleOptOut={() => {}} />
</Story>

View File

@ -0,0 +1,67 @@
<script lang="ts">
import { getButtonClassName } from '$lib/wildcard/Button'
import Toggle from '$lib/wildcard/Toggle.svelte'
export let handleOptOut: (() => void) | undefined
</script>
<div class="root">
<div class="section header">
<h4 class="m0">New, faster UX (Beta)</h4>
{#if handleOptOut}
<Toggle on={true} on:click={() => handleOptOut()} />
{/if}
</div>
<div class="section">
<p class="m0">
You're currently on the new, faster Code Search user experience. It's in beta, and is our effort to rebuild
the tool from the ground up for performance.
</p>
</div>
<div class="section">
<p>Got feedback for us on the beta? We'd love to hear from you.</p>
<a
class={getButtonClassName({ variant: 'secondary' })}
href="https://community.sourcegraph.com/c/code-search/9"
target="_blank"
rel="noreferrer noopener"
>
Leave feedback
</a>
<p class="small">It only takes two minutes and helps a ton!</p>
</div>
</div>
<style lang="scss">
.root {
width: 360px;
.section {
padding: 1rem;
& + .section {
border-top: 1px solid var(--border-color);
}
}
.m0 {
margin: 0;
}
.header {
display: flex;
justify-content: space-between;
}
a {
width: 100%;
}
.small {
color: var(--text-muted);
font-size: var(--font-size-extra-small);
text-align: center;
margin-top: 0.25rem;
margin-bottom: 0;
}
}
</style>

View File

@ -21,9 +21,10 @@
import SourcegraphLogo from '$lib/SourcegraphLogo.svelte'
import { isViewportMediumDown } from '$lib/stores'
import { Button } from '$lib/wildcard'
import Badge from '$lib/wildcard/Badge.svelte'
import Toggle from '$lib/wildcard/Toggle.svelte'
import { getButtonClassName } from '$lib/wildcard/Button'
import ProductStatusBadge from '$lib/wildcard/ProductStatusBadge.svelte'
import FeedbackDialog from './FeedbackDialog.svelte'
import { GlobalNavigation_User } from './GlobalNavigation.gql'
import { type NavigationEntry, type NavigationMenu, isNavigationMenu, isCurrent } from './mainNavigation'
import UserMenu from './UserMenu.svelte'
@ -123,28 +124,24 @@
<div class="global-portal" bind:this={$extensionElement} />
<div class="web-next-notice">
{#if handleOptOut}
<Toggle on={true} on:click={() => handleOptOut && handleOptOut()} />
{/if}
<Popover let:toggle let:registerTrigger>
<button class="web-next-badge" use:registerTrigger on:click={() => toggle()}>
<Badge variant="warning">Experimental</Badge>
<ProductStatusBadge status="beta" />
<a
class={getButtonClassName({ variant: 'secondary', size: 'sm' })}
href="https://community.sourcegraph.com/c/code-search/9"
target="_blank"
rel="noreferrer noopener"
>
Feedback
</a>
<Popover let:registerTrigger let:toggle placement="bottom-end">
<button
use:registerTrigger
class={getButtonClassName({ variant: 'secondary', size: 'sm' })}
on:click={() => toggle()}
>
<Icon icon={ILucideEllipsis} inline />
</button>
<div slot="content" class="web-next-content">
<h3>Experimental web app</h3>
<p>
You are using an experimental version of the Sourcegraph web app. This version is under active
development and may contain bugs or incomplete features.
</p>
<p>
If you encounter any issues, please report them in our <a href="https://community.sourcegraph.com/"
>community forums</a
>.
</p>
{#if handleOptOut}
<p>You can opt out of the new experience with the toggle above.</p>
{/if}
</div>
<FeedbackDialog slot="content" {handleOptOut} />
</Popover>
</div>
<div>
@ -413,35 +410,13 @@
}
}
// Opt out experiment badge and tooltip styles
.opt-out {
all: unset;
cursor: pointer;
color: var(--link-color);
text-decoration: underline;
}
.web-next-notice {
display: flex;
align-items: center;
gap: 0.5rem;
}
.web-next-badge {
all: unset;
display: flex;
align-items: center;
cursor: pointer;
margin-left: auto;
}
.web-next-content {
padding: 1rem;
width: 20rem;
p:last-child {
margin-bottom: 0;
}
font-size: var(--font-size-small);
font-weight: 500;
margin-right: 1rem;
}
// Custom menu with sidebar navigation controls styles

View File

@ -2,6 +2,7 @@
// In addition to the props explicitly listed here, this component also
// accepts any HTMLButton attributes. Note that those will only be used when
// the default implementation is used.
import type { HTMLButtonAttributes } from 'svelte/elements'
import { type BUTTON_DISPLAY, type BUTTON_SIZES, type BUTTON_VARIANTS, getButtonClassName } from './Button'

View File

@ -22,6 +22,7 @@
import { isRouteEnabled } from '$lib/navigation'
import type { LayoutData } from './$types'
import WelcomeOverlay from './WelcomeOverlay.svelte'
export let data: LayoutData
@ -81,12 +82,17 @@
$: currentUserID = data.user?.id
$: handleOptOut = currentUserID
? async (): Promise<void> => {
if (currentUserID) {
await data.disableSvelteFeatureFlags(currentUserID)
window.location.reload()
}
// Show departure message after switching off
$temporarySettingsStorage.set('webNext.departureMessage.dismissed', false)
await data.disableSvelteFeatureFlags(currentUserID)
window.location.reload()
}
: undefined
$: welcomeOverlayDismissed = $temporarySettingsStorage.get('webNext.welcomeOverlay.dismissed', false)
function handleDismissWelcomeOverlay() {
$temporarySettingsStorage.set('webNext.welcomeOverlay.dismissed', true)
}
</script>
<svelte:head>
@ -105,6 +111,11 @@
<slot />
</main>
<WelcomeOverlay
show={(process.env.PW_TEST === 'true' && !$welcomeOverlayDismissed) ?? false}
handleDismiss={handleDismissWelcomeOverlay}
/>
<FuzzyFinderContainer />
<style lang="scss">

View File

@ -0,0 +1,211 @@
<script lang="ts">
import { allHotkey } from '$lib/fuzzyfinder/keys'
import Icon from '$lib/Icon.svelte'
import KeyboardShortcut from '$lib/KeyboardShortcut.svelte'
import { isLightTheme } from '$lib/theme'
import Button from '$lib/wildcard/Button.svelte'
import ProductStatusBadge from '$lib/wildcard/ProductStatusBadge.svelte'
import WelcomeOverlayScreenshotDark from './WelcomeOverlayScreenshotDark.svelte'
import WelcomeOverlayScreenshotLight from './WelcomeOverlayScreenshotLight.svelte'
export let show: boolean
export let handleDismiss: () => void
let root: HTMLDialogElement
$: if (show) {
root?.showModal()
} else {
root?.close()
}
</script>
<dialog bind:this={root}>
<div class="content">
<div class="logo"><Icon icon={ISgMark} /><ProductStatusBadge status="beta" /></div>
<div class="message">
<h1><span>You've activated a better, faster experience</span></h1>
<p class="subtitle">
Get ready for a new Code Search experience: rewritten from the ground-up for performance to empower your
workflow.
</p>
</div>
<div class="features">
<div>
<Icon icon={ILucideFileDiff} />
<h5>New in-line diff view</h5>
<p>Easily compare commits and see how a file changed over time, all in-line</p>
</div>
<div>
<Icon icon={ILucideNetwork} />
<h5>Revamped code navigation</h5>
<p>Quickly find a list of references of a given symbol, or immediately jump to the definition</p>
</div>
<div>
<Icon icon={ILucideScanSearch} />
<h5>Reworked fuzzy finder <KeyboardShortcut shortcut={allHotkey} /></h5>
<p>Find files and symbols quickly and easily with our whole new fuzzy finder.</p>
</div>
</div>
<div class="cta">
<div>
<Button variant="secondary" on:click={() => handleDismiss()}>Awesome. Im ready to use it!</Button>
<!-- <a href="TODO">Read release notes</a> -->
</div>
<p> You can opt out at any time by using the toggle at the top of the screen. </p>
<p>
Whilst exploring the new experience, consider leaving us some feedback via the button at the top. We'd
love to hear from you!
</p>
</div>
</div>
{#if $isLightTheme}
<WelcomeOverlayScreenshotLight />
{:else}
<WelcomeOverlayScreenshotDark />
{/if}
</dialog>
<style lang="scss">
dialog {
width: 80vw;
border-radius: 0.75rem;
border: 1px solid var(--border-color);
padding: 2rem;
overflow: hidden;
background-color: var(--color-bg-1);
box-shadow: var(--fuzzy-finder-shadow);
&::backdrop {
opacity: 0.48;
:global(.theme-light) & {
background: var(--color-background, #f9fafb);
}
:global(.theme-dark) & {
background: var(--color-background, #f9fafb);
}
}
container-type: inline-size;
@media (--mobile) {
border-radius: 0;
border: none;
position: fixed;
width: 100vw;
height: 100vh;
max-height: 100vh;
max-width: 100vw;
}
> :global(svg) {
position: absolute;
right: 0;
bottom: 0;
filter: drop-shadow(0px 25px 50px rgba(15, 17, 26, 0.25));
@container (width < 975px) {
display: none;
}
}
}
.content {
// TODO: import this from shadcn color library (once it exists)
:global(.theme-light) & {
--color-text-subtle: var(--text-body);
}
:global(.theme-dark) & {
--color-text-subtle: #a6b6d9;
}
width: calc(100% - 350px);
@container (width < 975px) {
width: 100%;
}
display: flex;
gap: 1rem;
flex-direction: column;
.logo {
--icon-color: initial;
--icon-size: 32px;
display: flex;
gap: 1rem;
align-items: center;
}
.message {
h1 {
text-wrap: balance;
span {
background: linear-gradient(90deg, #00cbec 0%, #a112ff 48.53%, #ff5543 97.06%);
color: transparent;
background-clip: text;
}
}
}
.subtitle {
margin: 0;
font-size: var(--font-size-large);
font-weight: 500;
color: var(--color-text-subtle);
}
.features {
display: grid;
max-width: 700px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem 0.75rem;
padding: 1rem 0;
> div {
display: grid;
grid-template-columns: min-content auto;
gap: 0.25rem 0.75rem;
:global([data-icon]) {
--icon-size: 20px;
grid-column: 1;
grid-row: 1;
}
h5 {
all: unset;
font-weight: 600;
grid-column: 2;
grid-row: 1;
}
p {
all: unset;
font-size: var(--font-size-small);
font-weight: 400;
color: var(--color-text-subtle);
grid-column: 2;
grid-row: 2;
}
}
}
.cta {
display: flex;
gap: 1rem;
flex-direction: column;
div {
grid-column: 1 / -1;
display: flex;
gap: 1rem;
align-items: center;
}
p {
grid-column: 1 / -1;
color: var(--text-muted);
font-size: var(--font-size-small);
font-weight: 400;
margin: 0;
}
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -10,14 +10,6 @@ test('has sign in button', async ({ page }) => {
await expect(page).toHaveURL('/sign-in')
})
test('has experimental opt out popover', async ({ sg, page }) => {
sg.signIn({ username: 'test' })
await page.goto('/')
await page.getByText('Experimental').click()
await expect(page.getByText('opt out')).toBeVisible()
})
test('has user menu', async ({ sg, page }) => {
sg.signIn({ username: 'test' })
const userMenu = page.getByLabel('Open user menu')

View File

@ -2,6 +2,11 @@
display: flex;
gap: 0.5rem;
align-items: center;
font-size: 0.875rem;
p {
margin: 0;
}
}
.toggle {
@ -13,10 +18,39 @@
}
.popover {
padding: 1rem;
width: 20rem;
width: 24rem;
p:last-child {
margin-bottom: 0;
p {
margin: 0;
}
a {
width: 100%;
}
h3 {
display: flex;
align-items: center;
justify-content: space-between;
}
}
.section {
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
+ .section {
border-top: 1px solid var(--border-color);
}
}
.small {
text-align: center;
font-size: var(--font-size-small);
color: var(--text-muted);
}
.help-icon {
color: var(--icon-color);
}

View File

@ -1,10 +1,12 @@
import { FC } from 'react'
import { FC, useRef, useEffect, useCallback } from 'react'
import { useApolloClient } from '@apollo/client'
import { mdiHelpCircleOutline, mdiClose } from '@mdi/js'
import { useLocation } from 'react-router-dom'
import { Toggle } from '@sourcegraph/branded/src/components/Toggle'
import { Text, H3, Popover, PopoverTrigger, PopoverContent, Badge } from '@sourcegraph/wildcard'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import { Text, H3, Popover, PopoverTrigger, PopoverContent, Icon, Button } from '@sourcegraph/wildcard'
import { enableSvelteAndReload, canEnableSvelteKit } from './util'
@ -13,28 +15,91 @@ import styles from './SvelteKitNavItem.module.scss'
export const SvelteKitNavItem: FC<{ userID?: string }> = ({ userID }) => {
const location = useLocation()
const client = useApolloClient()
const [departureDismissed, setDepartureDismissed] = useTemporarySetting('webNext.departureMessage.dismissed', false)
const [_welcomeDismissed, setWelcomeDismissed] = useTemporarySetting('webNext.welcomeOverlay.dismissed', false)
const departureRef = useRef<HTMLDivElement | null>(null)
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (departureRef.current && !departureRef.current.contains(event.target as Node)) {
setDepartureDismissed(true)
}
},
[departureRef, setDepartureDismissed]
)
useEffect(() => {
document.addEventListener('click', handleClickOutside)
return () => {
document.removeEventListener('click', handleClickOutside)
}
}, [handleClickOutside])
if (!userID || !canEnableSvelteKit(location.pathname)) {
return null
}
const showDeparture = !departureDismissed
const popoverProps = showDeparture ? { isOpen: true, onOpenChange: () => {} } : {}
return (
<div className={styles.container}>
<Toggle
value={false}
onToggle={() => enableSvelteAndReload(client, userID)}
title="Go to experimental web app"
className={styles.toggle}
/>
<Popover>
<PopoverTrigger className={styles.badge}>
<Badge variant="warning">Try the new experience</Badge>
</PopoverTrigger>
<PopoverContent className={styles.popover}>
<H3>Sourcegraph is getting a refresh!</H3>
<Text>Try it out early with the toggle above.</Text>
</PopoverContent>
</Popover>
</div>
<Popover {...popoverProps}>
<PopoverTrigger className={styles.badge}>
<div className={styles.container}>
<Icon className={styles.helpIcon} svgPath={mdiHelpCircleOutline} aria-hidden={true} />
<Text>New, faster UX</Text>
<Toggle
value={false}
onToggle={() => {
setWelcomeDismissed(false) // Show welcome after switching on
enableSvelteAndReload(client, userID)
}}
title="Enable new, faster UX"
className={styles.toggle}
/>
</div>
</PopoverTrigger>
<PopoverContent className={styles.popover} position="bottomEnd">
{showDeparture ? (
<div ref={departureRef}>
<div className={styles.section}>
<H3>
<span>Switched out of the new experience?</span>
<Button variant="icon" onClick={() => setDepartureDismissed(true)}>
<Icon svgPath={mdiClose} inline={true} aria-label="close" />
</Button>
</H3>
<Text>
Remember, you can always switch back using the toggle above. We're still working on it,
so check back soon.
</Text>
</div>
<div className={styles.section}>
<Text>Got feedback for us on the beta? Wed love to hear from you.</Text>
<Button
as="a"
variant="secondary"
href="https://community.sourcegraph.com/c/code-search/9"
target="_blank"
rel="noreferrer noopener"
>
Leave feedback
</Button>
<Text className={styles.small}>It only takes two minutes and helps a ton!</Text>
</div>
</div>
) : (
<div className={styles.section}>
<H3>What's this "New, faster UX"?</H3>
<Text>
We've been busy at work on a new Code Search experience, built from the ground up for
performance, which is now available in beta.
</Text>
<Text>Simply activate the toggle to get it.</Text>
</div>
)}
</PopoverContent>
</Popover>
)
}