mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
Svelte: Implement and instantiate FilePopover (#62498)
Implements the file popover on the SvelteKit web app. Co-authored-by: Camden Cheek <camden@ccheek.com>
This commit is contained in:
parent
3f8eaca56f
commit
939e000cdc
@ -0,0 +1,63 @@
|
||||
query FileOrDirPopoverQuery($repoName: String!, $revision: String!, $filePath: String!) {
|
||||
repository(name: $repoName) {
|
||||
commit(rev: $revision) {
|
||||
path(path: $filePath) {
|
||||
...on GitBlob {
|
||||
...FilePopoverFragment
|
||||
}
|
||||
...on GitTree {
|
||||
...DirPopoverFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment FilePopoverFragment on GitBlob {
|
||||
__typename
|
||||
path
|
||||
languages
|
||||
byteSize
|
||||
totalLines
|
||||
history(first: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
...FilePopoverLastCommitFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
...FileIcon_GitBlob
|
||||
}
|
||||
|
||||
fragment DirPopoverFragment on GitTree {
|
||||
__typename
|
||||
path
|
||||
# TODO(camdencheek): fix this to use a count, which currently does not exist in our API.
|
||||
# This currently does not scale well with very large directories.
|
||||
files {
|
||||
name
|
||||
}
|
||||
directories {
|
||||
name
|
||||
}
|
||||
history(first: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
...FilePopoverLastCommitFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment FilePopoverLastCommitFragment on GitCommit {
|
||||
abbreviatedOID
|
||||
oid
|
||||
subject
|
||||
canonicalURL
|
||||
author {
|
||||
date
|
||||
person {
|
||||
...Avatar_Person
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
<script lang="ts" context="module">
|
||||
import { Story } from '@storybook/addon-svelte-csf'
|
||||
|
||||
import type { FilePopoverFragment, DirPopoverFragment, FilePopoverLastCommitFragment } from './FilePopover.gql'
|
||||
import FilePopover from './FilePopover.svelte'
|
||||
|
||||
export const meta = {
|
||||
component: FilePopover,
|
||||
}
|
||||
|
||||
const lastCommit: FilePopoverLastCommitFragment = {
|
||||
abbreviatedOID: '1234567',
|
||||
oid: '1234567890123456789012345678901234567890',
|
||||
subject: 'Test subject',
|
||||
canonicalURL: 'https://sourcegraph.com/about',
|
||||
author: {
|
||||
date: '2021-01-01T00:00:00Z',
|
||||
person: {
|
||||
__typename: 'Person',
|
||||
name: 'camdencheek',
|
||||
avatarURL: 'https://github.com/camdencheek.png',
|
||||
displayName: 'Camden Cheek',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const fileEntry: FilePopoverFragment = {
|
||||
__typename: 'GitBlob',
|
||||
name: 'results.go',
|
||||
path: 'internal/search/results/results.go',
|
||||
languages: ['Go'],
|
||||
byteSize: 325,
|
||||
totalLines: 12,
|
||||
history: {
|
||||
nodes: [
|
||||
{
|
||||
commit: lastCommit,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const dirEntry: DirPopoverFragment = {
|
||||
__typename: 'GitTree',
|
||||
path: 'internal/search/results',
|
||||
files: [{ name: 'results.go' }, { name: 'results_test.go' }],
|
||||
directories: [{ name: 'testdata' }],
|
||||
history: {
|
||||
nodes: [{ commit: lastCommit }],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
<FilePopover repoName={'github.com/sourcegraph/sourcegraph'} entry={fileEntry} />
|
||||
|
||||
<FilePopover repoName={'github.com/sourcegraph/sourcegraph'} entry={dirEntry} />
|
||||
</Story>
|
||||
188
client/web-sveltekit/src/lib/repo/filePopover/FilePopover.svelte
Normal file
188
client/web-sveltekit/src/lib/repo/filePopover/FilePopover.svelte
Normal file
@ -0,0 +1,188 @@
|
||||
<script lang="ts" context="module">
|
||||
import { type DirPopoverFragment, type FilePopoverFragment, FileOrDirPopoverQuery } from './FilePopover.gql'
|
||||
|
||||
export interface FilePopoverInputs {
|
||||
repoName: string
|
||||
revision: string
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export async function fetchPopoverData(args: FilePopoverInputs): Promise<DirPopoverFragment | FilePopoverFragment> {
|
||||
const client = getGraphQLClient()
|
||||
const result = await client.query(FileOrDirPopoverQuery, args)
|
||||
if (result.error) {
|
||||
throw new Error('could not fetch file or dir popover', result.error)
|
||||
}
|
||||
const fragment = result.data?.repository?.commit?.path
|
||||
if (!fragment) {
|
||||
throw new Error('entry does not exist')
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiFolder } from '@mdi/js'
|
||||
|
||||
import Avatar from '$lib/Avatar.svelte'
|
||||
import { pluralize } from '$lib/common'
|
||||
import { getGraphQLClient } from '$lib/graphql'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { displayRepoName } from '$lib/shared'
|
||||
import Timestamp from '$lib/Timestamp.svelte'
|
||||
import { formatBytes } from '$lib/utils'
|
||||
import Badge from '$lib/wildcard/Badge.svelte'
|
||||
|
||||
import FileIcon from '../FileIcon.svelte'
|
||||
|
||||
import NodeLine from './NodeLine.svelte'
|
||||
|
||||
export let repoName: string
|
||||
export let entry: FilePopoverFragment | DirPopoverFragment
|
||||
|
||||
function splitPath(filePath: string): [string, string] {
|
||||
let parts = filePath.split('/')
|
||||
return [parts.slice(0, parts.length - 1).join('/'), parts[parts.length - 1]]
|
||||
}
|
||||
|
||||
$: [dirName, baseName] = splitPath(entry.path)
|
||||
$: lastCommit = entry.history.nodes[0].commit
|
||||
</script>
|
||||
|
||||
<div class="root section muted">
|
||||
<div class="repo-and-path section mono">
|
||||
<small>
|
||||
{displayRepoName(repoName).replaceAll('/', ' / ')}
|
||||
·
|
||||
{dirName ? `${dirName.replaceAll('/', ' / ')}` : '/'}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="lang-and-file section">
|
||||
{#if entry.__typename === 'GitBlob'}
|
||||
<FileIcon file={entry} inline={false} --icon-size="1.5rem" />
|
||||
<div class="file mono">
|
||||
<div>{baseName}</div>
|
||||
<small>
|
||||
{entry.languages[0] ? `${entry.languages[0]} ·` : ''}
|
||||
{entry.totalLines}
|
||||
{pluralize('Line', entry.totalLines)} ·
|
||||
{formatBytes(entry.byteSize)}
|
||||
</small>
|
||||
</div>
|
||||
{:else if entry.__typename === 'GitTree'}
|
||||
<Icon svgPath={mdiFolder} --icon-fill-color="var(--primary)" --icon-size="1.5rem" />
|
||||
<div class="file mono">
|
||||
<div class="title">{baseName}</div>
|
||||
<small>
|
||||
Subdirectories {entry.directories.length}
|
||||
· Files {entry.files.length}
|
||||
</small>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="last-changed section">Last Changed @</div>
|
||||
|
||||
<div class="commit">
|
||||
<div class="node-line"><NodeLine /></div>
|
||||
<div class="commit-info">
|
||||
<Badge variant="link">
|
||||
<a href={lastCommit.canonicalURL} target="_blank">
|
||||
{lastCommit.abbreviatedOID}
|
||||
</a>
|
||||
</Badge>
|
||||
<div class="body">{lastCommit.subject}</div>
|
||||
<div class="author">
|
||||
<Avatar avatar={lastCommit.author.person} --avatar-size="1.0rem" />
|
||||
{lastCommit.author.person.displayName}
|
||||
·
|
||||
<Timestamp date={lastCommit.author.date} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
width: 400px;
|
||||
background: var(--body-bg);
|
||||
|
||||
.section {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.repo-and-path {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lang-and-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
|
||||
div {
|
||||
color: var(--text-body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.last-changed {
|
||||
background-color: var(--secondary-4);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.commit {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
|
||||
.node-line {
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
|
||||
.commit-info {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 0;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: var(--font-size-tiny);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-title);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.body {
|
||||
color: var(--text-body);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,29 @@
|
||||
<div class="line">
|
||||
<div class="connector" />
|
||||
<div class="circle" />
|
||||
<div class="connector" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
$circle-size: 8px;
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.connector {
|
||||
border-left: 1px solid var(--border-color);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.circle {
|
||||
height: $circle-size;
|
||||
width: $circle-size;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-color);
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -96,9 +96,9 @@
|
||||
}
|
||||
|
||||
$: if (!!lastCommitQuery) {
|
||||
// Reset commit history when the query observable changes. Without
|
||||
// this we are showing the commit history of the previously selected
|
||||
// file/folder until the new commit history is loaded.
|
||||
// Reset last commit when the query observable changes. Without
|
||||
// this we are showing the last commit of the previously selected
|
||||
// file/folder until the last commit is loaded.
|
||||
lastCommit = null
|
||||
}
|
||||
|
||||
|
||||
@ -5,12 +5,14 @@
|
||||
|
||||
import { goto } from '$app/navigation'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import Popover from '$lib/Popover.svelte'
|
||||
import { type FileTreeProvider, NODE_LIMIT, type TreeEntry } from '$lib/repo/api/tree'
|
||||
import FileIcon from '$lib/repo/FileIcon.svelte'
|
||||
import FilePopover, { fetchPopoverData } from '$lib/repo/filePopover/FilePopover.svelte'
|
||||
import { getSidebarFileTreeStateForRepo } from '$lib/repo/stores'
|
||||
import { replaceRevisionInURL } from '$lib/shared'
|
||||
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
|
||||
import { createForwardStore } from '$lib/utils'
|
||||
import { createForwardStore, delay } from '$lib/utils'
|
||||
import { Alert } from '$lib/wildcard'
|
||||
|
||||
export let repoName: string
|
||||
@ -122,19 +124,27 @@
|
||||
We handle navigation via the TreeView's select event, to preserve the focus state.
|
||||
Using a link here allows us to benefit from data preloading.
|
||||
-->
|
||||
<a
|
||||
tabindex={-1}
|
||||
href={replaceRevisionInURL(entry.canonicalURL, revision)}
|
||||
on:click|preventDefault={() => {}}
|
||||
data-go-up={isRoot ? true : undefined}
|
||||
>
|
||||
{#if entry.isDirectory}
|
||||
<Icon svgPath={getDirectoryIconPath(entry, expanded)} inline />
|
||||
{:else}
|
||||
<FileIcon inline file={entry.__typename === 'GitBlob' ? entry : null} />
|
||||
{/if}
|
||||
{isRoot ? '..' : entry.name}
|
||||
</a>
|
||||
<Popover let:registerTrigger placement="right-end" showOnHover>
|
||||
<a
|
||||
href={replaceRevisionInURL(entry.canonicalURL, revision)}
|
||||
on:click|preventDefault={() => {}}
|
||||
tabindex={-1}
|
||||
data-go-up={isRoot ? true : undefined}
|
||||
use:registerTrigger
|
||||
>
|
||||
{#if entry.isDirectory}
|
||||
<Icon svgPath={getDirectoryIconPath(entry, expanded)} inline />
|
||||
{:else}
|
||||
<FileIcon inline file={entry.__typename === 'GitBlob' ? entry : null} />
|
||||
{/if}
|
||||
{isRoot ? '..' : entry.name}
|
||||
</a>
|
||||
<svelte:fragment slot="content">
|
||||
{#await delay(fetchPopoverData({ repoName, revision, filePath: entry.path }), 300) then entry}
|
||||
<FilePopover {repoName} {entry} />
|
||||
{/await}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Alert slot="error" let:error variant="danger">
|
||||
|
||||
@ -239,3 +239,15 @@ test('history panel', async ({ page, sg }) => {
|
||||
await page.getByRole('tab', { name: 'History' }).click()
|
||||
await expect(page.getByText('Test commit')).toBeHidden()
|
||||
})
|
||||
|
||||
test('file popover', async ({ page }) => {
|
||||
await page.goto(`/${repoName}`)
|
||||
|
||||
await page.locator('#sidebar-panel').getByRole('button').click()
|
||||
|
||||
await page.getByRole('link', { name: 'index.js' }).hover()
|
||||
await expect(page.getByText('Last Changed')).toBeVisible()
|
||||
|
||||
await page.getByRole('link', { name: 'Sourcegraph', exact: true }).hover()
|
||||
await expect(page.getByText('Last Changed')).toBeHidden()
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user