mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
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:
parent
4d2df54f1e
commit
fffe80d114
@ -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",
|
||||
|
||||
@ -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:*",
|
||||
|
||||
6
client/web-sveltekit/src/app.d.ts
vendored
6
client/web-sveltekit/src/app.d.ts
vendored
@ -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'
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
@ -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 {
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
48
client/web-sveltekit/src/lib/Tooltip.stories.svelte
Normal file
48
client/web-sveltekit/src/lib/Tooltip.stories.svelte
Normal 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>
|
||||
104
client/web-sveltekit/src/lib/TreeView.stories.svelte
Normal file
104
client/web-sveltekit/src/lib/TreeView.stories.svelte
Normal 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>
|
||||
25
client/web-sveltekit/src/lib/UserAvatar.stories.svelte
Normal file
25
client/web-sveltekit/src/lib/UserAvatar.stories.svelte
Normal 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>
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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}>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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!) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
44
client/web-sveltekit/src/lib/wildcard/Badge.stories.svelte
Normal file
44
client/web-sveltekit/src/lib/wildcard/Badge.stories.svelte
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
fragment FileDiffHunks_Hunk on FileDiffHunk {
|
||||
oldRange {
|
||||
startLine
|
||||
lines
|
||||
}
|
||||
newRange {
|
||||
startLine
|
||||
lines
|
||||
}
|
||||
highlight(disableTimeout: false) {
|
||||
lines {
|
||||
kind
|
||||
html
|
||||
}
|
||||
}
|
||||
section
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = {}
|
||||
@ -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>
|
||||
@ -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}
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
@ -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 = {}
|
||||
@ -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 = {}
|
||||
@ -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 = {}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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(),
|
||||
},
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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,
|
||||
|
||||
7
client/web-sveltekit/src/testing/graphql.ts
Normal file
7
client/web-sveltekit/src/testing/graphql.ts
Normal 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(),
|
||||
})
|
||||
}
|
||||
@ -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({})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
|
||||
1123
pnpm-lock.yaml
1123
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user