mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
svelte: Add pagination to commit page (#58847)
This commit adds pagination to the commits page and moves all GraphQL codegen logic into the packages `vite.config.ts` file.
This commit is contained in:
parent
afa86eb401
commit
7ae9d65c22
@ -1,14 +1,13 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { CodegenConfig, generate } from '@graphql-codegen/cli'
|
||||
import { type CodegenConfig, generate } from '@graphql-codegen/cli'
|
||||
import { glob } from 'glob'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
const ROOT_FOLDER = path.resolve(__dirname, '../../../')
|
||||
|
||||
const WEB_FOLDER = path.resolve(ROOT_FOLDER, './client/web')
|
||||
const SVELTEKIT_FOLDER = path.resolve(ROOT_FOLDER, './client/web-sveltekit')
|
||||
const BROWSER_FOLDER = path.resolve(ROOT_FOLDER, './client/browser')
|
||||
const SHARED_FOLDER = path.resolve(ROOT_FOLDER, './client/shared')
|
||||
const VSCODE_FOLDER = path.resolve(ROOT_FOLDER, './client/vscode')
|
||||
@ -23,8 +22,6 @@ const WEB_DOCUMENTS_GLOB = [
|
||||
`!${WEB_FOLDER}/src/end-to-end/**/*.*`, // TODO(bazel): can remove when non-bazel dropped
|
||||
]
|
||||
|
||||
const SVELTEKIT_DOCUMENTS_GLOB = [`${SVELTEKIT_FOLDER}/src/lib/**/*.ts`]
|
||||
|
||||
const BROWSER_DOCUMENTS_GLOB = [
|
||||
`${BROWSER_FOLDER}/src/**/*.{ts,tsx}`,
|
||||
`!${BROWSER_FOLDER}/src/end-to-end/**/*.*`, // TODO(bazel): can remove when non-bazel dropped
|
||||
@ -41,11 +38,11 @@ const GLOBS: Record<string, string[]> = {
|
||||
SharedGraphQlOperations: SHARED_DOCUMENTS_GLOB,
|
||||
VSCodeGraphQlOperations: VSCODE_DOCUMENTS_GLOB,
|
||||
WebGraphQlOperations: WEB_DOCUMENTS_GLOB,
|
||||
SvelteKitGraphQlOperations: SVELTEKIT_DOCUMENTS_GLOB,
|
||||
}
|
||||
|
||||
const EXTRA_PLUGINS: Record<string, string[]> = {
|
||||
SharedGraphQlOperations: ['typescript-apollo-client-helpers'],
|
||||
SvelteKitGraphQlOperations: ['typed-document-node'],
|
||||
}
|
||||
|
||||
const SHARED_PLUGINS = [
|
||||
@ -67,10 +64,6 @@ export const ALL_INPUTS: Input[] = [
|
||||
interfaceNameForOperations: 'WebGraphQlOperations',
|
||||
outputPath: path.join(WEB_FOLDER, './src/graphql-operations.ts'),
|
||||
},
|
||||
{
|
||||
interfaceNameForOperations: 'SvelteKitGraphQlOperations',
|
||||
outputPath: path.join(SVELTEKIT_FOLDER, './src/lib/graphql-operations.ts'),
|
||||
},
|
||||
{
|
||||
interfaceNameForOperations: 'SharedGraphQlOperations',
|
||||
outputPath: path.join(SHARED_FOLDER, './src/graphql-operations.ts'),
|
||||
@ -152,7 +145,7 @@ function createCodegenConfig(operations: Input[]): CodegenConfig {
|
||||
|
||||
if (require.main === module) {
|
||||
// Entry point to generate all GraphQL operations files, or a single one.
|
||||
async function main(args: string[]) {
|
||||
async function main(args: string[]): Promise<void> {
|
||||
if (args.length !== 0 && args.length !== 2) {
|
||||
throw new Error('Usage: [<schemaName> <outputPath>]')
|
||||
}
|
||||
|
||||
@ -9,10 +9,14 @@ node_modules
|
||||
vite.config.ts
|
||||
svelte.config.js
|
||||
playwright.config.ts
|
||||
src/lib/graphql-operations.ts
|
||||
/server.js
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Generated files
|
||||
*.gql.d.ts
|
||||
src/lib/graphql-operations.ts
|
||||
src/lib/graphql-types.ts
|
||||
|
||||
5
client/web-sveltekit/.gitignore
vendored
5
client/web-sveltekit/.gitignore
vendored
@ -7,3 +7,8 @@ node_modules
|
||||
.env.*.local
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Generated TypeScript type definitions and GraphQL documents
|
||||
*.gql.d.ts
|
||||
src/lib/graphql-types.ts
|
||||
src/lib/graphql-operations.ts
|
||||
|
||||
@ -6,30 +6,6 @@ load("@npm//:vite/package_json.bzl", vite_bin = "bin")
|
||||
|
||||
# gazelle:ignore
|
||||
|
||||
js_library(
|
||||
name = "graphql_operations_files",
|
||||
# Keep in sync with glob in client/shared/dev/generateGraphQlOperations.js
|
||||
srcs = glob(
|
||||
["src/**/*.ts"],
|
||||
[
|
||||
# TODO: Ignore legacy build generated file as it conflicts with the Bazel
|
||||
# build. This can be removed after the migration.
|
||||
"src/lib/graphql-operations.ts",
|
||||
],
|
||||
),
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
generate_graphql_operations(
|
||||
name = "graphql_operations_ts",
|
||||
srcs = [
|
||||
":graphql_operations_files",
|
||||
],
|
||||
out = "src/lib/graphql-operations.ts",
|
||||
interface_name = "SvelteKitGraphQlOperations",
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
SRCS = [
|
||||
"package.json",
|
||||
"vite.config.ts",
|
||||
@ -51,18 +27,26 @@ SRCS = [
|
||||
"**/*.svelte",
|
||||
"**/*.html",
|
||||
"**/*.tsx",
|
||||
"**/*.gql",
|
||||
]],
|
||||
[
|
||||
"src/lib/graphql-operations.ts",
|
||||
"src/lib/graphql-types.ts",
|
||||
"src/**/*.gql.d.ts",
|
||||
],
|
||||
)
|
||||
) + glob(["dev/**/*.cjs"])
|
||||
|
||||
BUILD_DEPS = [
|
||||
":graphql_operations_ts",
|
||||
":node_modules/@faker-js/faker",
|
||||
":node_modules/@graphql-codegen/cli",
|
||||
":node_modules/@graphql-codegen/typescript",
|
||||
":node_modules/@graphql-codegen/typescript-operations",
|
||||
":node_modules/@graphql-codegen/near-operation-file-preset",
|
||||
":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",
|
||||
":node_modules/@sourcegraph/http-client",
|
||||
@ -74,6 +58,7 @@ BUILD_DEPS = [
|
||||
":node_modules/@sveltejs/kit",
|
||||
":node_modules/@types/prismjs",
|
||||
":node_modules/lodash-es",
|
||||
":node_modules/graphql",
|
||||
":node_modules/prismjs",
|
||||
":node_modules/react",
|
||||
":node_modules/svelte",
|
||||
@ -102,6 +87,7 @@ BUILD_DEPS = [
|
||||
"//:node_modules/react-router-dom",
|
||||
"//:node_modules/rxjs",
|
||||
"//:node_modules/uuid",
|
||||
"//cmd/frontend/graphqlbackend:graphql_schema",
|
||||
]
|
||||
|
||||
CONFIGS = [
|
||||
|
||||
49
client/web-sveltekit/dev/typed-document-node.cjs
Normal file
49
client/web-sveltekit/dev/typed-document-node.cjs
Normal file
@ -0,0 +1,49 @@
|
||||
// @ts-check
|
||||
|
||||
const { visit } = require('graphql')
|
||||
|
||||
/**
|
||||
* Custom version of the `typed-document-node` plugin that only generates the type definitions
|
||||
* for the documents without generating a parsed version. This is all we need because the
|
||||
* `@rollup/plugin-graphql` plugin already parses the documents for us.
|
||||
*
|
||||
* @param {import('graphql').GraphQLSchema} _schema
|
||||
* @param {import('@graphql-codegen/plugin-helpers').Types.DocumentFile[]} documents
|
||||
* @param {{operationResultSuffix?: string}} config
|
||||
*/
|
||||
const plugin = (_schema, documents, config) => {
|
||||
const { operationResultSuffix = '' } = config
|
||||
|
||||
/** @type {{name: string}[]} */
|
||||
const allOperations = []
|
||||
|
||||
for (const item of documents) {
|
||||
if (item.document) {
|
||||
visit(item.document, {
|
||||
OperationDefinition: {
|
||||
enter: node => {
|
||||
if (node.name && node.name.value) {
|
||||
allOperations.push({
|
||||
name: node.name.value,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const documentNodes = allOperations.map(
|
||||
({ name }) =>
|
||||
`export declare const ${name}: TypedDocumentNode<${name}${operationResultSuffix}, ${name}Variables>`
|
||||
)
|
||||
if (documentNodes.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return {
|
||||
prepend: ["import type { TypedDocumentNode } from '@graphql-typed-document-node/core'\n"],
|
||||
content: documentNodes.join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { plugin }
|
||||
@ -18,7 +18,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/near-operation-file-preset": "^3.0.0",
|
||||
"@graphql-codegen/typescript": "^4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||
"@graphql-tools/utils": "^10.0.11",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@playwright/test": "1.25.0",
|
||||
"@rollup/plugin-graphql": "^2.0.4",
|
||||
"@storybook/addon-essentials": "^7.2.0",
|
||||
"@storybook/addon-interactions": "^7.2.0",
|
||||
"@storybook/addon-links": "^7.2.0",
|
||||
@ -37,6 +44,7 @@
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"graphql": "^15.0.0",
|
||||
"msw": "^1.2.3",
|
||||
"msw-storybook-addon": "^1.8.0",
|
||||
"prettier": "^3.0.0",
|
||||
@ -50,7 +58,6 @@
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "2.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite-plugin-graphql-codegen": "^3.2.3",
|
||||
"vite-plugin-inspect": "^0.7.35",
|
||||
"vitest": "^0.33.0"
|
||||
},
|
||||
@ -60,12 +67,12 @@
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@remix-run/router": "~1.3.3",
|
||||
"@sourcegraph/branded": "workspace:*",
|
||||
"@sourcegraph/client-api": "workspace:*",
|
||||
"@sourcegraph/common": "workspace:*",
|
||||
"@sourcegraph/http-client": "workspace:*",
|
||||
"@sourcegraph/shared": "workspace:*",
|
||||
"@sourcegraph/web": "workspace:*",
|
||||
"@sourcegraph/wildcard": "workspace:*",
|
||||
"@sourcegraph/client-api": "workspace:*",
|
||||
"highlight.js": "^10.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"prismjs": "^1.29.0",
|
||||
|
||||
25
client/web-sveltekit/src/lib/Commit.gql
Normal file
25
client/web-sveltekit/src/lib/Commit.gql
Normal file
@ -0,0 +1,25 @@
|
||||
fragment Commit on GitCommit {
|
||||
id
|
||||
abbreviatedOID
|
||||
canonicalURL
|
||||
subject
|
||||
body
|
||||
author {
|
||||
date
|
||||
person {
|
||||
name
|
||||
displayName
|
||||
email
|
||||
avatarURL
|
||||
}
|
||||
}
|
||||
committer {
|
||||
date
|
||||
person {
|
||||
name
|
||||
displayName
|
||||
email
|
||||
avatarURL
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { mdiDotsHorizontal } from '@mdi/js'
|
||||
|
||||
import type { GitCommitFields } from '$lib/graphql-operations'
|
||||
import type { Commit } from './Commit.gql'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import UserAvatar from '$lib/UserAvatar.svelte'
|
||||
import Timestamp from './Timestamp.svelte'
|
||||
import Timestamp from '$lib/Timestamp.svelte'
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
|
||||
export let commit: GitCommitFields
|
||||
export let commit: Commit
|
||||
export let alwaysExpanded: boolean = false
|
||||
|
||||
function getCommitter({ committer }: Commit): NonNullable<Commit['committer']>['person'] | null {
|
||||
if (!committer) {
|
||||
return null
|
||||
}
|
||||
// Do not show if committer is GitHub (e.g. squash merge)
|
||||
if (committer.person.name === 'GitHub' && committer.person.email === 'noreply@github.com') {
|
||||
return null
|
||||
}
|
||||
return committer.person
|
||||
}
|
||||
|
||||
$: commitDate = new Date(commit.committer ? commit.committer.date : commit.author.date)
|
||||
$: author = commit.author.person
|
||||
$: committer = getCommitter(commit)
|
||||
$: authorAvatarTooltip = author.name + (committer ? ' (author)' : '')
|
||||
let expanded = alwaysExpanded
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="avatar">
|
||||
<UserAvatar user={commit.author.person} />
|
||||
<Tooltip tooltip={authorAvatarTooltip}>
|
||||
<UserAvatar user={author} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{#if commit.committer}
|
||||
{#if committer && committer.name !== author.name}
|
||||
<div class="avatar">
|
||||
<UserAvatar user={commit.committer.person} />
|
||||
<Tooltip tooltip="{committer.name} (committer)">
|
||||
<UserAvatar user={committer} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="info">
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
</script>
|
||||
|
||||
<div class:center>
|
||||
<div class="loading-spinner" class:center class:icon-inline={inline} aria-label="loading" aria-live="polite" />
|
||||
<div class="loading-spinner" class:icon-inline={inline} aria-label="loading" aria-live="polite" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@ -12,6 +12,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
|
||||
@ -1,28 +1,3 @@
|
||||
<script context="module" lang="ts">
|
||||
enum Param {
|
||||
before = '$before',
|
||||
after = '$after',
|
||||
last = '$last',
|
||||
}
|
||||
|
||||
export function getPaginationParams(
|
||||
searchParams: URLSearchParams,
|
||||
pageSize: number
|
||||
):
|
||||
| { first: number; last: null; before: null; after: string | null }
|
||||
| { first: null; last: number; before: string | null; after: null } {
|
||||
if (searchParams.has('$before')) {
|
||||
return { first: null, last: pageSize, before: searchParams.get(Param.before), after: null }
|
||||
} else if (searchParams.has('$after')) {
|
||||
return { first: pageSize, last: null, before: null, after: searchParams.get(Param.after) }
|
||||
} else if (searchParams.has('$last')) {
|
||||
return { first: null, last: pageSize, before: null, after: null }
|
||||
} else {
|
||||
return { first: pageSize, last: null, before: null, after: null }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiPageFirst, mdiPageLast, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
|
||||
|
||||
@ -30,14 +5,23 @@
|
||||
|
||||
import Icon from './Icon.svelte'
|
||||
import { Button } from './wildcard'
|
||||
import { Param } from './Paginator'
|
||||
|
||||
export let pageInfo: {
|
||||
hasPreviousPage: boolean
|
||||
hasNextPage: boolean
|
||||
startCursor: string | null
|
||||
endCursor: string | null
|
||||
}
|
||||
export let disabled: boolean
|
||||
type PageInfo =
|
||||
// Bidirection pagination
|
||||
| { hasPreviousPage: boolean; hasNextPage: boolean; startCursor: string | null; endCursor: string | null }
|
||||
// Unidirection pagination
|
||||
| {
|
||||
hasNextPage: boolean
|
||||
hasPreviousPage: boolean
|
||||
endCursor: string | null
|
||||
startCursor?: undefined
|
||||
previousEndCursor: string | null
|
||||
}
|
||||
|
||||
export let pageInfo: PageInfo
|
||||
export let disabled: boolean = false
|
||||
export let showLastpageButton: boolean = true
|
||||
|
||||
function urlWithParameter(name: string, value: string | null): string {
|
||||
const url = new URL($page.url)
|
||||
@ -59,14 +43,18 @@
|
||||
|
||||
let firstPageURL = urlWithParameter('', null)
|
||||
let lastPageURL = urlWithParameter(Param.last, '')
|
||||
$: previousPageURL = urlWithParameter(Param.before, pageInfo.startCursor)
|
||||
$: previousPageURL =
|
||||
pageInfo.startCursor !== undefined
|
||||
? urlWithParameter(Param.before, pageInfo.startCursor)
|
||||
: urlWithParameter(Param.after, pageInfo.previousEndCursor)
|
||||
$: nextPageURL = urlWithParameter(Param.after, pageInfo.endCursor)
|
||||
$: firstAndPreviousDisabled = disabled || !pageInfo.hasPreviousPage
|
||||
$: nextAndLastDisabled = disabled || !pageInfo.hasNextPage
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- The event handler is used for event delegation -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- 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}>
|
||||
@ -89,11 +77,13 @@
|
||||
Next <Icon svgPath={mdiChevronRight} inline />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="secondary" outline>
|
||||
<a slot="custom" let:className class={className} href={lastPageURL} aria-disabled={nextAndLastDisabled}>
|
||||
<Icon svgPath={mdiPageLast} inline />
|
||||
</a>
|
||||
</Button>
|
||||
{#if showLastpageButton}
|
||||
<Button variant="secondary" outline>
|
||||
<a slot="custom" let:className class={className} href={lastPageURL} aria-disabled={nextAndLastDisabled}>
|
||||
<Icon svgPath={mdiPageLast} inline />
|
||||
</a>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
23
client/web-sveltekit/src/lib/Paginator.ts
Normal file
23
client/web-sveltekit/src/lib/Paginator.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export enum Param {
|
||||
before = '$before',
|
||||
after = '$after',
|
||||
last = '$last',
|
||||
}
|
||||
|
||||
export function getPaginationParams(
|
||||
searchParams: URLSearchParams,
|
||||
pageSize: number
|
||||
):
|
||||
| { first: number; last: null; before: null; after: string | null }
|
||||
| { first: null; last: number; before: string | null; after: null } {
|
||||
if (searchParams.has('$before')) {
|
||||
return { first: null, last: pageSize, before: searchParams.get(Param.before), after: null }
|
||||
}
|
||||
if (searchParams.has('$after')) {
|
||||
return { first: pageSize, last: null, before: null, after: searchParams.get(Param.after) }
|
||||
}
|
||||
if (searchParams.has('$last')) {
|
||||
return { first: null, last: pageSize, before: null, after: null }
|
||||
}
|
||||
return { first: pageSize, last: null, before: null, after: null }
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { query, gql, type NodeFromResult } from '$lib/graphql'
|
||||
import type { BlobResult, BlobVariables, HighlightResult, HighlightVariables, Scalars } from '$lib/graphql-operations'
|
||||
|
||||
interface FetchBlobOptions {
|
||||
repoID: Scalars['ID']
|
||||
repoID: Scalars['ID']['input']
|
||||
commitID: string
|
||||
filePath: string
|
||||
disableTimeout?: boolean
|
||||
|
||||
@ -174,7 +174,7 @@ const COMMIT_QUERY = gql`
|
||||
`
|
||||
|
||||
interface FetchRepoCommitsArgs {
|
||||
repoID: Scalars['ID']
|
||||
repoID: Scalars['ID']['input']
|
||||
revision: string
|
||||
filePath?: string
|
||||
first?: number
|
||||
@ -213,7 +213,7 @@ 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']
|
||||
repo: Scalars['ID']['input']
|
||||
base: string | null
|
||||
head: string | null
|
||||
first: number | null
|
||||
@ -270,7 +270,7 @@ export async function queryRepositoryComparisonFileDiffs(args: {
|
||||
}
|
||||
|
||||
export async function fetchDiff(
|
||||
repoID: Scalars['ID'],
|
||||
repoID: Scalars['ID']['input'],
|
||||
revspec: string,
|
||||
paths: string[] = []
|
||||
): Promise<FileDiffFields[]> {
|
||||
|
||||
@ -75,7 +75,7 @@ export const REPOSITORY_GIT_REFS = gql`
|
||||
`
|
||||
|
||||
export async function queryGitReferences(args: {
|
||||
repo: Scalars['ID']
|
||||
repo: Scalars['ID']['input']
|
||||
first?: number
|
||||
query?: string
|
||||
type: GitRefType
|
||||
@ -102,7 +102,7 @@ interface Data {
|
||||
hasMoreActiveBranches: boolean
|
||||
}
|
||||
|
||||
export async function queryGitBranchesOverview(args: { repo: Scalars['ID']; first: number }): Promise<Data> {
|
||||
export async function queryGitBranchesOverview(args: { repo: Scalars['ID']['input']; first: number }): Promise<Data> {
|
||||
const data = await query<RepositoryGitBranchesOverviewResult, RepositoryGitBranchesOverviewVariables>(
|
||||
gql`
|
||||
query RepositoryGitBranchesOverview($repo: ID!, $first: Int!, $withBehindAhead: Boolean!) {
|
||||
|
||||
@ -89,7 +89,7 @@ export async function fetchSidebarFileTree({
|
||||
commitID,
|
||||
filePath,
|
||||
}: {
|
||||
repoID: Scalars['ID']
|
||||
repoID: Scalars['ID']['input']
|
||||
commitID: string
|
||||
filePath: string
|
||||
}): Promise<{ root: TreeRoot; values: FileTreeNodeValue[] }> {
|
||||
@ -111,7 +111,7 @@ export async function fetchSidebarFileTree({
|
||||
}
|
||||
|
||||
export type FileTreeLoader = (args: {
|
||||
repoID: Scalars['ID']
|
||||
repoID: Scalars['ID']['input']
|
||||
commitID: string
|
||||
filePath: string
|
||||
parent?: FileTreeProvider
|
||||
@ -120,7 +120,7 @@ export type FileTreeLoader = (args: {
|
||||
interface FileTreeProviderArgs {
|
||||
root: NonNullable<GitCommitFieldsWithTree['tree']>
|
||||
values: FileTreeNodeValue[]
|
||||
repoID: Scalars['ID']
|
||||
repoID: Scalars['ID']['input']
|
||||
commitID: string
|
||||
loader: FileTreeLoader
|
||||
parent?: TreeProvider<FileTreeNodeValue>
|
||||
@ -133,7 +133,7 @@ export class FileTreeProvider implements TreeProvider<FileTreeNodeValue> {
|
||||
return this.args.root
|
||||
}
|
||||
|
||||
getRepoID(): Scalars['ID'] {
|
||||
getRepoID(): Scalars['ID']['input'] {
|
||||
return this.args.repoID
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
)
|
||||
let treeProvider: FileTreeProvider | null = null
|
||||
|
||||
async function updateFileTreeProvider(repoID: Scalars['ID'], commitID: string, parentPath: string) {
|
||||
async function updateFileTreeProvider(repoID: Scalars['ID']['input'], commitID: string, parentPath: string) {
|
||||
const result = await data.deferred.fileTree
|
||||
if (!result) {
|
||||
treeProvider = null
|
||||
|
||||
@ -3,11 +3,22 @@
|
||||
import { createPromiseStore } from '$lib/utils'
|
||||
|
||||
import type { PageData } from './$types'
|
||||
import type { Commits } from './page.gql'
|
||||
import Paginator from '$lib/Paginator.svelte'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
|
||||
export let data: PageData
|
||||
|
||||
const { pending, value: commits, set } = createPromiseStore<PageData['deferred']['commits']>()
|
||||
const { pending, latestValue: commits, set } = createPromiseStore<Promise<Commits>>()
|
||||
$: set(data.deferred.commits)
|
||||
|
||||
// This is a hack to make backword pagination work. It looks like the cursor
|
||||
// for the commits connection is simply a counter. So if it's > 0 we know that
|
||||
// there are are previous pages. We just need to take the page size into account.
|
||||
const PAGE_SIZE = 20
|
||||
$: cursor = $commits?.pageInfo.endCursor ? +$commits.pageInfo.endCursor : null
|
||||
$: hasPreviousPage = cursor !== null && cursor > PAGE_SIZE
|
||||
$: previousEndCursor = String(cursor === null ? 0 : cursor - PAGE_SIZE - PAGE_SIZE)
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@ -15,39 +26,75 @@
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<div>
|
||||
<h2>View commits from this repsitory</h2>
|
||||
<h3>Changes</h3>
|
||||
{#if $pending}
|
||||
Loading...
|
||||
{:else if $commits}
|
||||
{#if $pending && !$commits}
|
||||
<div class="loader">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else if $commits}
|
||||
<div class="commits">
|
||||
<ul>
|
||||
{#each $commits as commit (commit.canonicalURL)}
|
||||
{#each $commits.nodes as commit (commit.canonicalURL)}
|
||||
<li><Commit {commit} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="paginator">
|
||||
<Paginator
|
||||
disabled={$pending}
|
||||
pageInfo={{
|
||||
...$commits.pageInfo,
|
||||
hasPreviousPage,
|
||||
previousEndCursor,
|
||||
}}
|
||||
showLastpageButton={false}
|
||||
/>
|
||||
<div class="loader" class:visible={$pending}>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
> .loader {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.commits {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
max-width: var(--viewport-xl);
|
||||
margin: 0 auto;
|
||||
--avatar-size: 2.5rem;
|
||||
}
|
||||
|
||||
section {
|
||||
overflow: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.paginator {
|
||||
flex: 0 0 auto;
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
max-width: 54rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
.loader {
|
||||
margin-left: 1rem;
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
@ -1,15 +1,35 @@
|
||||
import { fetchRepoCommits } from '$lib/repo/api/commits'
|
||||
import { getPaginationParams } from '$lib/Paginator'
|
||||
|
||||
import type { PageLoad } from './$types'
|
||||
import { CommitsQuery } from './page.gql'
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { resolvedRevision } = await parent()
|
||||
const pageSize = 20
|
||||
|
||||
export const load: PageLoad = async ({ parent, url }) => {
|
||||
const { resolvedRevision, graphqlClient } = await parent()
|
||||
const { first, after } = getPaginationParams(url.searchParams, pageSize)
|
||||
|
||||
return {
|
||||
deferred: {
|
||||
commits: fetchRepoCommits({ repoID: resolvedRevision.repo.id, revision: resolvedRevision.commitID }).then(
|
||||
result => result?.nodes ?? []
|
||||
),
|
||||
commits: graphqlClient
|
||||
.query({
|
||||
query: CommitsQuery,
|
||||
variables: {
|
||||
repo: resolvedRevision.repo.id,
|
||||
revspec: resolvedRevision.commitID,
|
||||
first,
|
||||
afterCursor: after,
|
||||
},
|
||||
})
|
||||
.then(result => {
|
||||
if (result.data.node?.__typename !== 'Repository') {
|
||||
throw new Error('Unable to find repository')
|
||||
}
|
||||
if (!result.data.node.commit) {
|
||||
throw new Error('Unable to find commit')
|
||||
}
|
||||
return result.data.node.commit.ancestors
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
#import "$lib/Commit.gql"
|
||||
|
||||
fragment Commits on GitCommitConnection {
|
||||
nodes {
|
||||
...Commit
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
|
||||
query CommitsQuery($repo: ID!, $revspec: String!, $first: Int, $afterCursor: String) {
|
||||
node(id: $repo) {
|
||||
__typename
|
||||
... on Repository {
|
||||
id
|
||||
commit(rev: $revspec) {
|
||||
id
|
||||
ancestors(first: $first, afterCursor: $afterCursor) {
|
||||
...Commits
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { getPaginationParams } from '$lib/Paginator.svelte'
|
||||
import { getPaginationParams } from '$lib/Paginator'
|
||||
import { fetchContributors } from '$lib/repo/api/contributors'
|
||||
|
||||
import type { PageLoad } from './$types'
|
||||
|
||||
@ -1,84 +1,129 @@
|
||||
import { dirname, join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { generate, type CodegenConfig } from '@graphql-codegen/cli'
|
||||
import graphql from '@rollup/plugin-graphql'
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import { defineConfig, mergeConfig, type Plugin, type UserConfig } from 'vite'
|
||||
import inspect from 'vite-plugin-inspect'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function generateGraphQLOperations(): Promise<Plugin> {
|
||||
const outputPath = './src/lib/graphql-operations.ts'
|
||||
const documents = ['src/lib/**/*.{ts,graphql}', '!src/lib/graphql-operations.ts']
|
||||
|
||||
// We have to dynamically import this module to not make it a dependency when using
|
||||
// Bazel
|
||||
const codegen = (await import('vite-plugin-graphql-codegen')).default
|
||||
|
||||
return codegen({
|
||||
// Keep in sync with client/shared/dev/generateGraphQlOperations.ts
|
||||
config: {
|
||||
generates: {
|
||||
[outputPath]: {
|
||||
documents: 'src/lib/**/*.{ts,graphql}',
|
||||
config: {
|
||||
onlyOperationTypes: true,
|
||||
noExport: false,
|
||||
enumValues: '@sourcegraph/shared/src/graphql-operations',
|
||||
interfaceNameForOperations: 'SvelteKitGraphQlOperations',
|
||||
},
|
||||
plugins: [
|
||||
'../shared/dev/extractGraphQlOperationCodegenPlugin.js',
|
||||
'typescript',
|
||||
'typescript-operations',
|
||||
],
|
||||
// Generates typescript types for gql-tags and .graphql files
|
||||
// We don't use vite-plugin-graphql-codegen because it doesn't support watch mode
|
||||
// when documents are defined separately for every generated file.
|
||||
// Defining a single set of documents at the top level doesn't work either because
|
||||
// it would generated unnecessary files (e.g. .qql.d.ts files for .ts file) and also
|
||||
// caused duplicate code generation issues.
|
||||
function generateGraphQLTypes(): Plugin {
|
||||
const codgegenConfig: CodegenConfig = {
|
||||
generates: {
|
||||
'./src/lib/graphql-operations.ts': {
|
||||
documents: ['src/{lib,routes}/**/*.ts', '!src/lib/graphql-{operations,types}.ts'],
|
||||
config: {
|
||||
onlyOperationTypes: true,
|
||||
enumValues: '$lib/graphql-types.ts',
|
||||
//interfaceNameForOperations: 'SvelteKitGraphQlOperations',
|
||||
},
|
||||
plugins: ['typescript', 'typescript-operations'],
|
||||
},
|
||||
schema: '../../cmd/frontend/graphqlbackend/*.graphql',
|
||||
errorsOnly: true,
|
||||
silent: true,
|
||||
config: {
|
||||
// https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-operations#config-api-reference
|
||||
arrayInputCoercion: false,
|
||||
preResolveTypes: true,
|
||||
operationResultSuffix: 'Result',
|
||||
omitOperationSuffix: true,
|
||||
namingConvention: {
|
||||
typeNames: 'keep',
|
||||
enumValues: 'keep',
|
||||
transformUnderscore: true,
|
||||
},
|
||||
declarationKind: 'interface',
|
||||
avoidOptionals: {
|
||||
field: true,
|
||||
inputValue: false,
|
||||
object: true,
|
||||
},
|
||||
scalars: {
|
||||
DateTime: 'string',
|
||||
JSON: 'object',
|
||||
JSONValue: 'unknown',
|
||||
GitObjectID: 'string',
|
||||
JSONCString: 'string',
|
||||
PublishedValue: "boolean | 'draft'",
|
||||
BigInt: 'string',
|
||||
},
|
||||
'src/lib/graphql-types.ts': {
|
||||
plugins: ['typescript'],
|
||||
},
|
||||
'src/': {
|
||||
documents: ['src/**/*.gql', '!src/**/*.gql.d.ts'],
|
||||
preset: 'near-operation-file',
|
||||
presetConfig: {
|
||||
baseTypesPath: 'lib/graphql-types.ts',
|
||||
extension: '.gql.d.ts',
|
||||
},
|
||||
config: {
|
||||
useTypeImports: true,
|
||||
},
|
||||
plugins: ['typescript-operations', `${__dirname}/dev/typed-document-node.cjs`],
|
||||
},
|
||||
// Top-level documents needs to be expliclity configured, otherwise vite-plugin-graphql-codgen
|
||||
// won't regenerate on change.
|
||||
documents,
|
||||
},
|
||||
})
|
||||
schema: '../../cmd/frontend/graphqlbackend/*.graphql',
|
||||
errorsOnly: true,
|
||||
config: {
|
||||
// https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-operations#config-api-reference
|
||||
arrayInputCoercion: false,
|
||||
preResolveTypes: true,
|
||||
operationResultSuffix: 'Result',
|
||||
omitOperationSuffix: true,
|
||||
namingConvention: {
|
||||
typeNames: 'keep',
|
||||
enumValues: 'keep',
|
||||
transformUnderscore: true,
|
||||
},
|
||||
declarationKind: 'interface',
|
||||
avoidOptionals: {
|
||||
field: true,
|
||||
inputValue: false,
|
||||
object: true,
|
||||
},
|
||||
scalars: {
|
||||
DateTime: 'string',
|
||||
JSON: 'object',
|
||||
JSONValue: 'unknown',
|
||||
GitObjectID: 'string',
|
||||
JSONCString: 'string',
|
||||
PublishedValue: "boolean | 'draft'",
|
||||
BigInt: 'string',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Cheap custom function to check whether we should run codegen for the provided path
|
||||
function shouldRunCodegen(path: string): boolean {
|
||||
// Do not run codegen for generated files
|
||||
if (/(graphql-(operations|types)|\.gql\.d)\.ts$/.test(path)) {
|
||||
return false
|
||||
}
|
||||
if (/\.(ts|gql)$/.test(path)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function codegen(): Promise<void> {
|
||||
try {
|
||||
await generate(codgegenConfig, true)
|
||||
} catch {
|
||||
// generate already logs errors to the console
|
||||
// but we still need to catch it otherwise vite will terminate
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'graphql-codegen',
|
||||
buildStart() {
|
||||
return codegen()
|
||||
},
|
||||
configureServer(server) {
|
||||
server.watcher.on('add', path => {
|
||||
if (shouldRunCodegen(path)) {
|
||||
codegen()
|
||||
}
|
||||
})
|
||||
server.watcher.on('change', path => {
|
||||
if (shouldRunCodegen(path)) {
|
||||
codegen()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
let config: UserConfig = {
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
// When using bazel the graphql operations fiel is generated
|
||||
// by bazel targets
|
||||
process.env.BAZEL ? null : generateGraphQLOperations(),
|
||||
// Generates typescript types for gql-tags and .graphql files
|
||||
generateGraphQLTypes(),
|
||||
inspect(),
|
||||
// Parses .graphql files and imports them as AST
|
||||
graphql(),
|
||||
],
|
||||
define:
|
||||
mode === 'test'
|
||||
|
||||
@ -16,6 +16,7 @@ js_library(
|
||||
visibility = [
|
||||
"//client/backstage-backend/node_modules/@sourcegraph/shared/dev:__pkg__",
|
||||
"//client/shared/dev:__pkg__",
|
||||
"//client/web-sveltekit:__pkg__",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@ -441,6 +441,13 @@
|
||||
"dependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"@graphql-codegen/cli@5": {
|
||||
"peerDependencies": {
|
||||
"@graphql-codegen/typescript": "*",
|
||||
"@graphql-codegen/typescript-operations": "*",
|
||||
"@graphql-codegen/near-operation-file-preset": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
807
pnpm-lock.yaml
807
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user