svelte: Fix svelte-check, lint and TS errors (#59325)

This PR addresses all issues reported by svelte-check related to the packages own files. svelte-check reports many more errors but they all related to files in other packages (e.g. client/web). I haven't figured out a way yet to have svelte-check/TypeScript ignore these.

What was done:
-  Remove React interop code (wasn't used)
- Move all stories to Svelte component format and co-located them with the tested component.
- Fixed TypeScript types
- Fixed eslint issues
- Migrated some GraphQL queries to .gql files (I think those had some errors/warnings associated with them; I'll migrate more in an upcoming commit)

What hasn't worked:

- Upgrading svelte-check. I mean, it works but then it reports issues with extending $$Props in some Svelte components, which is strange because that's the way to do it. Idk if it's related to our TS setup.
- Migrating to eslint-plugin-svelte. This reports many more errors, including not being able to resolve aliased paths like $lib/.... This should be done eventually but requires more time to get the config right.
- Making use of TypeScript project references. I probably didn't get the configuration right. I wanted to speed up pnpm check but it resulted in more errors about not being able to find types.

What's still problematic:

- eslint produces a bunch of warnings regarding accessing properties on any types. It looks like the eslint setup doesn't take the types generated from SvelteKit into account. Will have to investigate this.
This commit is contained in:
Felix Kling 2024-01-04 18:17:19 +01:00 committed by GitHub
parent 4d2df54f1e
commit fffe80d114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 656 additions and 1858 deletions

View File

@ -46,7 +46,6 @@ BUILD_DEPS = [
":node_modules/@graphql-tools/utils",
":node_modules/@melt-ui/svelte",
":node_modules/@popperjs/core",
":node_modules/@remix-run/router",
":node_modules/@rollup/plugin-graphql",
":node_modules/@sourcegraph/branded",
":node_modules/@sourcegraph/common",
@ -61,7 +60,6 @@ BUILD_DEPS = [
":node_modules/lodash-es",
":node_modules/graphql",
":node_modules/prismjs",
":node_modules/react",
":node_modules/svelte",
":node_modules/ts-key-enum",
":node_modules/vite",
@ -76,16 +74,13 @@ BUILD_DEPS = [
"//:node_modules/@reach/menu-button",
"//:node_modules/@types/lodash",
"//:node_modules/@types/node",
"//:node_modules/@types/react",
"//:node_modules/classnames",
"//:node_modules/date-fns",
"//:node_modules/highlight.js",
"//:node_modules/lodash",
"//:node_modules/open-color",
"//:node_modules/path-browserify",
"//:node_modules/react-dom",
"//:node_modules/react-resizable",
"//:node_modules/react-router-dom",
"//:node_modules/rxjs",
"//:node_modules/uuid",
"//cmd/frontend/graphqlbackend:graphql_schema",

View File

@ -29,7 +29,7 @@
"@storybook/addon-essentials": "^7.2.0",
"@storybook/addon-interactions": "^7.2.0",
"@storybook/addon-links": "^7.2.0",
"@storybook/addon-svelte-csf": "^3.0.7",
"@storybook/addon-svelte-csf": "^4.1.0",
"@storybook/blocks": "^7.2.0",
"@storybook/svelte": "^7.2.0",
"@storybook/sveltekit": "^7.2.0",
@ -49,8 +49,6 @@
"msw-storybook-addon": "^1.8.0",
"prettier": "2.8.1",
"prettier-plugin-svelte": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"signale": "^1.4.0",
"storybook": "^7.2.0",
"storybook-dark-mode": "^3.0.1",
@ -65,7 +63,6 @@
"dependencies": {
"@melt-ui/svelte": "^0.66.2",
"@popperjs/core": "^2.11.8",
"@remix-run/router": "~1.3.3",
"@sourcegraph/branded": "workspace:*",
"@sourcegraph/client-api": "workspace:*",
"@sourcegraph/common": "workspace:*",

View File

@ -7,3 +7,9 @@ declare global {
interface PageData {}
}
}
// Importing highlight.js/lib/core or a language (highlight.js/lib/languages/*) results in
// a compiler error about not being able to find the types. Adding this declaration fixes it.
declare module 'highlight.js/lib/core' {
export * from 'highlight.js'
}

View File

@ -107,7 +107,7 @@
temporaryTooltip,
} from '$lib/web'
import { goto } from '$app/navigation'
import { type CodeIntelAPI } from '$lib/shared'
import type { CodeIntelAPI } from '$lib/shared'
import { goToDefinition, openImplementations, openReferences } from './repo/blob'
import type { LineOrPositionOrRange } from '$lib/common'

View File

@ -57,31 +57,33 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click={preventClickOnDisabledLink}>
<Button variant="secondary" outline>
<a slot="custom" let:className href={firstPageURL} class={className} aria-disabled={firstAndPreviousDisabled}>
<Icon svgPath={mdiPageFirst} inline />
</a>
<svelte:fragment slot="custom" let:buttonClass>
<a href={firstPageURL} class={buttonClass} aria-disabled={firstAndPreviousDisabled}>
<Icon svgPath={mdiPageFirst} inline />
</a>
</svelte:fragment>
</Button>
<Button variant="secondary" outline>
<a
slot="custom"
let:className
class={className}
href={previousPageURL}
aria-disabled={firstAndPreviousDisabled}
>
<Icon svgPath={mdiChevronLeft} inline />Previous
</a>
<svelte:fragment slot="custom" let:buttonClass>
<a class={buttonClass} href={previousPageURL} aria-disabled={firstAndPreviousDisabled}>
<Icon svgPath={mdiChevronLeft} inline />Previous
</a>
</svelte:fragment>
</Button>
<Button variant="secondary" outline>
<a slot="custom" let:className class={className} href={nextPageURL} aria-disabled={nextAndLastDisabled}>
Next <Icon svgPath={mdiChevronRight} inline />
</a>
<svelte:fragment slot="custom" let:buttonClass>
<a class={buttonClass} href={nextPageURL} aria-disabled={nextAndLastDisabled}>
Next <Icon svgPath={mdiChevronRight} inline />
</a>
</svelte:fragment>
</Button>
{#if showLastpageButton}
<Button variant="secondary" outline>
<a slot="custom" let:className class={className} href={lastPageURL} aria-disabled={nextAndLastDisabled}>
<Icon svgPath={mdiPageLast} inline />
</a>
<svelte:fragment slot="custom" let:buttonClass>
<a class={buttonClass} href={lastPageURL} aria-disabled={nextAndLastDisabled}>
<Icon svgPath={mdiPageLast} inline />
</a>
</svelte:fragment>
</Button>
{/if}
</div>

View File

@ -1,45 +0,0 @@
<script lang="ts">
import React from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { onDestroy, onMount } from 'svelte'
import type { SettingsCascadeOrError } from '$lib/shared'
import { ReactAdapter } from './react-interop'
type ComponentProps = $$Generic<{}>
export let component: React.FunctionComponent<ComponentProps>
export let props: ComponentProps
export let route: string
export let settings: SettingsCascadeOrError
let container: HTMLDivElement
let root: Root | null = null
function renderComponent(
root: Root | null,
component: React.FunctionComponent<ComponentProps>,
props: ComponentProps,
route: string,
settings: SettingsCascadeOrError
) {
root?.render(
React.createElement(
ReactAdapter,
{
route,
settings,
},
React.createElement(component, props)
)
)
}
onMount(() => (root = createRoot(container)))
onDestroy(() => root?.unmount())
$: renderComponent(root, component, props, route, settings)
</script>
<div bind:this={container} />

View File

@ -1,15 +1,24 @@
<script lang="ts">
<script lang="ts" context="module">
import Separator, { getSeparatorPosition } from '$lib/Separator.svelte'
import { Story } from '@storybook/addon-svelte-csf'
export const meta = {
component: Separator,
}
</script>
<script lang="ts">
const currentPosition = getSeparatorPosition('separator-example', 0.5)
$: width = `${$currentPosition * 100}%`
</script>
<section>
<div class="left match-highlight" style:min-width={width} style:max-width={width}>Left content</div>
<Separator {currentPosition} />
<div class="right">Right content</div>
</section>
<Story name="Default">
<section>
<div class="left match-highlight" style:min-width={width} style:max-width={width}>Left content</div>
<Separator {currentPosition} />
<div class="right">Right content</div>
</section>
</Story>
<style lang="scss">
section {

View File

@ -61,10 +61,10 @@
</script>
<!-- TODO: implement keyboard handlers. See https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
bind:this={divider}
role="separator"
tabindex="0"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={$currentPosition}

View File

@ -1,8 +1,16 @@
<script lang="ts">
<script lang="ts" context="module">
import Timestamp from '$lib/Timestamp.svelte'
import { faker } from '@faker-js/faker'
import { Story } from '@storybook/addon-svelte-csf'
import type { ComponentProps } from 'svelte'
export const meta = {
component: Timestamp,
}
</script>
<script lang="ts">
faker.seed(1)
const date = faker.date.recent()
const cases: [string, Partial<ComponentProps<Timestamp>>][] = [
['default', {}],
@ -15,15 +23,17 @@
]
</script>
<h2>Timestamp props</h2>
<table>
{#each cases as [title, props]}
<tr>
<th>{title}</th>
<td><Timestamp {date} {...props} /></td>
</tr>
{/each}
</table>
<Story name="Default">
<h2>Timestamp props</h2>
<table>
{#each cases as [title, props]}
<tr>
<th>{title}</th>
<td><Timestamp {date} {...props} /></td>
</tr>
{/each}
</table>
</Story>
<style lang="scss">
td,

View File

@ -0,0 +1,48 @@
<script lang="ts" context="module">
import { Story } from '@storybook/addon-svelte-csf'
import Tooltip, { type Placement } from '$lib/Tooltip.svelte'
export const meta = {
component: Tooltip,
}
</script>
<script lang="ts">
const placements: Placement[] = ['auto', 'top', 'left', 'bottom', 'right']
let count = 0
</script>
<Story name="Default">
<h2>Static Tooltip</h2>
<p>
<Tooltip tooltip="Some static tooltip text" placement="auto">
<span>Hover over me</span>
</Tooltip>
</p>
<h2>Dynamic Tooltip</h2>
<p>
Move the mouse over
<Tooltip tooltip="This text was moused over {count} times" placement="top">
<strong on:mousemove={() => count++}>this text</strong>
</Tooltip>
to update the counter in the tooltip.
</p>
<h2>Tooltip placement</h2>
<table>
{#each placements as placement}
<tr>
<th>{placement}</th>
<td><Tooltip tooltip="Tooltip" {placement} alwaysVisible><span>Trigger</span></Tooltip></td>
</tr>
{/each}
</table>
</Story>
<style lang="scss">
td,
th {
padding: 2rem;
}
</style>

View File

@ -0,0 +1,104 @@
<script lang="ts" context="module">
import { Story, Template } from '@storybook/addon-svelte-csf'
import {
createEmptySingleSelectTreeState,
updateTreeState,
type TreeProvider,
type TreeState,
TreeStateUpdate,
} from '$lib/TreeView'
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
export const meta = {
component: TreeView,
}
// Keep in sync with TreeView.stories.ts (can't be exported for some reason)
interface ExampleData {
name: string
children?: ExampleData[]
}
class ExampleProvider implements TreeProvider<ExampleData> {
constructor(private nodes: ExampleData[], private parentPath: string = '') {}
public isSelectable(_entry: ExampleData): boolean {
return true
}
public isExpandable(entry: ExampleData): boolean {
return !!entry.children
}
public getNodeID(entry: ExampleData): string {
return this.parentPath + entry.name
}
public getEntries(): ExampleData[] {
return this.nodes
}
public fetchChildren(entry: ExampleData): Promise<TreeProvider<ExampleData>> {
return Promise.resolve(new ExampleProvider(entry.children ?? [], `${this.parentPath}${entry.name}/`))
}
}
type TreeConfig = [number, ...(TreeConfig | undefined)[]]
function makeExampleData(config: TreeConfig, level = 0): ExampleData[] {
const [n, ...children] = config
return Array.from({ length: n }, (_, i) => ({
name: `level${level}-${i + 1}`,
children: children[i] ? makeExampleData(children[i]!, level + 1) : undefined,
}))
}
</script>
<script lang="ts">
import { writable } from 'svelte/store'
const treeState: TreeState = createEmptySingleSelectTreeState()
const treeStateStore = writable(treeState)
setTreeContext(treeStateStore)
$: $treeStateStore = treeState
function handleSelect({ detail: node }: { detail: HTMLElement }) {
const nodeId = node.dataset.nodeId
if (nodeId) {
$treeStateStore = updateTreeState(
$treeStateStore,
node.dataset.nodeId ?? '',
TreeStateUpdate.SELECT | TreeStateUpdate.EXPAND
)
node.focus()
}
}
</script>
<Template let:args>
<TreeView treeProvider={new ExampleProvider(makeExampleData(args.data))} on:select={handleSelect}>
<svelte:fragment let:entry>
{entry.name}
</svelte:fragment>
</TreeView>
</Template>
<Story
name="Simple"
args={{
data: [3, [2], [3]],
}}
/>
<Story
name="DeeplyNested"
args={{
data: [5, [3, [2, [2, [3]]], [1], [2, [1, [2]]]], , [3, [2, [2], [3]], [1], [2, [1, [2]]]], [3]],
}}
/>
<style lang="scss">
:global(.label:hover),
:global(.treeitem.selected) > :global(.label) {
background-color: lightblue;
}
:global(.treeitem:focus) > :global(.label) {
outline: 2px solid green !important;
}
</style>

View File

@ -0,0 +1,25 @@
<script lang="ts" context="module">
import { Story } from '@storybook/addon-svelte-csf'
import UserAvatar from './UserAvatar.svelte'
import { faker } from '@faker-js/faker'
export const meta = {
component: UserAvatar,
}
</script>
<script lang="ts">
faker.seed(1)
const avatarURL = faker.internet.avatar()
const username = faker.internet.userName()
const displayName = `${faker.person.firstName()} ${faker.person.lastName()}`
</script>
<Story name="Default">
<h2>With <code>avatarURL</code></h2>
<UserAvatar user={{ avatarURL }} />
<h2>With <code>username</code> "{username}"</h2>
<UserAvatar user={{ username }} />
<h2>With <code>displayName</code> "{displayName}"</h2>
<UserAvatar user={{ displayName }} />
</Story>

View File

@ -128,6 +128,10 @@ export const restrictToViewport: Action<HTMLElement, { offset?: number }> = (nod
}
}
interface ComputeFitAttributes {
'on:fit': (event: CustomEvent<{ itemCount: number }>) => void
}
/**
* An action to compute the number of elements that fit inside the container.
* This works by caching the position of the right hand of each child element,
@ -136,9 +140,7 @@ export const restrictToViewport: Action<HTMLElement, { offset?: number }> = (nod
* Because of this this action only works for static element lists,
* i.e. on initial render the node needs to contain all possible child elements.
*/
export const computeFit: Action<HTMLElement> = (
node
): ActionReturn<void, { 'on:fit': (event: CustomEvent<{ itemCount: number }>) => void }> => {
export const computeFit: Action<HTMLElement, void, ComputeFitAttributes> = node => {
// Holds the cumulative width of all elements up to element i.
const widths: number[] = [0]

View File

@ -10,22 +10,22 @@ describe('featureflags', () => {
useFakeTimers()
const store = createFeatureFlagStore(
[{ name: 'sentinel', value: true }],
[{ name: 'search-debug', value: true }],
vi
.fn()
.mockResolvedValueOnce([{ name: 'sentinel', value: false }])
.mockResolvedValueOnce([{ name: 'sentinel', value: true }])
.mockResolvedValueOnce([{ name: 'search-debug', value: false }])
.mockResolvedValueOnce([{ name: 'search-debug', value: true }])
)
const sub = vi.fn()
store.subscribe(sub)
expect(sub).toHaveBeenLastCalledWith([{ name: 'sentinel', value: true }])
expect(sub).toHaveBeenLastCalledWith([{ name: 'search-debug', value: true }])
await vi.advanceTimersToNextTimerAsync()
expect(sub).toHaveBeenLastCalledWith([{ name: 'sentinel', value: false }])
expect(sub).toHaveBeenLastCalledWith([{ name: 'search-debug', value: false }])
await vi.advanceTimersToNextTimerAsync()
expect(sub).toHaveBeenLastCalledWith([{ name: 'sentinel', value: true }])
expect(sub).toHaveBeenLastCalledWith([{ name: 'search-debug', value: true }])
useRealTimers()
})
@ -33,15 +33,15 @@ describe('featureflags', () => {
describe('featureFlag()', () => {
test('returns the current feature flag value', () => {
mockFeatureFlags({ sentinel: false })
mockFeatureFlags({ 'search-debug': false })
const store = featureFlag('sentinel')
const store = featureFlag('search-debug')
const sub = vi.fn()
store.subscribe(sub)
expect(sub).toHaveBeenLastCalledWith(false)
mockFeatureFlags({ sentinel: true })
mockFeatureFlags({ 'search-debug': true })
expect(sub).toHaveBeenLastCalledWith(true)
unmockFeatureFlags()

View File

@ -1,205 +0,0 @@
import React, { useEffect, useMemo, type FC, type PropsWithChildren } from 'react'
import { createRouter, type History, Action, type Location, type Router } from '@remix-run/router'
import type { Navigation } from '@sveltejs/kit'
import { RouterProvider, type RouteObject, UNSAFE_enhanceManualRouteObjects } from 'react-router-dom'
import { RouterLink, setLinkComponent } from '@sourcegraph/wildcard'
import { goto } from '$app/navigation'
import { navigating } from '$app/stores'
import { SettingsProvider, type SettingsCascadeOrError } from '$lib/shared'
import { WildcardThemeContext, type WildcardTheme } from './wildcard'
setLinkComponent(RouterLink)
const WILDCARD_THEME: WildcardTheme = {
isBranded: true,
}
/**
* Creates a minimal context for rendering React components inside Svelte, including a
* custom React Router router to integrate with SvelteKit.
*/
export const ReactAdapter: FC<PropsWithChildren<{ route: string; settings: SettingsCascadeOrError }>> = ({
route,
children,
settings,
}) => {
const router = useMemo(
() =>
createSvelteKitRouter([
{
path: route,
// React.Suspense seems necessary to render the components without error
element: <React.Suspense fallback={true}>{children}</React.Suspense>,
},
]),
[route, children]
)
// Dispose is marked as INTERNAL but without calling it the listeners created by React
// Router are not removed. It doesn't look like React Router expects to be unmounted
// during the lifetime of the application.
useEffect(() => () => router.dispose(), [router])
return (
<WildcardThemeContext.Provider value={WILDCARD_THEME}>
<SettingsProvider settingsCascade={settings}>
<RouterProvider router={router} />
</SettingsProvider>
</WildcardThemeContext.Provider>
)
}
/**
* Custom router that synchronizes between the SvelteKit routing and React router.
*/
function createSvelteKitRouter(routes: RouteObject[]): Router {
return createRouter({
routes: UNSAFE_enhanceManualRouteObjects(routes),
history: createSvelteKitHistory(),
}).initialize()
}
/**
* Custom history that synchronizes between the SvelteKit routing and React router.
* This is a "best effort" implementation because the API used here is not very well
* documented or doesn't seem to be intended for this use case.
*
* Caveat: Using the browser back/forward buttons to navigate between hash targets on the
* same page doesn't seem to scroll the target into view. It's not clear yet why that is.
*/
function createSvelteKitHistory(): History {
let action: Action = Action.Push
const history: History = {
get action() {
return action
},
get location() {
return createLocation(window.location)
},
createURL,
createHref(to) {
return typeof to === 'string' ? to : toPath(createURL(to))
},
go(delta) {
window.history.go(delta)
},
push(to, state) {
action = Action.Push
// Without this, links that are outside of the React component (i.e. Svelte)
// pointing to a path handle by the React component causes duplicate entries
// See below for more information.
// This is safe to do because the browser does the same when clicking on a
// link that navigates to the curren page.
if (createURL(to).href !== window.location.href) {
goto(createURL(to), { state: state ?? undefined })
// Make eslint happy
.catch(() => {})
}
},
replace(to, state) {
action = Action.Replace
goto(createURL(to), { state: state ?? undefined, replaceState: true })
// Make eslint happy
.catch(() => {})
},
encodeLocation(to) {
const url = createURL(to)
return {
hash: url.hash,
search: url.search,
pathname: url.pathname,
}
},
listen(listener) {
let prevState: Navigation | null = null
return navigating.subscribe(state => {
// Events are emitted when navigation *starts*. That means the browser URL hasn't updated yet
// I don't know whether that's relevant for React Router or not, but in order to make the
// equality check in the `push` method possible we need to wait to call `listener` until the
// navigation completed.
// This is done by storing the emitted value in `prevState` and wait until the next event, which
// will emit `null`.
// NOTE: SvelteKit does not emit events when the back/forward buttons are used to navigate between
// "hashes" on the same page. SvelteKit instead lets the browser handle these natively. However
// I noticed that at least on Notebook pages this won't scroll the target into view.
if (!state && prevState) {
switch (prevState.type) {
case 'popstate': {
action = Action.Pop
if (prevState.to) {
listener({
action,
location: createLocation(prevState.to.url),
delta: prevState.delta ?? 0,
})
}
break
}
case 'link': {
// This is a special case for SvelteKit. In a normal browser context it seems that `listen`
// should only handle popstate events. Listening to the SvelteKit 'link' event seems
// necessary to properly handle SvelteKit links which point to paths handled by this React
// component (it neither works without it nor with the default browser router).
// However, React Router doesn't seem to expect that `listener` can be called for "push" events
// and will subequently call `history.push`. In order to prevent a double history entry
// we are checking whether the target URL is the same as the current URL and do not call
// back to SvelteKit again if that's the case.
action = Action.Push
if (prevState.to) {
listener({ action, location: createLocation(prevState.to.url), delta: 1 })
}
break
}
}
}
prevState = state
})
},
}
return history
}
function createURL(path: string | { pathname?: string; search?: string; hash?: string }): URL {
if (typeof path === 'string') {
return new URL(path, window.location.href)
}
const url = new URL(window.location.href)
if (path.pathname !== undefined) {
url.pathname = path.pathname
}
if (path.search !== undefined) {
url.search = path.search
}
if (path.hash !== undefined) {
url.hash = path.hash
}
return url
}
function createLocation(target: URL | typeof window['location']): Location {
return {
pathname: target.pathname,
search: target.search,
hash: target.hash,
state: window.history.state?.usr ?? null,
key: window.history.state?.key ?? 'default',
}
}
function toPath(location: { pathname: string; search: string; hash: string }): string {
return location.pathname + location.search + location.hash
}

View File

@ -82,12 +82,14 @@
<Tooltip {tooltip}>
{#if !disabled && url}
<Button variant="secondary" size="sm" {disabled}>
<a slot="custom" let:className class={className} href={url} on:click={run} {...newTabProps}>
{#if icon}
<img src={icon.url} alt={icon.description} />
{/if}
{content}
</a>
<svelte:fragment slot="custom" let:buttonClass>
<a class={buttonClass} href={url} on:click={run} {...newTabProps}>
{#if icon}
<img src={icon.url} alt={icon.description} />
{/if}
{content}
</a>
</svelte:fragment>
</Button>
{:else if action.command}
<Button variant="secondary" size="sm" {disabled} on:click={run}>

View File

@ -2,11 +2,12 @@
import { mdiFileDocumentOutline, mdiFolderOutline } from '@mdi/js'
import Icon from '$lib/Icon.svelte'
import type { TreeEntry, TreeEntryWithCommitInfo } from './FileTable.gql'
import type { TreeEntryWithCommitInfo } from './FileTable.gql'
import { replaceRevisionInURL } from '$lib/web'
import Timestamp from '$lib/Timestamp.svelte'
import type { TreeEntryFields } from './api/tree'
export let entries: TreeEntry[]
export let entries: TreeEntryFields[]
export let commitInfo: TreeEntryWithCommitInfo[]
export let revision: string

View File

@ -0,0 +1,21 @@
<script lang="ts" context="module">
import { createHistoryResults } from '$testdata'
import { Story } from '@storybook/addon-svelte-csf'
import HistoryPanel from './HistoryPanel.svelte'
export const meta = {
component: HistoryPanel,
}
</script>
<script lang="ts">
let commitCount = 5
$: [initial, next] = createHistoryResults(2, commitCount)
</script>
<Story name="Default">
<p>Commits to show: <input type="number" bind:value={commitCount} min="1" max="100" /></p>
<hr />
{#key commitCount}
<HistoryPanel history={Promise.resolve(initial)} fetchMoreHandler={async () => next} />
{/key}
</Story>

View File

@ -79,7 +79,7 @@
overflow-x: auto;
word-wrap: normal;
> *:first-child {
> :global(*:first-child) {
margin-top: var(--hover-overlay-content-margin-top);
margin-bottom: 0.5rem;
}
@ -127,7 +127,7 @@
// We use <hr>s as a divider between multiple contents.
// This has the nice property of having floating buttons that text wraps around.
hr {
:global(hr) {
// `<p>` and `<pre>` define their own margins, `<hr>` is only concerned with rendering the separator itself.
margin-top: 0;
margin-bottom: 0;

View File

@ -9,8 +9,9 @@ export class HovercardView implements TooltipView {
private readonly hovercard: Hovercard
constructor(
private readonly view: EditorView,
private readonly tokenRange: TooltipViewOptions['token'],
// TODO: Add support for pinned tooltips
_view: EditorView,
_tokenRange: TooltipViewOptions['token'],
hovercardData: TooltipViewOptions['hovercardData']
) {
this.dom = document.createElement('div')
@ -18,8 +19,6 @@ export class HovercardView implements TooltipView {
target: this.dom,
props: {
hovercardData,
tokenRange,
view,
},
})
}

View File

@ -2,8 +2,6 @@ import { query, gql } from '$lib/graphql'
import type {
RepositoryCommitResult,
Scalars,
RepositoryComparisonDiffResult,
RepositoryComparisonDiffVariables,
HistoryResult,
GitHistoryResult,
GitHistoryVariables,
@ -60,14 +58,6 @@ const gitCommitFragment = gql`
}
`
const diffStatFields = gql`
fragment DiffStatFields on DiffStat {
__typename
added
deleted
}
`
const fileDiffHunkFields = gql`
fragment FileDiffHunkFields on FileDiffHunk {
oldNoNewlineAt
@ -210,65 +200,6 @@ export async function fetchRepoCommit(repoId: string, revision: string): Promise
})
}
export type RepositoryComparisonDiff = Extract<RepositoryComparisonDiffResult['node'], { __typename?: 'Repository' }>
export async function queryRepositoryComparisonFileDiffs(args: {
repo: Scalars['ID']['input']
base: string | null
head: string | null
first: number | null
after: string | null
paths: string[] | null
}): Promise<RepositoryComparisonDiff['comparison']['fileDiffs']> {
const data = await query<RepositoryComparisonDiffResult, RepositoryComparisonDiffVariables>(
gql`
query RepositoryComparisonDiff(
$repo: ID!
$base: String
$head: String
$first: Int
$after: String
$paths: [String!]
) {
node(id: $repo) {
id
... on Repository {
comparison(base: $base, head: $head) {
fileDiffs(first: $first, after: $after, paths: $paths) {
nodes {
...FileDiffFields
}
totalCount
pageInfo {
endCursor
hasNextPage
}
diffStat {
...DiffStatFields
}
}
}
}
}
}
${fileDiffFields}
${diffStatFields}
`,
args
)
const repo = data.node
if (repo === null) {
throw new Error('Repository not found')
}
if (repo.__typename !== 'Repository') {
throw new Error('Not a repository')
}
return repo.comparison.fileDiffs
}
export async function fetchDiff(
repoID: Scalars['ID']['input'],
revspec: string,

View File

@ -96,13 +96,16 @@ export async function queryGitReferences(args: {
return data.node.gitRefs
}
interface Data {
export interface GitBranchesOverview {
defaultBranch: GitRefFields | null
activeBranches: GitRefFields[]
hasMoreActiveBranches: boolean
}
export async function queryGitBranchesOverview(args: { repo: Scalars['ID']['input']; first: number }): Promise<Data> {
export async function queryGitBranchesOverview(args: {
repo: Scalars['ID']['input']
first: number
}): Promise<GitBranchesOverview> {
const data = await query<RepositoryGitBranchesOverviewResult, RepositoryGitBranchesOverviewVariables>(
gql`
query RepositoryGitBranchesOverview($repo: ID!, $first: Int!, $withBehindAhead: Boolean!) {

View File

@ -25,7 +25,7 @@ export function navFromPath(path: string, repo: string): [string, string][] {
part,
resolvePath(TREE_ROUTE_ID, { repo, path: all.slice(0, index + 1).join('/') }),
])
.concat([[parts.at(-1), '']])
.concat([[parts.at(-1) ?? '', '']])
}
export function getRevisionLabel(

View File

@ -324,18 +324,4 @@
background-color: transparent;
cursor: pointer;
}
.popover-content {
input {
margin-left: 0;
}
label {
max-width: 17rem;
display: flex;
cursor: pointer;
padding: 0.5rem 1rem;
border-top: 1px solid var(--border-color);
}
}
</style>

View File

@ -89,13 +89,6 @@
}
}
.divider {
width: 1px;
height: 1rem;
background-color: var(--border-color-2);
margin: 0 0.5rem;
}
button.icon {
padding: 0;
margin: 0;

View File

@ -0,0 +1,44 @@
<script lang="ts" context="module">
import Badge, { BADGE_VARIANTS } from '$lib/wildcard/Badge.svelte'
import { Story } from '@storybook/addon-svelte-csf'
export const meta = {
component: Badge,
}
</script>
<Story name="Default">
<h2>Default</h2>
<table>
<thead>
<tr><th>Variant</th><th /><th>pill=true</th><th>small=true</th><th>pill=true small=true</th></tr>
</thead>
<tbody>
{#each BADGE_VARIANTS as variant}
<tr>
<th>{variant}</th>
<td><Badge {variant}>Badge</Badge> </td>
<td><Badge {variant} pill>Badge</Badge></td>
<td><Badge {variant} small>Badge</Badge></td>
<td><Badge {variant} pill small>Badge</Badge></td>
</tr>
{/each}
</tbody>
</table>
<h2 id="custom">Custom elements</h2>
<Badge variant="primary">
<a slot="custom" let:class={cls} class={cls} href="#custom">I'm a link</a>
</Badge>
<style lang="scss">
td,
th {
padding: 0.5rem;
}
td {
text-align: center;
}
</style>
</Story>

View File

@ -18,12 +18,12 @@
export let display: $$Props['display'] = undefined
export let outline: $$Props['outline'] = undefined
$: brandedButtonClassname = getButtonClassName({ variant, outline, display, size })
$: buttonClass = getButtonClassName({ variant, outline, display, size })
</script>
<slot name="custom" className={brandedButtonClassname}>
<slot name="custom" {buttonClass}>
<!-- $$restProps holds all the additional props that are passed to the component -->
<button class={brandedButtonClassname} {...$$restProps} on:click|preventDefault>
<button class={buttonClass} {...$$restProps} on:click|preventDefault>
<slot />
</button>
</slot>

View File

@ -7,11 +7,11 @@
export let direction: typeof BUTTON_GROUP_DIRECTION[number] = 'horizontal'
$: className = classNames(styles.btnGroup, direction === 'vertical' && styles.btnGroupVertical)
$: buttonClass = classNames(styles.btnGroup, direction === 'vertical' && styles.btnGroupVertical)
</script>
<slot name="custom" role="group" {className}>
<div role="group" class={className}>
<slot name="custom" role="group" {buttonClass}>
<div role="group" class={buttonClass}>
<slot />
</div>
</slot>

View File

@ -1,9 +1,7 @@
// We want to limit the number of imported modules as much as possible
/* eslint-disable no-restricted-imports */
export { default as Button } from './Button.svelte'
export { default as ButtonGroup } from './ButtonGroup.svelte'
export { WildcardThemeContext, type WildcardTheme } from '@sourcegraph/wildcard/src/hooks/useWildcardTheme'
export { default as Badge } from './Badge.svelte'
export { default as DropdownMenu } from './menu/Menu.svelte'
export { default as MenuLink } from './menu/MenuLink.svelte'

View File

@ -61,7 +61,9 @@
<UserMenu {authenticatedUser} />
{:else}
<Button variant="secondary" outline>
<a slot="custom" let:className class={className} href="/sign-in" data-sveltekit-reload>Sign in</a>
<svelte:fragment slot="custom" let:buttonClass>
<a class={buttonClass} href="/sign-in" data-sveltekit-reload>Sign in</a>
</svelte:fragment>
</Button>
{/if}
</div>

View File

@ -1,32 +1,47 @@
import { fetchRepoCommit, queryRepositoryComparisonFileDiffs } from '$lib/repo/api/commits'
import type { PageLoad } from './$types'
import { CommitQuery, DiffQuery } from './page.gql'
export const load: PageLoad = async ({ parent, params }) => {
const { resolvedRevision } = await parent()
const commit = fetchRepoCommit(resolvedRevision.repo.id, params.revspec).then(data => {
if (data?.node?.__typename === 'Repository') {
return { commit: data.node.commit, repo: resolvedRevision.repo }
}
return { commit: null, repo: resolvedRevision.repo }
})
const {
resolvedRevision: { repo },
graphqlClient,
} = await parent()
const commit = graphqlClient
.query({ query: CommitQuery, variables: { repo: repo.id, revspec: params.revspec } })
.then(result => {
if (result.data.node?.__typename === 'Repository') {
return result.data.node.commit
}
return null
})
return {
deferred: {
commit: commit.then(result => result?.commit ?? null),
diff: commit.then(result => {
if (!result.commit?.oid || !result.commit.parents[0]?.oid) {
return null
}
return queryRepositoryComparisonFileDiffs({
repo: result.repo.id,
base: result.commit?.parents[0].oid,
head: result.commit?.oid,
paths: [],
first: null,
after: null,
commit,
// TODO: Support pagination
diff: commit
.then(commit => {
if (!commit?.oid || !commit.parents[0]?.oid) {
return null
}
return graphqlClient.query({
query: DiffQuery,
variables: {
repo: repo.id,
base: commit.parents[0].oid,
head: commit.oid,
paths: [],
first: null,
after: null,
},
})
})
}),
.then(result => {
if (result?.data.node?.__typename === 'Repository') {
return result.data.node.comparison.fileDiffs
}
return null
}),
},
}
}

View File

@ -0,0 +1,21 @@
#import './FileDiffHunks.gql'
fragment FileDiff_Diff on FileDiff {
newPath
oldPath
mostRelevantFile {
canonicalURL # key field
url
}
newFile {
canonicalURL # key field
binary
}
stat {
added
deleted
}
hunks {
...FileDiffHunks_Hunk
}
}

View File

@ -4,13 +4,13 @@
import { mdiChevronRight, mdiChevronDown } from '@mdi/js'
import { numberWithCommas } from '$lib/common'
import type { FileDiffFields } from '$lib/graphql-operations'
import Icon from '$lib/Icon.svelte'
import DiffSquares from './DiffSquares.svelte'
import FileDiffHunks from './FileDiffHunks.svelte'
import type { FileDiff_Diff } from './FileDiff.gql'
export let fileDiff: FileDiffFields
export let fileDiff: FileDiff_Diff
export let expanded = !!fileDiff.newPath
$: isBinary = fileDiff.newFile?.binary

View File

@ -0,0 +1,17 @@
fragment FileDiffHunks_Hunk on FileDiffHunk {
oldRange {
startLine
lines
}
newRange {
startLine
lines
}
highlight(disableTimeout: false) {
lines {
kind
html
}
}
section
}

View File

@ -1,9 +1,10 @@
<script lang="ts">
import { DiffHunkLineType } from '$lib/graphql-types'
import '$lib/highlight.scss'
import { DiffHunkLineType, type FileDiffFields } from '$lib/graphql-operations'
import type { FileDiffHunks_Hunk } from './FileDiffHunks.gql'
export let hunks: FileDiffFields['hunks']
export let hunks: FileDiffHunks_Hunk[]
</script>
{#if hunks.length === 0}

View File

@ -0,0 +1,42 @@
#import '$lib/Commit.gql'
#import './FileDiff.gql'
query CommitQuery($repo: ID!, $revspec: String!) {
node(id: $repo) {
... on Repository {
id
commit(rev: $revspec) {
id
oid
parents {
id
oid
abbreviatedOID
canonicalURL
}
...Commit
}
}
}
}
query DiffQuery($repo: ID!, $base: String, $head: String, $first: Int, $after: String, $paths: [String!]) {
node(id: $repo) {
... on Repository {
id
comparison(base: $base, head: $head) {
fileDiffs(first: $first, after: $after, paths: $paths) {
nodes {
...FileDiff_Diff
}
totalCount
pageInfo {
endCursor
hasNextPage
}
}
}
}
}
}

View File

@ -54,15 +54,15 @@
<ButtonGroup>
{#each timePeriodButtons as [label, value]}
<Button variant="secondary">
<button
slot="custom"
let:className
class={className}
class:active={timePeriod === value}
type="button"
data-value={value}
on:click={setTimePeriod}>{label}</button
>
<svelte:fragment slot="custom" let:buttonClass>
<button
class={buttonClass}
class:active={timePeriod === value}
type="button"
data-value={value}
on:click={setTimePeriod}>{label}</button
>
</svelte:fragment>
</Button>
{/each}
</ButtonGroup>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import { mdiMapSearch } from '@mdi/js'
import { asError, isErrorLike, type ErrorLike } from '$lib/common'
import HeroPage from '$lib/HeroPage.svelte'
import { logViewEvent } from '$lib/logger'
@ -14,18 +13,10 @@
<HeroPage title="Repository not found" svgIconPath={mdiMapSearch}>
{#if viewerCanAdminister}
<p>
As a site admin, you can add <Code>{repo}</Code> to Sourcegraph to allow users to search and view it by
As a site admin, you can add <code>{repoName}</code> to Sourcegraph to allow users to search and view it by
<a href="/site-admin/external-services">connecting an external service</a> referencing it.
</p>
{:else}
<p>To access this repository, contact the Sourcegraph admin.</p>
{/if}
</HeroPage>
<style lang="scss">
div {
padding: 2rem;
width: 100vw;
max-width: 36rem;
}
</style>

View File

@ -73,7 +73,7 @@
}
let hasBeenVisible = false
let highlightedHTMLRows: string[][] = undefined
let highlightedHTMLRows: string[][] = []
async function onIntersection(event: { detail: boolean }) {
if (hasBeenVisible) {
return
@ -104,7 +104,7 @@
startLine={group.startLine}
matches={group.matches}
plaintextLines={group.plaintextLines}
highlightedHTMLRows={highlightedHTMLRows?.[index]}
highlightedHTMLRows={highlightedHTMLRows[index]}
/>
</a>
</div>

View File

@ -25,14 +25,14 @@
import SymbolSearchResult from './SymbolSearchResult.svelte'
import { createTemporarySettingsStorage } from '$lib/temporarySettings'
import { setSearchResultsContext } from './searchResultsContext'
import { createTestGraphqlClient } from '$testing/graphql'
setContext<SourcegraphContext>(KEY, {
user: readable(null),
settings: readable({}),
isLightTheme: readable(true),
featureFlags: readable([]),
temporarySettingsStorage: createTemporarySettingsStorage(),
client: readable(null),
client: readable(createTestGraphqlClient()),
})
setSearchResultsContext({

View File

@ -23,7 +23,7 @@
import { resultTypeFilter } from '$lib/search/sidebar'
import { submitSearch, type QueryStateStore, getQueryURL } from '$lib/search/state'
import { groupFilters } from '$lib/search/utils'
import { type AggregateStreamingSearchResults, displayRepoName, type SearchMatch } from '$lib/shared'
import { type AggregateStreamingSearchResults, displayRepoName, type SearchMatch, type Progress } from '$lib/shared'
import Section from './SidebarSection.svelte'
import StreamingProgress from './StreamingProgress.svelte'
@ -40,7 +40,6 @@
export let queryState: QueryStateStore
let resultContainer: HTMLElement | null = null
let searchInput: SearchInput
const sidebarSize = getSeparatorPosition('search-results-sidebar', 0.2)
@ -48,7 +47,7 @@
$: progress = $stream?.progress
// NOTE: done is present but apparently not officially exposed. However
// $stream.state is always "loading". Need to look into this.
$: loading = !progress?.done
$: loading = !(progress as Progress & { done?: boolean })?.done
$: results = $stream?.results
$: filters = groupFilters($stream?.filters)
$: hasFilters = filters.lang.length > 0 || filters.repo.length > 0 || filters.file.length > 0
@ -110,7 +109,7 @@
</svelte:head>
<div class="search">
<SearchInput bind:this={searchInput} {queryState} showSmartSearchButton />
<SearchInput {queryState} showSmartSearchButton />
</div>
<div class="search-results">

View File

@ -37,17 +37,13 @@
<Popover let:registerTrigger let:toggle placement="bottom-start">
<Button variant="secondary" size="sm" outline>
<button
slot="custom"
let:className
use:registerTrigger
class="{className} progress-button"
on:click={() => toggle()}
>
<Icon svgPath={icons[severity]} inline />
{getProgressText(progress).visibleText}
<Icon svgPath={mdiChevronDown} inline />
</button>
<svelte:fragment slot="custom" let:buttonClass>
<button use:registerTrigger class="{buttonClass} progress-button" on:click={() => toggle()}>
<Icon svgPath={icons[severity]} inline />
{getProgressText(progress).visibleText}
<Icon svgPath={mdiChevronDown} inline />
</button>
</svelte:fragment>
</Button>
<div slot="content" class="streaming-popover">
<p>
@ -63,24 +59,24 @@
{#each sortedItems as item, index (item.reason)}
{@const open = openItems[index]}
<Button variant="primary" outline>
<button
slot="custom"
type="button"
let:className
class="{className} p-2 w-100 bg-transparent border-0"
aria-expanded={open}
on:click={() => (openItems[index] = !open)}
>
<h4 class="d-flex align-items-center mb-0 w-100">
<span class="mr-1 flex-shrink-0"><Icon svgPath={icons[item.severity]} inline /></span>
<span class="flex-grow-1 text-left">{item.title}</span>
{#if item.message}
<span class="chevron flex-shrink-0"
><Icon svgPath={open ? mdiChevronDown : mdiChevronLeft} inline /></span
>
{/if}
</h4>
</button>
<svelte:fragment slot="custom" let:buttonClass>
<button
type="button"
class="{buttonClass} p-2 w-100 bg-transparent border-0"
aria-expanded={open}
on:click={() => (openItems[index] = !open)}
>
<h4 class="d-flex align-items-center mb-0 w-100">
<span class="mr-1 flex-shrink-0"><Icon svgPath={icons[item.severity]} inline /></span>
<span class="flex-grow-1 text-left">{item.title}</span>
{#if item.message}
<span class="chevron flex-shrink-0"
><Icon svgPath={open ? mdiChevronDown : mdiChevronLeft} inline /></span
>
{/if}
</h4>
</button>
</svelte:fragment>
</Button>
{#if item.message && open}
<div class="message">
@ -106,10 +102,12 @@
</label>
{/each}
<Button variant="primary">
<button slot="custom" let:className class="{className} mt-3" disabled={searchAgainDisabled}>
<Icon svgPath={mdiMagnify} />
<span>Search again</span>
</button>
<svelte:fragment slot="custom" let:buttonClass>
<button class="{buttonClass} mt-3" disabled={searchAgainDisabled}>
<Icon svgPath={mdiMagnify} />
<span>Search again</span>
</button>
</svelte:fragment>
</Button>
</form>
{/if}

View File

@ -21,7 +21,7 @@
}))
let hasBeenVisible = false
let highlightedHTMLRows: string[][] = undefined
let highlightedHTMLRows: string[][] = []
async function onIntersection(event: { detail: boolean }) {
if (hasBeenVisible) {
return
@ -50,7 +50,7 @@
<CodeExcerpt
startLine={symbol.line - 1}
plaintextLines={['']}
highlightedHTMLRows={highlightedHTMLRows?.[index]}
highlightedHTMLRows={highlightedHTMLRows[index]}
--background-color="transparent"
/>
</div>

View File

@ -1,13 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import BadgeExample from './BadgeExample.svelte'
const meta: Meta<typeof BadgeExample> = {
title: 'wildcard/Badge',
component: BadgeExample,
}
export default meta
type Story = StoryObj<typeof meta>
export const Badge: Story = {}

View File

@ -1,33 +0,0 @@
<script lang="ts">
import Badge, { BADGE_VARIANTS } from '$lib/wildcard/Badge.svelte'
</script>
<h2>Default</h2>
<table>
<thead>
<tr><th>Variant</th><th /><th>pill=true</th><th>small=true</th><th>pill=true small=true</th></tr>
</thead>
<tbody>
{#each BADGE_VARIANTS as variant}
<tr>
<th>{variant}</th>
<td><Badge {variant}>Badge</Badge> </td>
<td><Badge {variant} pill>Badge</Badge></td>
<td><Badge {variant} small>Badge</Badge></td>
<td><Badge {variant} pill small>Badge</Badge></td>
</tr>
{/each}
</tbody>
</table>
<h2 id="custom">Custom elements</h2>
<Badge variant="primary">
<a slot="custom" let:class={cls} class={cls} href="#custom">I'm a link</a>
</Badge>
<style lang="scss">
td,
th {
padding: 0.5rem;
}
</style>

View File

@ -1,13 +0,0 @@
<!--
@component
Decorator for forcing the story component to be destroyed an recreated
-->
<script lang="ts">
import type { SvelteComponent } from 'svelte'
export let component: typeof SvelteComponent<any>
</script>
{#key $$restProps}
<svelte:component this={component} {...$$restProps} />
{/key}

View File

@ -1,33 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import HistoryPanel from '$lib/repo/HistoryPanel.svelte'
import { createHistoryResults } from '$testdata'
import ForceUpdate from './ForceUpdate.svelte'
const meta: Meta<typeof ForceUpdate> = {
title: 'stories/HistoryPanel',
parameters: {
fullscreen: true,
},
}
export default meta
export const Default: StoryObj<{ commitCount: number }> = {
render: args => {
const [initial, next] = createHistoryResults(2, args.commitCount)
return {
Component: ForceUpdate,
props: {
component: HistoryPanel,
history: Promise.resolve(initial),
fetchMoreHandler: async () => next,
},
}
},
args: {
commitCount: 5,
},
}

View File

@ -1,12 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import SeparatorExample from './SeparatorExample.svelte'
const meta: Meta<typeof SeparatorExample> = {
component: SeparatorExample,
}
export default meta
type Story = StoryObj<typeof meta>
export const SplitPane: Story = {}

View File

@ -1,12 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import TimestampExample from './TimestampExample.svelte'
const meta: Meta<typeof TimestampExample> = {
component: TimestampExample,
}
export default meta
type Story = StoryObj<typeof meta>
export const Timestamp: Story = {}

View File

@ -1,12 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import TooltipExample from './TooltipExample.svelte'
const meta: Meta<typeof TooltipExample> = {
component: TooltipExample,
}
export default meta
type Story = StoryObj<typeof meta>
export const Tooltip: Story = {}

View File

@ -1,39 +0,0 @@
<script lang="ts">
import Tooltip, { type Placement } from '$lib/Tooltip.svelte'
const placements: Placement[] = ['auto', 'top', 'left', 'bottom', 'right']
let count = 0
</script>
<h2>Static Tooltip</h2>
<p>
<Tooltip tooltip="Some static tooltip text" placement="auto">
<span>Hover over me</span>
</Tooltip>
</p>
<h2>Dynamic Tooltip</h2>
<p>
Move the mouse over
<Tooltip tooltip="This text was moused over {count} times" placement="top">
<strong on:mousemove={() => count++}>this text</strong>
</Tooltip>
to update the counter in the tooltip.
</p>
<h2>Tooltip placement</h2>
<table>
{#each placements as placement}
<tr>
<th>{placement}</th>
<td><Tooltip tooltip="Tooltip" {placement} alwaysVisible><span>Trigger</span></Tooltip></td>
</tr>
{/each}
</table>
<style lang="scss">
td,
th {
padding: 2rem;
}
</style>

View File

@ -1,50 +0,0 @@
<script lang="ts" context="module">
// Keep in sync with TreeView.stories.ts (can't be exported for some reason)
interface ExampleData {
name: string
children?: ExampleData[]
}
</script>
<script lang="ts">
import { writable } from 'svelte/store'
import { updateTreeState, type TreeProvider, type TreeState, TreeStateUpdate } from '$lib/TreeView'
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
export let treeProvider: TreeProvider<ExampleData>
export let treeState: TreeState
const treeStateStore = writable(treeState)
setTreeContext(treeStateStore)
$: $treeStateStore = treeState
function handleSelect({ detail: node }: { detail: HTMLElement }) {
const nodeId = node.dataset.nodeId
if (nodeId) {
$treeStateStore = updateTreeState(
$treeStateStore,
node.dataset.nodeId ?? '',
TreeStateUpdate.SELECT | TreeStateUpdate.EXPAND
)
node.focus()
}
}
</script>
<TreeView {treeProvider} on:select={handleSelect}>
<svelte:fragment let:entry>
{entry.name}
</svelte:fragment>
</TreeView>
<style lang="scss">
:global(.label:hover),
:global(.treeitem.selected) > :global(.label) {
background-color: lightblue;
}
:global(.treeitem:focus) > :global(.label) {
outline: 2px solid green !important;
}
</style>

View File

@ -1,71 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import { createEmptySingleSelectTreeState, type TreeProvider } from '$lib/TreeView'
import TreeViewExample from './TreeView.example.svelte'
// Keep in sync with TreeView.example.svelte (can't be imported for some reason)
interface ExampleData {
name: string
children?: ExampleData[]
}
class ExampleProvider implements TreeProvider<ExampleData> {
constructor(private nodes: ExampleData[], private parentPath: string = '') {}
isSelectable(_entry: ExampleData): boolean {
return true
}
isExpandable(entry: ExampleData): boolean {
return !!entry.children
}
getNodeID(entry: ExampleData): string {
return this.parentPath + entry.name
}
getEntries(): ExampleData[] {
return this.nodes
}
fetchChildren(entry: ExampleData): Promise<TreeProvider<ExampleData>> {
return Promise.resolve(new ExampleProvider(entry.children ?? [], `${this.parentPath}${entry.name}/`))
}
}
type TreeConfig = [number, ...(TreeConfig | undefined)[]]
function makeExampleData(config: TreeConfig, level = 0): ExampleData[] {
const [n, ...children] = config
return Array.from({ length: n }, (_, i) => ({
name: `level${level}-${i + 1}`,
children: children[i] ? makeExampleData(children[i]!, level + 1) : undefined,
}))
}
const meta = {
component: TreeViewExample,
} satisfies Meta<TreeViewExample>
export default meta
type Story = StoryObj<typeof meta>
export const Simple: Story = {
render: args => ({
Component: TreeViewExample,
props: args,
}),
args: {
treeProvider: new ExampleProvider(makeExampleData([3, [2], [3]])),
treeState: createEmptySingleSelectTreeState(),
},
}
export const DeeplyNested: Story = {
render: args => ({
Component: TreeViewExample,
props: args,
}),
args: {
treeProvider: new ExampleProvider(
makeExampleData([5, [3, [2, [2, [3]]], [1], [2, [1, [2]]]], , [3, [2, [2], [3]], [1], [2, [1, [2]]]], [3]])
),
treeState: createEmptySingleSelectTreeState(),
},
}

View File

@ -1,35 +0,0 @@
import { faker } from '@faker-js/faker'
import type { Meta, StoryObj } from '@storybook/svelte'
import UserAvatar from '$lib/UserAvatar.svelte'
const meta: Meta<typeof UserAvatar> = {
component: UserAvatar,
}
export default meta
type Story = StoryObj<typeof meta>
export const WithAvatarUrl: Story = {
args: {
user: {
avatarURL: faker.internet.avatar(),
},
},
}
export const WithUsername: Story = {
args: {
user: {
username: 'hunter',
},
},
}
export const WithDisplayName: Story = {
args: {
user: {
displayName: 'John Doe',
},
},
}

View File

@ -52,11 +52,11 @@ export function createGitCommit(initial?: Partial<GitCommitFields>): GitCommitFi
oid,
abbreviatedOID: oid.slice(0, 7),
url: faker.internet.url(),
canonicalURL: faker.internet.url(),
}
},
{ count: { min: 1, max: 2 } }
),
url: faker.internet.url(),
canonicalURL: faker.internet.url(),
externalURLs: [],
...initial,

View File

@ -0,0 +1,7 @@
import { ApolloClient, InMemoryCache, type NormalizedCacheObject } from '@apollo/client/core'
export function createTestGraphqlClient(): ApolloClient<NormalizedCacheObject> {
return new ApolloClient({
cache: new InMemoryCache(),
})
}

View File

@ -12,7 +12,7 @@ let fakerRefDate: Date
/**
* Use fake timers and optionally set the current date and reference date for data generation.
*/
export function useFakeTimers(refDate?: Date) {
export function useFakeTimers(refDate?: Date): void {
if (!refDate) {
refDate = faker.defaultRefDate()
} else {
@ -28,7 +28,7 @@ export function useFakeTimers(refDate?: Date) {
* Use real timers. The reference date for date generation will be
* restored to a fixed default value.
*/
export function useRealTimers() {
export function useRealTimers(): void {
faker.setDefaultRefDate(fakerRefDate)
vi.useFakeTimers()
vi.useRealTimers()
@ -37,18 +37,18 @@ export function useRealTimers() {
/**
* Mocks arbitrary Svelte context values
*/
export function mockSvelteContext<T>(key: any, value: T) {
export function mockSvelteContext<T>(key: any, value: T): void {
mockedContexts.set(key, value)
}
/**
* Unmock SvelteContext
*/
export function unmockSvelteContext(key: any) {
export function unmockSvelteContext(key: any): void {
mockedContexts.delete(key)
}
// Stores all mocke context values
// Stores all mocked context values
export const mockedContexts = new Map<any, any>()
type SourcegraphContextKey = keyof SourcegraphContext
@ -64,7 +64,6 @@ const mockedSourcgraphContext: {
client: unmocked,
settings: writable({}),
featureFlags: writable([]),
isLightTheme: writable(true),
temporarySettingsStorage: unmocked,
}
@ -95,7 +94,7 @@ mockedContexts.set(
* calling `unmockFeatureFlags` in between then subsequent calls will update the underlying feature flag
* store, updating all subscribers.
*/
export function mockFeatureFlags(evaluatedFeatureFlags: Partial<Record<FeatureFlagName, boolean>>) {
export function mockFeatureFlags(evaluatedFeatureFlags: Partial<Record<FeatureFlagName, boolean>>): void {
const flags = Object.entries(evaluatedFeatureFlags).map(([name, value]) => ({ name, value }))
if (mockedSourcgraphContext.featureFlags === unmocked) {
@ -108,7 +107,7 @@ export function mockFeatureFlags(evaluatedFeatureFlags: Partial<Record<FeatureFl
/**
* Unmock all feature flags.
*/
export function unmockFeatureFlags() {
export function unmockFeatureFlags(): void {
mockedSourcgraphContext.featureFlags = writable([])
}
@ -117,7 +116,7 @@ export function unmockFeatureFlags() {
* calling `unmockUserSettings` in between then subsequent calls will update the underlying settings
* store, updating all subscribers.
*/
export function mockUserSettings(settings: Partial<SettingsCascade['final']>) {
export function mockUserSettings(settings: Partial<SettingsCascade['final']>): void {
if (mockedSourcgraphContext.settings === unmocked) {
mockedSourcgraphContext.settings = writable(settings)
} else {
@ -128,6 +127,6 @@ export function mockUserSettings(settings: Partial<SettingsCascade['final']>) {
/**
* Unmock all user settings.
*/
export function unmockUserSettings() {
export function unmockUserSettings(): void {
mockedSourcgraphContext.settings = writable({})
}

View File

@ -54,8 +54,8 @@ const config = {
$root: '../../',
// Used inside tests for easy access to helpers
$testdata: 'src/testdata.ts',
// Makes it easier to refer to files outside packages (such as images)
$mocks: 'src/testing/mocks.ts',
$testing: 'src/testing',
// Map node-module to browser version
path: '../../node_modules/path-browserify',
// These are directories and cannot be imported from directly in

View File

@ -22,7 +22,7 @@ function generateGraphQLTypes(): Plugin {
documents: ['src/{lib,routes}/**/*.ts', '!src/lib/graphql-{operations,types}.ts'],
config: {
onlyOperationTypes: true,
enumValues: '$lib/graphql-types.ts',
enumValues: '$lib/graphql-types',
//interfaceNameForOperations: 'SvelteKitGraphQlOperations',
},
plugins: ['typescript', 'typescript-operations'],
@ -34,7 +34,7 @@ function generateGraphQLTypes(): Plugin {
documents: ['src/**/*.gql', '!src/**/*.gql.d.ts'],
preset: 'near-operation-file',
presetConfig: {
baseTypesPath: 'lib/graphql-types.ts',
baseTypesPath: 'lib/graphql-types',
extension: '.gql.d.ts',
},
config: {

File diff suppressed because it is too large Load Diff