mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3117b03be9 | ||
|
|
9cf00da25a | ||
|
|
6b8d334563 | ||
|
|
21247e44ac | ||
|
|
20adc60d67 | ||
|
|
308624f144 | ||
|
|
174c08c8c2 | ||
|
|
8ee41490b9 | ||
|
|
344169fd47 |
@ -59,4 +59,9 @@ test --build_event_binary_file_path_conversion=false
|
||||
test --build_event_binary_file_upload_mode=wait_for_upload_complete
|
||||
test --build_event_publish_all_actions=true
|
||||
|
||||
build --experimental_execution_log_compact_file=execution_log.zstd
|
||||
test --experimental_execution_log_compact_file=execution_log.zstd
|
||||
|
||||
# These likely perform faster locally than the overhead of pulling/pushing from/to the remote cache,
|
||||
# as well as being able to reduce how much we push to the cache
|
||||
common --modify_execution_info=CopyDirectory=+no-remote,CopyToDirectory=+no-remote,CopyFile=+no-remote
|
||||
|
||||
@ -26,7 +26,6 @@ client/web/node_modules
|
||||
client/web-sveltekit/node_modules
|
||||
client/wildcard/node_modules
|
||||
internal/appliance/frontend/maintenance/node_modules
|
||||
internal/openapi/node_modules
|
||||
|
||||
cmd/symbols/internal/squirrel/test_repos/starlark
|
||||
|
||||
|
||||
@ -30,8 +30,6 @@ const config = {
|
||||
'typedoc.js',
|
||||
'client/web/dev/**/*',
|
||||
'graphql-schema-linter.config.js',
|
||||
// Generated code
|
||||
'client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/**',
|
||||
],
|
||||
extends: ['@sourcegraph/eslint-config', 'plugin:storybook/recommended'],
|
||||
env: {
|
||||
@ -94,7 +92,6 @@ const config = {
|
||||
'@typescript-eslint/no-unused-vars': 'off', // also duplicated by tsconfig noUnused{Locals,Parameters}
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'etc/no-deprecated': 'off',
|
||||
|
||||
'no-restricted-imports': [
|
||||
@ -114,6 +111,10 @@ const config = {
|
||||
importNames: ['Link'],
|
||||
message: 'Use the <Link /> component from @sourcegraph/wildcard instead.',
|
||||
},
|
||||
{
|
||||
name: 'chromatic/isChromatic',
|
||||
message: 'Please use `isChromatic` from the `@sourcegraph/storybook` package.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
|
||||
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@ -1,6 +0,0 @@
|
||||
# Sourcegraph uses CODENOTIFY to make individuals or groups aware of changes that are happening in code they care about,
|
||||
# without explicitly requiring those engineers to "own" the code.
|
||||
# This file is meant to protect critical code from accidental changes, and should be used sparsingly to prevent slowdowns
|
||||
# from code reviews.
|
||||
|
||||
/internal/tenant/ @sourcegraph/multi-tenant
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,12 +1,11 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report problems and unexpected behavior (Code Search ONLY)
|
||||
about: Report problems and unexpected behavior
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Please submit all feedback or bug reports for Cody in the [VS Code](https://github.com/sourcegraph/cody) or [JetBrains](https://github.com/sourcegraph/jetbrains) repo. -->
|
||||
|
||||
- **Sourcegraph version:** <!-- the version of Sourcegraph or "Sourcegraph.com" -->
|
||||
- **Platform information:** <!-- OS version, cloud provider, web browser version, Docker version, etc., depending on the issue -->
|
||||
|
||||
112
.github/workflows/cloud-gql-compat.yml
vendored
112
.github/workflows/cloud-gql-compat.yml
vendored
@ -1,112 +0,0 @@
|
||||
# Cloud controller has tight integration with GraphQL API
|
||||
# This workflow ensures that the query/mutation the controller uses are compatible with any changes to the GraphQL schema
|
||||
#
|
||||
# Maintained by the Cloud Operation team
|
||||
|
||||
name: Cloud Controller GQL Compat Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.graphql'
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
# The GitHub App is here:
|
||||
# https://github.com/organizations/sourcegraph/settings/apps/cloud-srcgql-compat-test-invoker
|
||||
app-id: ${{ secrets.CLOUD_SRCGQL_COMPAT_TEST_INVOKER_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.CLOUD_SRCGQL_COMPAT_TEST_INVOKER_GITHUB_APP_PRIVATE_KEY_PEM }}
|
||||
owner: sourcegraph
|
||||
|
||||
- uses: lasith-kg/dispatch-workflow@91345a2a3b705e950978a584446ab59f7e815ae3 #v2.0.0
|
||||
id: workflow-dispatch
|
||||
with:
|
||||
dispatch-method: workflow_dispatch
|
||||
discover: true
|
||||
repo: controller
|
||||
owner: sourcegraph
|
||||
ref: main
|
||||
# using ID instead of workflow file name to avoid requring content:read permission to the repo
|
||||
# retrieve by running 'gh workflow list' in the controller repo
|
||||
workflow: 58413286 # srcgql-compat.yaml
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
workflow-inputs: |
|
||||
{
|
||||
"ref": "${{ github.sha }}",
|
||||
"upstream_pr_number": "${{ github.event.number }}"
|
||||
}
|
||||
- name: await workflow run (id:${{ steps.workflow-dispatch.outputs.run-id }})
|
||||
uses: codex-/await-remote-run@d4a6dbf57245924ff4f23e0db929b8e3ef65486b #1.12.2
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
repo: controller
|
||||
owner: sourcegraph
|
||||
run_id: ${{ steps.workflow-dispatch.outputs.run-id }}
|
||||
run_timeout_seconds: 300
|
||||
poll_interval_ms: 5000
|
||||
|
||||
- uses: actions/github-script@v7
|
||||
if: ${{ success() || failure() }}
|
||||
env:
|
||||
FAILED: ${{ job.status == 'failure' }}
|
||||
RUN_URL: https://github.com/sourcegraph/controller/actions/runs/${{ steps.workflow-dispatch.outputs.run-id }}
|
||||
with:
|
||||
script: |
|
||||
const isFailed = process.env.FAILED === 'true'
|
||||
const commentMarker = '<!-- cloud-gql-compat-test-result-marker -->'
|
||||
|
||||
let message
|
||||
if (isFailed) {
|
||||
message = `
|
||||
## :x: Cloud Controller GraphQL Compatability Test Result
|
||||
|
||||
[sourcegraph/controller](https://github.com/sourcegraph/controller) uses the GraphQL API to perform automation. The compatibility test has failed and this pull request may have introduced breaking changes to the GraphQL schema.
|
||||
|
||||
Next steps:
|
||||
- Review the GitHub Actions [workflow logs](${process.env.RUN_URL}) for more details.
|
||||
- Reach out to the Cloud Ops team to resolve the issue before merging this pull request.
|
||||
|
||||
${commentMarker}
|
||||
`
|
||||
} else {
|
||||
message = `
|
||||
## :white_check_mark: Cloud Controller GraphQL Compatability Test Result
|
||||
|
||||
[sourcegraph/controller](https://github.com/sourcegraph/controller) uses the GraphQL API to perform automation. The compatibility test has passed.
|
||||
|
||||
Learn more from [workflow logs](${process.env.RUN_URL}).
|
||||
|
||||
${commentMarker}
|
||||
`
|
||||
}
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
})
|
||||
let existingComment = comments.find(comment => comment.body.includes(commentMarker))
|
||||
if (existingComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
comment_id: existingComment.id,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: message
|
||||
})
|
||||
} else if (isFailed) {
|
||||
// we only create comment if the test failed
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: message
|
||||
})
|
||||
}
|
||||
10
.github/workflows/pr-auditor.yml
vendored
10
.github/workflows/pr-auditor.yml
vendored
@ -1,21 +1,21 @@
|
||||
# See https://docs.sourcegraph.com/dev/background-information/ci#pr-auditor
|
||||
name: pr-auditor
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ closed, edited, opened, synchronize, ready_for_review ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'sourcegraph/devx-service'
|
||||
token: ${{ secrets.PR_AUDITOR_TOKEN }}
|
||||
repository: 'sourcegraph/pr-auditor'
|
||||
- uses: actions/setup-go@v4
|
||||
with: { go-version: '1.22' }
|
||||
|
||||
- run: 'go run ./cmd/pr-auditor'
|
||||
- run: './check-pr.sh'
|
||||
env:
|
||||
GITHUB_EVENT_PATH: ${{ env.GITHUB_EVENT_PATH }}
|
||||
GITHUB_TOKEN: ${{ secrets.PR_AUDITOR_TOKEN }}
|
||||
|
||||
@ -70,6 +70,3 @@ dev/linearhooks/internal/lineargql/schema.graphql
|
||||
# This is an embedded external minified library and should not be modified
|
||||
internal/appliance/web/static/script/htmx.min.js
|
||||
internal/appliance/web/static/script/bootstrap.bundle.min.js
|
||||
|
||||
# Generated code
|
||||
client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/**
|
||||
|
||||
14
BUILD.bazel
14
BUILD.bazel
@ -1,4 +1,4 @@
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "bool_setting")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo")
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
@ -271,6 +271,17 @@ go_proto_compiler(
|
||||
],
|
||||
)
|
||||
|
||||
# Settings for automatic building of frontend/single-server without client bundle included
|
||||
bool_setting(
|
||||
name = "integration_testing",
|
||||
build_setting_default = False,
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "integration_testing_enabled",
|
||||
flag_values = {":integration_testing": "true"},
|
||||
)
|
||||
|
||||
# nogo config
|
||||
#
|
||||
# For nogo to be able to run a linter, it needs to have `var Analyzer analysis.Analyzer` defined in the main package.
|
||||
@ -296,7 +307,6 @@ nogo(
|
||||
"//conditions:default": [
|
||||
"//dev/linters/bodyclose",
|
||||
"//dev/linters/depguard",
|
||||
"//dev/linters/exhaustruct",
|
||||
"//dev/linters/forbidigo",
|
||||
"//dev/linters/gocheckcompilerdirectives",
|
||||
"//dev/linters/gocritic",
|
||||
|
||||
@ -37,7 +37,6 @@ All notable changes to Sourcegraph are documented in this file.
|
||||
|
||||
- The default and recommended chat model for Anthropic and Cody Gateway configurations is now `claude-3-sonnet-20240229`. [#62757](https://github.com/sourcegraph/sourcegraph/pull/62757)
|
||||
- The default and recommended autocomplete model for Cody Gateway configurations is now `fireworks/starcoder`. [#62757](https://github.com/sourcegraph/sourcegraph/pull/62757)
|
||||
- Code Insights: Language Stats Insights performance improved by another 70-90%. It's now able to handle repositories above 40 GB. [#62946](https://github.com/sourcegraph/sourcegraph/pull/62946)
|
||||
- The keyword search toggle has been removed from the search results page. [Keyword search](https://sourcegraph.com/docs/code-search/queries#keyword-search-default) is now enabled by default for all searches in the Sourcegraph web app. [#63584](https://github.com/sourcegraph/sourcegraph/pull/63584)
|
||||
|
||||
### Fixed
|
||||
|
||||
42
WORKSPACE
42
WORKSPACE
@ -28,30 +28,30 @@ bazel_skylib_workspace()
|
||||
|
||||
http_archive(
|
||||
name = "aspect_bazel_lib",
|
||||
sha256 = "c780120ab99a4ca9daac69911eb06434b297214743ee7e0a1f1298353ef686db",
|
||||
strip_prefix = "bazel-lib-2.7.9",
|
||||
url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.7.9/bazel-lib-v2.7.9.tar.gz",
|
||||
sha256 = "6d758a8f646ecee7a3e294fbe4386daafbe0e5966723009c290d493f227c390b",
|
||||
strip_prefix = "bazel-lib-2.7.7",
|
||||
url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.7.7/bazel-lib-v2.7.7.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "aspect_rules_js",
|
||||
sha256 = "f8536470864c91f91c83aea91de9a27607ca5e6d8a9fcdd56132cf422c6b7b56",
|
||||
strip_prefix = "rules_js-2.0.0-rc9",
|
||||
url = "https://github.com/aspect-build/rules_js/releases/download/v2.0.0-rc9/rules_js-v2.0.0-rc9.tar.gz",
|
||||
sha256 = "3bad4ab669d4d38d0d137275b946a46ce6f8f17fecc6c7affba64966a9054246",
|
||||
strip_prefix = "rules_js-2.0.0-rc5",
|
||||
url = "https://github.com/aspect-build/rules_js/releases/download/v2.0.0-rc5/rules_js-v2.0.0-rc5.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "aspect_rules_ts",
|
||||
sha256 = "1d745fd7a5ffdb5bb7c0b77b36b91409a5933c0cbe25af32b05d90e26b7d14a7",
|
||||
strip_prefix = "rules_ts-3.0.0-rc2",
|
||||
url = "https://github.com/aspect-build/rules_ts/releases/download/v3.0.0-rc2/rules_ts-v3.0.0-rc2.tar.gz",
|
||||
sha256 = "3ea5cdb825d5dbffe286b3d9c5197a2648cf04b5e6bd8b913a45823cdf0ae960",
|
||||
strip_prefix = "rules_ts-3.0.0-rc0",
|
||||
url = "https://github.com/aspect-build/rules_ts/releases/download/v3.0.0-rc0/rules_ts-v3.0.0-rc0.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "aspect_rules_swc",
|
||||
sha256 = "0c2e8912725a1d97a37bb751777c9846783758f5a0a8e996f1b9d21cad42e839",
|
||||
strip_prefix = "rules_swc-2.0.0-rc1",
|
||||
url = "https://github.com/aspect-build/rules_swc/releases/download/v2.0.0-rc1/rules_swc-v2.0.0-rc1.tar.gz",
|
||||
sha256 = "c085647585c3d01bee3966eb9ba433a1efbb0ee79bb1b8c67882a81d82a9b37f",
|
||||
strip_prefix = "rules_swc-2.0.0-rc0",
|
||||
url = "https://github.com/aspect-build/rules_swc/releases/download/v2.0.0-rc0/rules_swc-v2.0.0-rc0.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
@ -106,9 +106,9 @@ http_archive(
|
||||
# Container rules
|
||||
http_archive(
|
||||
name = "rules_oci",
|
||||
sha256 = "311e78803a4161688cc79679c0fb95c56445a893868320a3caf174ff6e2c383b",
|
||||
strip_prefix = "rules_oci-2.0.0-beta2",
|
||||
url = "https://github.com/bazel-contrib/rules_oci/releases/download/v2.0.0-beta2/rules_oci-v2.0.0-beta2.tar.gz",
|
||||
sha256 = "647f4c6fd092dc7a86a7f79892d4b1b7f1de288bdb4829ca38f74fd430fcd2fe",
|
||||
strip_prefix = "rules_oci-1.7.6",
|
||||
url = "https://github.com/bazel-contrib/rules_oci/releases/download/v1.7.6/rules_oci-v1.7.6.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
@ -407,9 +407,15 @@ load("@rules_oci//oci:dependencies.bzl", "rules_oci_dependencies")
|
||||
|
||||
rules_oci_dependencies()
|
||||
|
||||
load("@rules_oci//oci:repositories.bzl", "oci_register_toolchains")
|
||||
load("@rules_oci//oci:repositories.bzl", "LATEST_CRANE_VERSION", "oci_register_toolchains")
|
||||
|
||||
oci_register_toolchains(name = "oci")
|
||||
oci_register_toolchains(
|
||||
name = "oci",
|
||||
crane_version = LATEST_CRANE_VERSION,
|
||||
# Uncommenting the zot toolchain will cause it to be used instead of crane for some tasks.
|
||||
# Note that it does not support docker-format images.
|
||||
# zot_version = LATEST_ZOT_VERSION,
|
||||
)
|
||||
|
||||
# Optional, for oci_tarball rule
|
||||
load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
|
||||
@ -495,7 +501,7 @@ load("//dev:schema_migrations.bzl", "schema_migrations")
|
||||
|
||||
schema_migrations(
|
||||
name = "schemas_migrations",
|
||||
updated_at = "2024-08-07 19:10",
|
||||
updated_at = "2024-07-10 23:24",
|
||||
)
|
||||
|
||||
# wolfi images setup ================================
|
||||
|
||||
@ -36,6 +36,12 @@ export const Interactive: StoryFn = () => {
|
||||
return <ToggleExample value={value} onToggle={onToggle} />
|
||||
}
|
||||
|
||||
Interactive.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Variants: StoryFn = () => (
|
||||
<>
|
||||
<ToggleExample value={true} onToggle={onToggle} />
|
||||
|
||||
@ -13,7 +13,11 @@ const decorator: Decorator = story => (
|
||||
const config: Meta = {
|
||||
title: 'branded/TabbedPanelContent',
|
||||
decorators: [decorator],
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: {
|
||||
viewports: [320, 576, 978, 1440],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -7,7 +7,9 @@ import { type RepoMetadataItem, RepoMetadata } from './RepoMetadata'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/RepoMetadata',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { viewports: [480] },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@ -48,3 +50,8 @@ export const RepoMetadataStory: StoryFn = () => (
|
||||
)
|
||||
|
||||
RepoMetadataStory.storyName = 'RepoMetadata'
|
||||
RepoMetadataStory.parameters = {
|
||||
chromatic: {
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ import { SyntaxHighlightedSearchQuery } from './SyntaxHighlightedSearchQuery'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/SyntaxHighlightedSearchQuery',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { viewports: [480] },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -10,7 +10,9 @@ import { BaseCodeMirrorQueryInput, type BaseCodeMirrorQueryInputProps } from './
|
||||
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/input/BaseCodeMirrorQueryInput',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { viewports: [500] },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -16,7 +16,9 @@ import { SearchBox, type SearchBoxProps } from './SearchBox'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/input/SearchBox',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { viewports: [575, 700], disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -22,6 +22,7 @@ const decorator: Decorator = story => (
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/input/SearchContextMenu',
|
||||
parameters: {
|
||||
chromatic: { viewports: [500], disableSnapshot: false },
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/4Fy9rURbfF2bsl4BvYunUO/RFC-261-Search-Contexts?node-id=581%3A4754',
|
||||
|
||||
@ -13,7 +13,9 @@ const decorator: Decorator = story => (
|
||||
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/input/SearchContextMenuItem',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { viewports: [1200], disableSnapshot: false },
|
||||
},
|
||||
decorators: [decorator],
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@ const decorator: Decorator = story => <BrandedStory>{props => story()}</BrandedS
|
||||
const config: Meta = {
|
||||
title: 'branded/search-ui/filters',
|
||||
decorators: [decorator],
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { viewports: [575, 700] },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -15,6 +15,7 @@ const config: Meta = {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/IyiXZIbPHK447NCXov0AvK/13928-Streaming-search?node-id=280%3A17768',
|
||||
},
|
||||
chromatic: { viewports: [1200], disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ const config: Meta = {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/IyiXZIbPHK447NCXov0AvK/13928-Streaming-search?node-id=280%3A17768',
|
||||
},
|
||||
chromatic: { viewports: [350], disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -356,7 +356,7 @@ export const ExhaustiveSearchMessage: FC<ExhaustiveSearchMessageProps> = props =
|
||||
if (!validationLoading) {
|
||||
telemetryService.log('SearchJobsSearchFormShown', { validState }, { validState })
|
||||
telemetryRecorder.recordEvent('search.exhaustiveJobs', 'view', {
|
||||
metadata: { validState: validState === 'valid' ? 1 : 0 },
|
||||
metadata: { validState: validState ? 1 : 0 },
|
||||
})
|
||||
}
|
||||
}, [telemetryService, telemetryRecorder, validationError, validationLoading])
|
||||
|
||||
@ -8,7 +8,12 @@ import brandedStyles from '../../branded.scss'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'browser/AfterInstallPage',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -162,3 +162,10 @@ AllOptionsPages.args = {
|
||||
version: '0.0.0',
|
||||
showSourcegraphComAlert: false,
|
||||
}
|
||||
|
||||
AllOptionsPages.parameters = {
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
@ -52,8 +52,8 @@ describe('GitHub', () => {
|
||||
})
|
||||
|
||||
testContext.overrideGraphQL({
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
ViewerConfiguration: () => ({
|
||||
viewerConfiguration: {
|
||||
subjects: [],
|
||||
merged: { contents: '', messages: [] },
|
||||
},
|
||||
@ -158,8 +158,8 @@ describe('GitHub', () => {
|
||||
// extensions: extensionSettings,
|
||||
// }
|
||||
// testContext.overrideGraphQL({
|
||||
// ViewerSettings: () => ({
|
||||
// viewerSettings: {
|
||||
// ViewerConfiguration: () => ({
|
||||
// viewerConfiguration: {
|
||||
// subjects: [
|
||||
// {
|
||||
// __typename: 'User',
|
||||
@ -321,8 +321,8 @@ describe('GitHub', () => {
|
||||
extensions: extensionSettings,
|
||||
}
|
||||
testContext.overrideGraphQL({
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
ViewerConfiguration: () => ({
|
||||
viewerConfiguration: {
|
||||
subjects: [
|
||||
{
|
||||
__typename: 'User',
|
||||
@ -643,8 +643,8 @@ describe('GitHub', () => {
|
||||
extensions: extensionSettings,
|
||||
}
|
||||
testContext.overrideGraphQL({
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
ViewerConfiguration: () => ({
|
||||
viewerConfiguration: {
|
||||
subjects: [
|
||||
{
|
||||
__typename: 'User',
|
||||
|
||||
@ -47,8 +47,8 @@ describe('GitLab', () => {
|
||||
})
|
||||
|
||||
testContext.overrideGraphQL({
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
ViewerConfiguration: () => ({
|
||||
viewerConfiguration: {
|
||||
subjects: [],
|
||||
merged: { contents: '', messages: [] },
|
||||
},
|
||||
@ -152,8 +152,8 @@ describe('GitLab', () => {
|
||||
extensions: extensionSettings,
|
||||
}
|
||||
testContext.overrideGraphQL({
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
ViewerConfiguration: () => ({
|
||||
viewerConfiguration: {
|
||||
subjects: [
|
||||
{
|
||||
__typename: 'User',
|
||||
|
||||
@ -28,7 +28,12 @@ const config: Meta = {
|
||||
// it uses the browser extension styles and bitbucket CSS module styles.
|
||||
title: 'shared/HoverOverlay',
|
||||
decorators: [decorator],
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from '@sourcegraph/shared/src/settings/settings'
|
||||
|
||||
import { observeStorageKey, storage } from '../../browser-extension/web-extension-api/storage'
|
||||
import type { ViewerSettingsResult } from '../../graphql-operations'
|
||||
import type { ViewerConfigurationResult } from '../../graphql-operations'
|
||||
import { isInPage } from '../context'
|
||||
|
||||
const inPageClientSettingsKey = 'sourcegraphClientSettings'
|
||||
@ -35,8 +35,7 @@ function observeLocalStorageKey(key: string, defaultValue: string): Observable<s
|
||||
}
|
||||
|
||||
const createStorageSettingsCascade: () => Observable<SettingsCascade> = () => {
|
||||
/**
|
||||
* Observable of the JSONC string of the settings.
|
||||
/** Observable of the JSONC string of the settings.
|
||||
*
|
||||
* NOTE: We can't use LocalStorageSubject here because the JSONC string is stored raw in localStorage and LocalStorageSubject also does parsing.
|
||||
* This could be changed, but users already have settings stored, so it would need a migration for little benefit.
|
||||
@ -96,8 +95,9 @@ export function mergeCascades(
|
||||
}
|
||||
}
|
||||
|
||||
const settingsCascadeFragment = gql`
|
||||
fragment SettingsCascadeFields on SettingsCascade {
|
||||
// This is a fragment on the DEPRECATED GraphQL API type ConfigurationCascade (not SettingsCascade) for backcompat.
|
||||
const configurationCascadeFragment = gql`
|
||||
fragment ConfigurationCascadeFields on ConfigurationCascade {
|
||||
subjects {
|
||||
__typename
|
||||
...OrgSettingFields
|
||||
@ -167,40 +167,42 @@ const settingsCascadeFragment = gql`
|
||||
|
||||
/**
|
||||
* Fetches the settings cascade for the viewer.
|
||||
*
|
||||
* TODO(sqs): This uses the DEPRECATED GraphQL Query.viewerConfiguration and ConfigurationCascade for backcompat.
|
||||
*/
|
||||
export function fetchViewerSettings(requestGraphQL: PlatformContext['requestGraphQL']): Observable<{
|
||||
final: string
|
||||
subjects: SettingsSubject[]
|
||||
}> {
|
||||
return from(
|
||||
requestGraphQL<ViewerSettingsResult>({
|
||||
requestGraphQL<ViewerConfigurationResult>({
|
||||
request: gql`
|
||||
query ViewerSettings {
|
||||
viewerSettings {
|
||||
...SettingsCascadeFields
|
||||
query ViewerConfiguration {
|
||||
viewerConfiguration {
|
||||
...ConfigurationCascadeFields
|
||||
}
|
||||
}
|
||||
${settingsCascadeFragment}
|
||||
${configurationCascadeFragment}
|
||||
`,
|
||||
variables: {},
|
||||
mightContainPrivateInfo: false,
|
||||
})
|
||||
).pipe(
|
||||
map(dataOrThrowErrors),
|
||||
map(({ viewerSettings }) => {
|
||||
if (!viewerSettings) {
|
||||
throw new Error('fetchViewerSettings: empty viewerSettings')
|
||||
map(({ viewerConfiguration }) => {
|
||||
if (!viewerConfiguration) {
|
||||
throw new Error('fetchViewerSettings: empty viewerConfiguration')
|
||||
}
|
||||
|
||||
for (const subject of viewerSettings.subjects) {
|
||||
for (const subject of viewerConfiguration.subjects) {
|
||||
// User/org/global settings cannot be edited from the
|
||||
// browser extension (only client settings can).
|
||||
subject.viewerCanAdminister = false
|
||||
}
|
||||
|
||||
return {
|
||||
subjects: viewerSettings.subjects,
|
||||
final: viewerSettings.merged.contents,
|
||||
subjects: viewerConfiguration.subjects,
|
||||
final: viewerConfiguration.merged.contents,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@ -150,9 +150,7 @@ function highlightNodeHelper(
|
||||
}
|
||||
|
||||
let newNode: Node
|
||||
if (newNodes.length === 0) {
|
||||
newNode = document.createTextNode('')
|
||||
} else if (newNodes.length === 1) {
|
||||
if (newNodes.length === 1) {
|
||||
// If we only have one new node, no need to wrap it in a containing span
|
||||
newNode = newNodes[0]
|
||||
} else {
|
||||
|
||||
@ -41,42 +41,6 @@ export const highlightCodeSafe = (code: string, language?: string): string => {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RenderMarkdownOptions {
|
||||
/**
|
||||
* Whether to render markdown inline, without paragraph tags
|
||||
*/
|
||||
inline?: boolean
|
||||
/**
|
||||
* Whether to render line breaks as HTML `<br>`s
|
||||
*/
|
||||
breaks?: boolean
|
||||
/**
|
||||
* Whether to disable autolinks. Explicit links using `[text](url)` are still allowed.
|
||||
*/
|
||||
disableAutolinks?: boolean
|
||||
/**
|
||||
* A custom renderer to use
|
||||
*/
|
||||
renderer?: marked.Renderer
|
||||
/**
|
||||
* A prefix to add to all header IDs
|
||||
*/
|
||||
headerPrefix?: string
|
||||
/**
|
||||
* Strip off any HTML and return a plain text string, useful for previews
|
||||
*/
|
||||
plainText?: boolean
|
||||
/**
|
||||
* DOMPurify configuration to use
|
||||
*/
|
||||
dompurifyConfig?: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false }
|
||||
/**
|
||||
* Add target="_blank" and rel="noopener" to all <a> links that have a href value.
|
||||
* This affects all markdown-formatted links and all inline HTML links.
|
||||
*/
|
||||
addTargetBlankToAllLinks?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given markdown to HTML, highlighting code and sanitizing dangerous HTML.
|
||||
* Can throw an exception on parse errors.
|
||||
@ -91,7 +55,18 @@ export interface RenderMarkdownOptions {
|
||||
* @param options.addTargetBlankToAllLinks Add target="_blank" and rel="noopener" to all <a> links
|
||||
* that have a href value. This affects all markdown-formatted links and all inline HTML links.
|
||||
*/
|
||||
export const renderMarkdown = (markdown: string, options: RenderMarkdownOptions = {}): string => {
|
||||
export const renderMarkdown = (
|
||||
markdown: string,
|
||||
options: {
|
||||
breaks?: boolean
|
||||
disableAutolinks?: boolean
|
||||
renderer?: marked.Renderer
|
||||
headerPrefix?: string
|
||||
plainText?: boolean
|
||||
dompurifyConfig?: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false }
|
||||
addTargetBlankToAllLinks?: boolean
|
||||
} = {}
|
||||
): string => {
|
||||
const tokenizer = new marked.Tokenizer()
|
||||
if (options.disableAutolinks) {
|
||||
// Why the odd double-casting below?
|
||||
@ -101,7 +76,7 @@ export const renderMarkdown = (markdown: string, options: RenderMarkdownOptions
|
||||
tokenizer.url = () => undefined as unknown as marked.Tokens.Link
|
||||
}
|
||||
|
||||
const rendered = (options.inline ? marked.parseInline : marked)(markdown, {
|
||||
const rendered = marked(markdown, {
|
||||
gfm: true,
|
||||
breaks: options.breaks,
|
||||
highlight: (code, language) => highlightCodeSafe(code, language),
|
||||
|
||||
@ -86,3 +86,9 @@ export const JetBrainsSearchBoxStory: StoryFn = () => {
|
||||
</WildcardThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
JetBrainsSearchBoxStory.parameters = {
|
||||
chromatic: {
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
@ -123,3 +123,9 @@ export const JetBrainsSearchResultListStory: StoryFn = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
JetBrainsSearchResultListStory.parameters = {
|
||||
chromatic: {
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
@ -30,4 +30,4 @@
|
||||
"@sourcegraph/wildcard": "workspace:*"
|
||||
},
|
||||
"sideEffects": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,12 @@ export const CommandAction: StoryFn = () => (
|
||||
)
|
||||
|
||||
CommandAction.storyName = 'Command action'
|
||||
CommandAction.parameters = {
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const LinkAction: StoryFn = () => (
|
||||
<ActionItem
|
||||
|
||||
@ -31,6 +31,7 @@ export const currentAuthStateQuery = gql`
|
||||
__typename
|
||||
id
|
||||
name
|
||||
displayName
|
||||
url
|
||||
settingsURL
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ import { parseRepoGitURI } from '../util/url'
|
||||
import type { DocumentSelector, TextDocument, DocumentHighlight } from './legacy-extensions/api'
|
||||
import * as sourcegraph from './legacy-extensions/api'
|
||||
import type { LanguageSpec } from './legacy-extensions/language-specs/language-spec'
|
||||
import { findLanguageSpec, languageSpecs } from './legacy-extensions/language-specs/languages'
|
||||
import { languageSpecs } from './legacy-extensions/language-specs/languages'
|
||||
import { RedactingLogger } from './legacy-extensions/logging'
|
||||
import { createProviders, emptySourcegraphProviders, type SourcegraphProviders } from './legacy-extensions/providers'
|
||||
import { SymbolRole, Occurrence } from './scip'
|
||||
@ -172,7 +172,12 @@ const languages: Language[] = languageSpecs.map(spec => ({
|
||||
|
||||
// Returns true if the provided language supports "Find implementations"
|
||||
export function hasFindImplementationsSupport(language: string): boolean {
|
||||
return findLanguageSpec(language)?.textDocumentImplemenationSupport ?? false
|
||||
for (const spec of languageSpecs) {
|
||||
if (spec.languageID === language) {
|
||||
return spec.textDocumentImplemenationSupport ?? false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function selectorForSpec(languageSpec: LanguageSpec): DocumentSelector {
|
||||
|
||||
@ -431,6 +431,5 @@ export const languageSpecs: LanguageSpec[] = [
|
||||
* @deprecated See FIXME(id: language-detection)
|
||||
*/
|
||||
export function findLanguageSpec(languageID: string): LanguageSpec | undefined {
|
||||
languageID = languageID.toLowerCase()
|
||||
return languageSpecs.find(spec => spec.languageID === languageID || spec.additionalLanguages?.includes(languageID))
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ import gql from 'tagged-template-noop'
|
||||
|
||||
import { isErrorLike } from '@sourcegraph/common'
|
||||
|
||||
import { SearchVersion } from '../../../graphql-operations'
|
||||
import type * as sourcegraph from '../api'
|
||||
import { cache } from '../util'
|
||||
|
||||
@ -252,7 +251,6 @@ export class API {
|
||||
|
||||
const data = await queryGraphQL<Response>(buildSearchQuery(fileLocal), {
|
||||
query,
|
||||
version: SearchVersion.V3,
|
||||
})
|
||||
return data.search.results.results.filter(isDefined)
|
||||
}
|
||||
@ -324,8 +322,8 @@ function buildSearchQuery(fileLocal: boolean): string {
|
||||
|
||||
if (fileLocal) {
|
||||
return gql`
|
||||
query LegacyCodeIntelSearch2($query: String!, $version: SearchVersion!) {
|
||||
search(query: $query, version: $version) {
|
||||
query LegacyCodeIntelSearch2($query: String!) {
|
||||
search(query: $query) {
|
||||
...SearchResults
|
||||
...FileLocal
|
||||
}
|
||||
@ -336,8 +334,8 @@ function buildSearchQuery(fileLocal: boolean): string {
|
||||
}
|
||||
|
||||
return gql`
|
||||
query LegacyCodeIntelSearch3($query: String!, $version: SearchVersion!) {
|
||||
search(query: $query, version: $version) {
|
||||
query LegacyCodeIntelSearch3($query: String!) {
|
||||
search(query: $query) {
|
||||
...SearchResults
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
// but it doesn't make sense to do so right now.
|
||||
import type * as extensions from '@sourcegraph/extension-api-types'
|
||||
|
||||
import { CodeGraphDataProvenance } from '../graphql-operations'
|
||||
|
||||
import type * as sourcegraph from './legacy-extensions/api'
|
||||
|
||||
export interface JsonDocument {
|
||||
@ -146,8 +144,7 @@ export class Occurrence {
|
||||
public readonly range: Range,
|
||||
public readonly kind?: SyntaxKind,
|
||||
public readonly symbol?: string,
|
||||
public readonly symbolRoles?: number,
|
||||
public readonly symbolProvenance?: CodeGraphDataProvenance
|
||||
public readonly symbolRoles?: number
|
||||
) {}
|
||||
|
||||
public withStartPosition(newStartPosition: Position): Occurrence {
|
||||
|
||||
@ -1675,40 +1675,40 @@ describe('scanSearchQuery() and decorate()', () => {
|
||||
|
||||
test('highlight repo:has predicate', () => {
|
||||
expect(getTokens(toSuccess(scanSearchQuery('repo:has(key:value)')))).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"startIndex": 0,
|
||||
"scopes": "field"
|
||||
},
|
||||
{
|
||||
"startIndex": 4,
|
||||
"scopes": "metaFilterSeparator"
|
||||
},
|
||||
{
|
||||
"startIndex": 5,
|
||||
"scopes": "metaPredicateNameAccess"
|
||||
},
|
||||
{
|
||||
"startIndex": 8,
|
||||
"scopes": "metaPredicateParenthesis"
|
||||
},
|
||||
{
|
||||
"startIndex": 9,
|
||||
"scopes": "identifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 12,
|
||||
"scopes": "metaFilterSeparator"
|
||||
},
|
||||
{
|
||||
"startIndex": 13,
|
||||
"scopes": "identifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 18,
|
||||
"scopes": "metaPredicateParenthesis"
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"startIndex": 0,
|
||||
"scopes": "field"
|
||||
},
|
||||
{
|
||||
"startIndex": 4,
|
||||
"scopes": "metaFilterSeparator"
|
||||
},
|
||||
{
|
||||
"startIndex": 5,
|
||||
"scopes": "metaPredicateNameAccess"
|
||||
},
|
||||
{
|
||||
"startIndex": 8,
|
||||
"scopes": "metaPredicateParenthesis"
|
||||
},
|
||||
{
|
||||
"startIndex": 9,
|
||||
"scopes": "identifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 12,
|
||||
"scopes": "metaFilterSeparator"
|
||||
},
|
||||
{
|
||||
"startIndex": 13,
|
||||
"scopes": "identifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 18,
|
||||
"scopes": "metaPredicateParenthesis"
|
||||
}
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
@ -1891,103 +1891,6 @@ describe('scanSearchQuery() and decorate()', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test('decorate repo:has.meta with regexp', () => {
|
||||
expect(
|
||||
getTokens(
|
||||
toSuccess(
|
||||
scanSearchQuery(String.raw`repo:has.meta(/abc.*/:/[def]+/)`, false, SearchPatternType.keyword)
|
||||
)
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"startIndex": 0,
|
||||
"scopes": "field"
|
||||
},
|
||||
{
|
||||
"startIndex": 4,
|
||||
"scopes": "metaFilterSeparator"
|
||||
},
|
||||
{
|
||||
"startIndex": 5,
|
||||
"scopes": "metaPredicateNameAccess"
|
||||
},
|
||||
{
|
||||
"startIndex": 8,
|
||||
"scopes": "metaPredicateDot"
|
||||
},
|
||||
{
|
||||
"startIndex": 9,
|
||||
"scopes": "metaPredicateNameAccess"
|
||||
},
|
||||
{
|
||||
"startIndex": 13,
|
||||
"scopes": "metaPredicateParenthesis"
|
||||
},
|
||||
{
|
||||
"startIndex": 14,
|
||||
"scopes": "metaRegexpDelimited"
|
||||
},
|
||||
{
|
||||
"startIndex": 15,
|
||||
"scopes": "identifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 18,
|
||||
"scopes": "metaRegexpCharacterSet"
|
||||
},
|
||||
{
|
||||
"startIndex": 19,
|
||||
"scopes": "metaRegexpRangeQuantifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 20,
|
||||
"scopes": "metaRegexpDelimited"
|
||||
},
|
||||
{
|
||||
"startIndex": 21,
|
||||
"scopes": "metaFilterSeparator"
|
||||
},
|
||||
{
|
||||
"startIndex": 22,
|
||||
"scopes": "metaRegexpDelimited"
|
||||
},
|
||||
{
|
||||
"startIndex": 23,
|
||||
"scopes": "metaRegexpCharacterClass"
|
||||
},
|
||||
{
|
||||
"startIndex": 24,
|
||||
"scopes": "metaRegexpCharacterClassMember"
|
||||
},
|
||||
{
|
||||
"startIndex": 25,
|
||||
"scopes": "metaRegexpCharacterClassMember"
|
||||
},
|
||||
{
|
||||
"startIndex": 26,
|
||||
"scopes": "metaRegexpCharacterClassMember"
|
||||
},
|
||||
{
|
||||
"startIndex": 27,
|
||||
"scopes": "metaRegexpCharacterClass"
|
||||
},
|
||||
{
|
||||
"startIndex": 28,
|
||||
"scopes": "metaRegexpRangeQuantifier"
|
||||
},
|
||||
{
|
||||
"startIndex": 29,
|
||||
"scopes": "metaRegexpDelimited"
|
||||
},
|
||||
{
|
||||
"startIndex": 30,
|
||||
"scopes": "metaPredicateParenthesis"
|
||||
}
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
test('do not decorate quotes inside quoted filter values', () => {
|
||||
expect(getTokens(toSuccess(scanSearchQuery(String.raw`file:"foo\"bar"`, false, SearchPatternType.keyword))))
|
||||
.toMatchInlineSnapshot(`
|
||||
|
||||
@ -13,8 +13,8 @@ import type {
|
||||
|
||||
import { SearchPatternType } from '../../graphql-operations'
|
||||
|
||||
import { type PredicateInstance, scanPredicate } from './predicates'
|
||||
import { quoted, scanSearchQuery, oneOf, ScanResult, toPatternResult } from './scanner'
|
||||
import { type Predicate, scanPredicate } from './predicates'
|
||||
import { scanSearchQuery } from './scanner'
|
||||
import { type Token, type Pattern, type Literal, PatternKind, type CharacterRange, createLiteral } from './token'
|
||||
|
||||
/* eslint-disable unicorn/better-regex */
|
||||
@ -200,7 +200,7 @@ export interface MetaPredicate {
|
||||
range: CharacterRange
|
||||
groupRange?: CharacterRange
|
||||
kind: MetaPredicateKind
|
||||
value: PredicateInstance
|
||||
value: Predicate
|
||||
}
|
||||
|
||||
enum MetaKeywordKind {
|
||||
@ -1014,48 +1014,33 @@ const decorateRepoHasMetaBody = (body: string, offset: number): DecoratedToken[]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const offsetToken = (offset: number) => (token: DecoratedToken) => {
|
||||
token.range.start += offset
|
||||
token.range.end += offset
|
||||
return token
|
||||
}
|
||||
|
||||
return [
|
||||
...(decoratePattern(matches[1])?.map(offsetToken(offset)) ?? []),
|
||||
{
|
||||
type: 'literal',
|
||||
value: matches[1],
|
||||
range: { start: offset, end: offset + matches[1].length },
|
||||
quoted: false,
|
||||
},
|
||||
{
|
||||
type: 'metaFilterSeparator',
|
||||
range: { start: offset + matches[1].length, end: offset + matches[1].length + 1 },
|
||||
value: ':',
|
||||
},
|
||||
...(decoratePattern(matches[2])?.map(offsetToken(offset + matches[1].length + 1)) ?? []),
|
||||
{
|
||||
type: 'literal',
|
||||
value: matches[1],
|
||||
range: { start: offset + matches[1].length + 1, end: offset + matches[1].length + 1 + matches[2].length },
|
||||
quoted: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const decoratePattern = (token: string): DecoratedToken[] | undefined => {
|
||||
const plainString = (token: string, offset: number): ScanResult<Literal> => ({
|
||||
type: 'success',
|
||||
term: createLiteral(token, { start: offset, end: offset + token.length }),
|
||||
})
|
||||
|
||||
const scanner = oneOf<Pattern>(
|
||||
toPatternResult(quoted("'"), PatternKind.Literal),
|
||||
toPatternResult(quoted('"'), PatternKind.Literal),
|
||||
toPatternResult(quoted('/'), PatternKind.Regexp),
|
||||
toPatternResult(plainString, PatternKind.Literal)
|
||||
)
|
||||
const scanResult = scanner(token, 0)
|
||||
if (scanResult.type === 'error') {
|
||||
return undefined
|
||||
}
|
||||
return decorate(scanResult.term)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates the body part of predicate syntax `name(body)`.
|
||||
*/
|
||||
const decoratePredicateBody = (name: string, body: string, offset: number): DecoratedToken[] => {
|
||||
const decoratePredicateBody = (path: string[], body: string, offset: number): DecoratedToken[] => {
|
||||
const decorated: DecoratedToken[] = []
|
||||
switch (name) {
|
||||
switch (path.join('.')) {
|
||||
case 'contains.file':
|
||||
case 'has.file': {
|
||||
const result = decorateContainsFileBody(body, offset)
|
||||
@ -1123,18 +1108,18 @@ const decoratePredicateBody = (name: string, body: string, offset: number): Deco
|
||||
return decorated
|
||||
}
|
||||
|
||||
const decoratePredicate = (predicate: PredicateInstance, range: CharacterRange): DecoratedToken[] => {
|
||||
const decoratePredicate = (predicate: Predicate, range: CharacterRange): DecoratedToken[] => {
|
||||
let offset = range.start
|
||||
const decorated: DecoratedToken[] = []
|
||||
for (const namePart of predicate.name.split('.')) {
|
||||
for (const nameAccess of predicate.path) {
|
||||
decorated.push({
|
||||
type: 'metaPredicate',
|
||||
kind: MetaPredicateKind.NameAccess,
|
||||
range: { start: offset, end: offset + namePart.length },
|
||||
range: { start: offset, end: offset + nameAccess.length },
|
||||
groupRange: range,
|
||||
value: predicate,
|
||||
})
|
||||
offset = offset + namePart.length
|
||||
offset = offset + nameAccess.length
|
||||
decorated.push({
|
||||
type: 'metaPredicate',
|
||||
kind: MetaPredicateKind.Dot,
|
||||
@ -1155,7 +1140,7 @@ const decoratePredicate = (predicate: PredicateInstance, range: CharacterRange):
|
||||
value: predicate,
|
||||
})
|
||||
offset = offset + 1
|
||||
decorated.push(...decoratePredicateBody(predicate.name, body, offset))
|
||||
decorated.push(...decoratePredicateBody(predicate.path, body, offset))
|
||||
offset = offset + body.length
|
||||
decorated.push({
|
||||
type: 'metaPredicate',
|
||||
|
||||
@ -76,7 +76,7 @@ describe('getDiagnostics()', () => {
|
||||
[
|
||||
{
|
||||
"severity": "error",
|
||||
"message": "Invalid filter value, expected one of: commit, diff, file, path, repo, symbol.",
|
||||
"message": "Invalid filter value, expected one of: diff, commit, symbol, repo, path, file.",
|
||||
"range": {
|
||||
"start": 10,
|
||||
"end": 15
|
||||
|
||||
@ -322,33 +322,32 @@ export const FILTERS: Record<NegatableFilter, NegatableFilterDefinition> &
|
||||
},
|
||||
[FilterType.type]: {
|
||||
description: 'Limit results to diffs, commits, file paths, symbols and other entities.',
|
||||
discreteValues: () =>
|
||||
[
|
||||
{
|
||||
label: 'diff',
|
||||
description: 'Search for file changes',
|
||||
},
|
||||
{
|
||||
label: 'commit',
|
||||
description: 'Search in commit messages',
|
||||
},
|
||||
{
|
||||
label: 'symbol',
|
||||
description: 'Search for symbol names',
|
||||
},
|
||||
{
|
||||
label: 'repo',
|
||||
description: 'Search for repositories',
|
||||
},
|
||||
{
|
||||
label: 'path',
|
||||
description: 'Search for file/directory names',
|
||||
},
|
||||
{
|
||||
label: 'file',
|
||||
description: 'Search for file content',
|
||||
},
|
||||
].sort((a, b) => a.label.localeCompare(b.label)),
|
||||
discreteValues: () => [
|
||||
{
|
||||
label: 'diff',
|
||||
description: 'Search for file changes',
|
||||
},
|
||||
{
|
||||
label: 'commit',
|
||||
description: 'Search in commit messages',
|
||||
},
|
||||
{
|
||||
label: 'symbol',
|
||||
description: 'Search for symbol names',
|
||||
},
|
||||
{
|
||||
label: 'repo',
|
||||
description: 'Search for repositories',
|
||||
},
|
||||
{
|
||||
label: 'path',
|
||||
description: 'Search for file/directory names',
|
||||
},
|
||||
{
|
||||
label: 'file',
|
||||
description: 'Search for file content',
|
||||
},
|
||||
],
|
||||
},
|
||||
[FilterType.visibility]: {
|
||||
discreteValues: () => ['any', 'private', 'public'].map(value => ({ label: value })),
|
||||
|
||||
@ -197,7 +197,7 @@ const toSelectorHover = (token: MetaSelector): string => {
|
||||
|
||||
const toPredicateHover = (token: MetaPredicate): string => {
|
||||
const parameters = token.value.parameters.slice(1, -1)
|
||||
switch (token.value.name) {
|
||||
switch (token.value.path.join('.')) {
|
||||
case 'contains.file':
|
||||
case 'has.file': {
|
||||
return '**Built-in predicate**. Search only inside repositories that satisfy the specified `path:` and `content:` filters. `path:` and `content:` filters should be regular expressions.'
|
||||
|
||||
@ -130,7 +130,7 @@ export const collectMetrics = (query: string): Metrics | undefined => {
|
||||
if (!predicate) {
|
||||
continue
|
||||
}
|
||||
switch (predicate.name) {
|
||||
switch (predicate.path.join('.')) {
|
||||
case 'contains.path': {
|
||||
count_repo_contains_path += 1
|
||||
break
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { scanPredicate } from './predicates'
|
||||
import { scanPredicate, resolveAccess, PREDICATES } from './predicates'
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
serialize: value => (value ? JSON.stringify(value) : 'invalid'),
|
||||
@ -10,19 +10,19 @@ expect.addSnapshotSerializer({
|
||||
describe('scanPredicate', () => {
|
||||
test('scan recognized and valid syntax', () => {
|
||||
expect(scanPredicate('repo', 'contains.file(content:stuff)')).toMatchInlineSnapshot(
|
||||
'{"field":"repo","name":"contains.file","parameters":"(content:stuff)"}'
|
||||
'{"path":["contains","file"],"parameters":"(content:stuff)"}'
|
||||
)
|
||||
})
|
||||
|
||||
test('scan recognized dot syntax', () => {
|
||||
expect(scanPredicate('repo', 'contains.commit.after(stuff)')).toMatchInlineSnapshot(
|
||||
'{"field":"repo","name":"contains.commit.after","parameters":"(stuff)"}'
|
||||
'{"path":["contains","commit","after"],"parameters":"(stuff)"}'
|
||||
)
|
||||
})
|
||||
|
||||
test('scan recognized and valid syntax with escapes', () => {
|
||||
expect(scanPredicate('repo', 'contains.file(content:\\((stuff))')).toMatchInlineSnapshot(
|
||||
'{"field":"repo","name":"contains.file","parameters":"(content:\\\\((stuff))"}'
|
||||
'{"path":["contains","file"],"parameters":"(content:\\\\((stuff))"}'
|
||||
)
|
||||
})
|
||||
|
||||
@ -40,13 +40,13 @@ describe('scanPredicate', () => {
|
||||
|
||||
test('resolve field aliases for predicates', () => {
|
||||
expect(scanPredicate('r', 'contains.file(content:stuff)')).toMatchInlineSnapshot(
|
||||
'{"field":"repo","name":"contains.file","parameters":"(content:stuff)"}'
|
||||
'{"path":["contains","file"],"parameters":"(content:stuff)"}'
|
||||
)
|
||||
})
|
||||
|
||||
test('scan recognized file:contains.content syntax', () => {
|
||||
expect(scanPredicate('file', 'contains.content(stuff)')).toMatchInlineSnapshot(
|
||||
'{"field":"file","name":"contains.content","parameters":"(stuff)"}'
|
||||
'{"path":["contains","content"],"parameters":"(stuff)"}'
|
||||
)
|
||||
})
|
||||
|
||||
@ -57,10 +57,28 @@ describe('scanPredicate', () => {
|
||||
test('scan invalid file:contains() syntax', () => {
|
||||
expect(scanPredicate('file', 'contains(stuff')).toMatchInlineSnapshot('invalid')
|
||||
})
|
||||
})
|
||||
|
||||
test('scan repo:has.meta with regex', () => {
|
||||
expect(scanPredicate('repo', 'has.meta(/abc.*/:/def.*/)')).toMatchInlineSnapshot(
|
||||
'{"field":"repo","name":"has.meta","parameters":"(/abc.*/:/def.*/)"}'
|
||||
describe('resolveAccess', () => {
|
||||
test('resolves partial access tree', () => {
|
||||
expect(resolveAccess(['repo'], PREDICATES)).toMatchInlineSnapshot(
|
||||
'[{"name":"contains","fields":[{"name":"file"},{"name":"path"},{"name":"content"},{"name":"commit","fields":[{"name":"after"}]}]},{"name":"has","fields":[{"name":"file"},{"name":"path"},{"name":"content"},{"name":"commit","fields":[{"name":"after"}]},{"name":"description"},{"name":"tag"},{"name":"key"},{"name":"meta"},{"name":"topic"}]}]'
|
||||
)
|
||||
})
|
||||
|
||||
test('resolves partial access tree depth 2', () => {
|
||||
expect(resolveAccess(['repo', 'contains', 'commit'], PREDICATES)).toMatchInlineSnapshot('[{"name":"after"}]')
|
||||
})
|
||||
|
||||
test('resolves fully qualified path', () => {
|
||||
expect(resolveAccess(['repo', 'contains', 'file'], PREDICATES)).toMatchInlineSnapshot('[]')
|
||||
})
|
||||
|
||||
test('undefind path', () => {
|
||||
expect(resolveAccess(['OCOTILLO', 'contains', 'file'], PREDICATES)).toMatchInlineSnapshot('invalid')
|
||||
})
|
||||
|
||||
test('invalid predicate syntax', () => {
|
||||
expect(resolveAccess(['repo', 'contains'], PREDICATES)).toMatchInlineSnapshot('invalid')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,39 +1,102 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import { type Completion, resolveFieldAlias, FilterType } from './filters'
|
||||
|
||||
interface PredicateDefinition {
|
||||
field: string
|
||||
interface Access {
|
||||
name: string
|
||||
fields?: Access[]
|
||||
}
|
||||
|
||||
// PREDICATES is a registry of predicates, grouped by field they belong to
|
||||
export const PREDICATES: PredicateDefinition[] = [
|
||||
{ field: 'repo', name: 'contains.file' },
|
||||
{ field: 'repo', name: 'contains.path' },
|
||||
{ field: 'repo', name: 'contains.content' },
|
||||
{ field: 'repo', name: 'contains.commit.after' },
|
||||
{ field: 'repo', name: 'contains.commit.after' },
|
||||
{ field: 'repo', name: 'has' },
|
||||
{ field: 'repo', name: 'has.file' },
|
||||
{ field: 'repo', name: 'has.path' },
|
||||
{ field: 'repo', name: 'has.content' },
|
||||
{ field: 'repo', name: 'has.commit.after' },
|
||||
{ field: 'repo', name: 'has.description' },
|
||||
{ field: 'repo', name: 'has.tag' },
|
||||
{ field: 'repo', name: 'has.key' },
|
||||
{ field: 'repo', name: 'has.meta' },
|
||||
{ field: 'repo', name: 'has.topic' },
|
||||
{ field: 'file', name: 'contains.content' },
|
||||
{ field: 'file', name: 'has.content' },
|
||||
{ field: 'file', name: 'has.owner' },
|
||||
{ field: 'rev', name: 'at.time' },
|
||||
/**
|
||||
* Represents recognized predicate accesses associated with fields. The
|
||||
* data structure is a tree, where nodes are lists to preserve ordering
|
||||
* for autocomplete suggestions.
|
||||
*/
|
||||
export const PREDICATES: Access[] = [
|
||||
{
|
||||
name: 'repo',
|
||||
fields: [
|
||||
{
|
||||
name: 'contains',
|
||||
fields: [
|
||||
{ name: 'file' },
|
||||
{ name: 'path' },
|
||||
{ name: 'content' },
|
||||
{
|
||||
name: 'commit',
|
||||
fields: [{ name: 'after' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'has',
|
||||
fields: [
|
||||
{ name: 'file' },
|
||||
{ name: 'path' },
|
||||
{ name: 'content' },
|
||||
{
|
||||
name: 'commit',
|
||||
fields: [{ name: 'after' }],
|
||||
},
|
||||
{ name: 'description' },
|
||||
{ name: 'tag' },
|
||||
{ name: 'key' },
|
||||
{ name: 'meta' },
|
||||
{ name: 'topic' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'file',
|
||||
fields: [
|
||||
{
|
||||
name: 'contains',
|
||||
fields: [{ name: 'content' }],
|
||||
},
|
||||
{
|
||||
name: 'has',
|
||||
fields: [{ name: 'content' }, { name: 'owner' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'rev',
|
||||
fields: [
|
||||
{
|
||||
name: 'at',
|
||||
fields: [{ name: 'time' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** Represents a predicate's components corresponding to the syntax path(parameters). */
|
||||
export interface PredicateInstance extends PredicateDefinition {
|
||||
export interface Predicate {
|
||||
path: string[]
|
||||
parameters: string
|
||||
}
|
||||
|
||||
/** Returns the access tree for a predicate path. */
|
||||
export const resolveAccess = (path: string[], tree: Access[]): Access[] | undefined => {
|
||||
if (path.length === 0) {
|
||||
return tree
|
||||
}
|
||||
|
||||
// repo:contains() and file:contains() are not supported
|
||||
if (path.length === 1 && path[0] === 'contains') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const subtree = tree.find(value => value.name === path[0])
|
||||
if (!subtree) {
|
||||
return undefined
|
||||
}
|
||||
if (!subtree.fields) {
|
||||
return []
|
||||
}
|
||||
return resolveAccess(path.slice(1), subtree.fields)
|
||||
}
|
||||
|
||||
// scans a string up to closing parentheses. Examples:
|
||||
// - `foo` succeeds, parentheses are absent, so it is vacuously balanced
|
||||
// - `foo(...)` succeeds up to the closing `)`
|
||||
@ -91,22 +154,23 @@ const scanBalancedParens = (input: string): string | undefined => {
|
||||
* (1) The (field, name) pair is a recognized predicate.
|
||||
* (2) The parameters value is well-balanced.
|
||||
*/
|
||||
export const scanPredicate = (field: string, value: string): PredicateInstance | undefined => {
|
||||
export const scanPredicate = (field: string, value: string): Predicate | undefined => {
|
||||
const match = value.match(/^[.a-z]+/i)
|
||||
if (!match) {
|
||||
return undefined
|
||||
}
|
||||
const name = match[0]
|
||||
const path = name.split('.')
|
||||
// Remove negation from the field for lookup
|
||||
if (field.startsWith('-')) {
|
||||
field = field.slice(1)
|
||||
}
|
||||
field = resolveFieldAlias(field)
|
||||
const predicate = PREDICATES.find(predicate => predicate.field === field && predicate.name === name)
|
||||
if (!predicate) {
|
||||
const access = resolveAccess([field, ...path], PREDICATES)
|
||||
if (!access) {
|
||||
return undefined
|
||||
}
|
||||
const rest = value.slice(predicate.name.length)
|
||||
const rest = value.slice(name.length)
|
||||
const parameters = scanBalancedParens(rest)
|
||||
if (!parameters) {
|
||||
return undefined
|
||||
@ -115,7 +179,7 @@ export const scanPredicate = (field: string, value: string): PredicateInstance |
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { ...predicate, parameters }
|
||||
return { path, parameters }
|
||||
}
|
||||
|
||||
export const predicateCompletion = (field: FilterType): Completion[] => {
|
||||
|
||||
@ -93,7 +93,7 @@ const zeroOrMore =
|
||||
/**
|
||||
* Returns a {@link Scanner} that succeeds if any of the given scanner succeeds.
|
||||
*/
|
||||
export const oneOf =
|
||||
const oneOf =
|
||||
<T>(...scanners: Scanner<T>[]): Scanner<T> =>
|
||||
(input, start) => {
|
||||
const expected: string[] = []
|
||||
@ -115,7 +115,7 @@ export const oneOf =
|
||||
* A {@link Scanner} that will attempt to scan delimited strings for an arbitrary
|
||||
* delimiter. `\` is treated as an escape character for the delimited string.
|
||||
*/
|
||||
export const quoted =
|
||||
const quoted =
|
||||
(delimiter: string): Scanner<Literal> =>
|
||||
(input, start) => {
|
||||
if (input[start] !== delimiter) {
|
||||
@ -297,7 +297,7 @@ export const scanPredicateValue = (input: string, start: number, field: Literal)
|
||||
at: start,
|
||||
}
|
||||
}
|
||||
const value = `${result.name}${result.parameters}`
|
||||
const value = `${result.path.join('.')}${result.parameters}`
|
||||
return {
|
||||
type: 'success',
|
||||
term: createLiteral(value, { start, end: start + value.length }),
|
||||
|
||||
@ -3,10 +3,10 @@ import { describe, expect, test } from 'vitest'
|
||||
import { createAggregateError, isErrorLike } from '@sourcegraph/common'
|
||||
|
||||
import {
|
||||
type CustomMergeFunctions,
|
||||
gqlToCascade,
|
||||
merge,
|
||||
mergeSettings,
|
||||
type CustomMergeFunctions,
|
||||
type Settings,
|
||||
type SettingsCascade,
|
||||
type SettingsSubject,
|
||||
@ -198,6 +198,7 @@ describe('mergeSettings', () => {
|
||||
key: '1',
|
||||
description: 'global saved query',
|
||||
query: 'type:diff global',
|
||||
notify: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -207,6 +208,7 @@ describe('mergeSettings', () => {
|
||||
key: '2',
|
||||
description: 'org saved query',
|
||||
query: 'type:diff org',
|
||||
notify: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -216,6 +218,7 @@ describe('mergeSettings', () => {
|
||||
key: '3',
|
||||
description: 'user saved query',
|
||||
query: 'type:diff user',
|
||||
notify: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -226,16 +229,19 @@ describe('mergeSettings', () => {
|
||||
key: '1',
|
||||
description: 'global saved query',
|
||||
query: 'type:diff global',
|
||||
notify: true,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
description: 'org saved query',
|
||||
query: 'type:diff org',
|
||||
notify: true,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
description: 'user saved query',
|
||||
query: 'type:diff user',
|
||||
notify: true,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
@ -311,6 +311,7 @@ const defaultFeatures: SettingsExperimentalFeatures = {
|
||||
isInitialized: true,
|
||||
searchQueryInput: 'v2',
|
||||
newSearchResultFiltersPanel: true,
|
||||
newCodyWeb: true,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -96,11 +96,6 @@ export interface TemporarySettingsSchema {
|
||||
|
||||
/** OpenCodeGraph */
|
||||
'openCodeGraph.annotations.visible': boolean
|
||||
|
||||
'webNext.welcomeOverlay.dismissed': boolean
|
||||
'webNext.welcomeOverlay.show': boolean
|
||||
'webNext.departureMessage.dismissed': boolean
|
||||
'webNext.departureMessage.show': boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -166,10 +161,6 @@ const TEMPORARY_SETTINGS: Record<keyof TemporarySettings, null> = {
|
||||
'simple.search.toggle': null,
|
||||
'cody.onboarding.completed': null,
|
||||
'openCodeGraph.annotations.visible': null,
|
||||
'webNext.welcomeOverlay.dismissed': null,
|
||||
'webNext.welcomeOverlay.show': null,
|
||||
'webNext.departureMessage.dismissed': null,
|
||||
'webNext.departureMessage.show': null,
|
||||
}
|
||||
|
||||
export const TEMPORARY_SETTINGS_KEYS = Object.keys(TEMPORARY_SETTINGS) as readonly (keyof TemporarySettings)[]
|
||||
|
||||
@ -10,7 +10,9 @@ const decorator: Decorator = story => <div className="p-3 container">{story()}</
|
||||
|
||||
const config: Meta = {
|
||||
title: 'shared/SymbolTag',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { disableSnapshots: false },
|
||||
},
|
||||
decorators: [decorator],
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// NOTE(naman): Remember to add events to allow list: https://docs-legacy.sourcegraph.com/dev/background-information/data-usage-pipeline#allow-list
|
||||
export const enum EventName {
|
||||
CODY_CHAT_PAGE_VIEWED = 'web:codyChat:pageViewed',
|
||||
CODY_CHAT_SUBMIT = 'web:codyChat:submit',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
load("//dev:defs.bzl", "npm_package", "ts_project")
|
||||
load("//dev:defs.bzl", "npm_package", "sass", "ts_project")
|
||||
load("//client/shared/dev:tools.bzl", "module_style_typings")
|
||||
load("//dev:eslint.bzl", "eslint_config_and_lint_root")
|
||||
|
||||
# gazelle:js_resolve **/*.module.scss :module_style_typings
|
||||
@ -24,10 +25,23 @@ ts_config(
|
||||
],
|
||||
)
|
||||
|
||||
module_style_typings(
|
||||
name = "module_style_typings",
|
||||
)
|
||||
|
||||
sass(
|
||||
name = "module_styles",
|
||||
srcs = glob(["src/**/*.module.scss"]),
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "storybook_lib",
|
||||
srcs = [
|
||||
"globals.d.ts",
|
||||
"src/decorators/withChromaticThemes/ChromaticRoot/ChromaticRoot.tsx",
|
||||
"src/decorators/withChromaticThemes/ChromaticRoot/index.ts",
|
||||
"src/decorators/withChromaticThemes/index.ts",
|
||||
"src/decorators/withChromaticThemes/withChromaticThemes.tsx",
|
||||
"src/dummyEventSourcePolyfill.ts",
|
||||
"src/environment-config.ts",
|
||||
"src/main.ts",
|
||||
@ -36,6 +50,7 @@ ts_project(
|
||||
],
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
":module_style_typings",
|
||||
":node_modules/@sourcegraph/build-config",
|
||||
":node_modules/@sourcegraph/wildcard",
|
||||
"//:node_modules/@storybook/addon-actions",
|
||||
@ -45,11 +60,14 @@ ts_project(
|
||||
"//:node_modules/@storybook/react-vite",
|
||||
"//:node_modules/@storybook/theming",
|
||||
"//:node_modules/@storybook/types",
|
||||
"//:node_modules/@types/classnames",
|
||||
"//:node_modules/@types/node",
|
||||
"//:node_modules/@types/react",
|
||||
"//:node_modules/classnames",
|
||||
"//:node_modules/focus-visible",
|
||||
"//:node_modules/open-color",
|
||||
"//:node_modules/react",
|
||||
"//:node_modules/vite-plugin-turbosnap",
|
||||
],
|
||||
)
|
||||
|
||||
@ -57,6 +75,7 @@ npm_package(
|
||||
name = "storybook_pkg",
|
||||
srcs = [
|
||||
"package.json",
|
||||
":module_styles", #keep
|
||||
":storybook_lib",
|
||||
],
|
||||
)
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"scripts": {
|
||||
"lint:js": "eslint --cache 'src/**/*.[jt]s?(x)'",
|
||||
"start": "TS_NODE_TRANSPILE_ONLY=true sb dev -p 9001 -c ./src",
|
||||
"start:chromatic": "CHROMATIC=true TS_NODE_TRANSPILE_ONLY=true sb dev -p 9001 -c ./src",
|
||||
"build": "TS_NODE_TRANSPILE_ONLY=true sb build -c ./src",
|
||||
"test": "echo no tests"
|
||||
},
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
.theme-wrapper {
|
||||
padding: 1rem;
|
||||
color: var(--body-color);
|
||||
background-color: var(--body-bg);
|
||||
position: relative;
|
||||
min-height: 50vh;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { type FunctionComponent, type PropsWithChildren, useState } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { PopoverRoot } from '@sourcegraph/wildcard'
|
||||
import { ChromaticThemeContext, type ChromaticTheme } from '@sourcegraph/wildcard/src/stories'
|
||||
|
||||
import styles from './ChromaticRoot.module.scss'
|
||||
|
||||
interface ChromaticRootProps extends ChromaticTheme {}
|
||||
|
||||
export const ChromaticRoot: FunctionComponent<PropsWithChildren<ChromaticRootProps>> = props => {
|
||||
const { theme, children } = props
|
||||
|
||||
const [rootReference, setElement] = useState<HTMLDivElement | null>(null)
|
||||
const themeClass = theme === 'light' ? 'theme-light' : 'theme-dark'
|
||||
|
||||
return (
|
||||
<ChromaticThemeContext.Provider value={{ theme }}>
|
||||
{/* Required to render `Popover` inside of the `ChromaticRoot` component. */}
|
||||
<PopoverRoot.Provider value={{ renderRoot: rootReference }}>
|
||||
<div className={classNames(themeClass, styles.themeWrapper)}>
|
||||
{children}
|
||||
|
||||
<div ref={setElement} />
|
||||
</div>
|
||||
</PopoverRoot.Provider>
|
||||
</ChromaticThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './ChromaticRoot'
|
||||
@ -0,0 +1 @@
|
||||
export * from './withChromaticThemes'
|
||||
@ -0,0 +1,32 @@
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
import type { Decorator } from '@storybook/react'
|
||||
|
||||
import { ChromaticRoot } from './ChromaticRoot'
|
||||
|
||||
/**
|
||||
* The global Storybook decorator used to snapshot stories with multiple themes in Chromatic.
|
||||
*
|
||||
* It's a recommended way of achieving this goal:
|
||||
* https://www.chromatic.com/docs/faq#do-you-support-taking-snapshots-of-a-component-with-multiple-the
|
||||
*
|
||||
* If the `chromatic.enableDarkMode` story parameter is set to `true`, the story will
|
||||
* be rendered twice in Chromatic — in light and dark modes.
|
||||
*/
|
||||
export const withChromaticThemes: Decorator<ReactElement> = (StoryFunc, { parameters }) => {
|
||||
if (parameters?.chromatic?.enableDarkMode) {
|
||||
return (
|
||||
<>
|
||||
<ChromaticRoot theme="light">
|
||||
<StoryFunc />
|
||||
</ChromaticRoot>
|
||||
|
||||
<ChromaticRoot theme="dark">
|
||||
<StoryFunc />
|
||||
</ChromaticRoot>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <StoryFunc />
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
import { getEnvironmentBoolean } from '@sourcegraph/build-config'
|
||||
|
||||
export const ENVIRONMENT_CONFIG = {
|
||||
STORIES_GLOB: process.env.STORIES_GLOB,
|
||||
CHROMATIC: getEnvironmentBoolean('CHROMATIC'),
|
||||
}
|
||||
|
||||
@ -3,8 +3,9 @@ import path from 'path'
|
||||
import type { StorybookConfigVite } from '@storybook/builder-vite'
|
||||
import type { StorybookConfig as ReactViteStorybookConfig } from '@storybook/react-vite'
|
||||
import type { StorybookConfig } from '@storybook/types'
|
||||
import turbosnap from 'vite-plugin-turbosnap'
|
||||
|
||||
import { ROOT_PATH, STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
|
||||
import { ROOT_PATH, STATIC_ASSETS_PATH, getEnvironmentBoolean } from '@sourcegraph/build-config'
|
||||
|
||||
import { ENVIRONMENT_CONFIG } from './environment-config'
|
||||
|
||||
@ -25,7 +26,12 @@ const getStoriesGlob = (): string[] => {
|
||||
}
|
||||
|
||||
const config: StorybookConfig & StorybookConfigVite & ReactViteStorybookConfig = {
|
||||
framework: '@storybook/react-vite',
|
||||
// TODO: This has to be an object and not a string for now due to a bug in Chromatic
|
||||
// that would cause the builder to not be identified correctly.
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: [path.resolve(__dirname, '../assets'), STATIC_ASSETS_PATH],
|
||||
stories: getStoriesGlob(),
|
||||
|
||||
@ -50,6 +56,15 @@ const config: StorybookConfig & StorybookConfigVite & ReactViteStorybookConfig =
|
||||
},
|
||||
|
||||
viteFinal: (config, { configType }) => {
|
||||
const isChromatic = getEnvironmentBoolean('CHROMATIC')
|
||||
config.define = { ...config.define, 'process.env.CHROMATIC': isChromatic }
|
||||
if (isChromatic && configType === 'PRODUCTION') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Using TurboSnap plugin!')
|
||||
config.plugins = config.plugins ?? []
|
||||
config.plugins.push(turbosnap({ rootDir: config.root ?? ROOT_PATH }))
|
||||
}
|
||||
|
||||
config.build = {
|
||||
...config.build,
|
||||
minify: false,
|
||||
@ -99,4 +114,23 @@ const config: StorybookConfig & StorybookConfigVite & ReactViteStorybookConfig =
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: We need to replace the @storybook/addon-storysource plugin with an object
|
||||
// definition to supply options here because chromatic CLI does not properly understand
|
||||
// the configured addons otherwise.
|
||||
const idx = config.addons?.findIndex(addon => addon === '@storybook/addon-storysource')
|
||||
if (idx !== undefined && idx >= 0) {
|
||||
config.addons![idx] = {
|
||||
name: '@storybook/addon-storysource',
|
||||
options: {
|
||||
rule: {
|
||||
test: /\.story\.tsx?$/,
|
||||
},
|
||||
sourceLoaderOptions: {
|
||||
injectStoryParameters: false,
|
||||
prettierConfig: { printWidth: 80, singleQuote: false },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
@ -7,12 +7,14 @@ import { withConsole } from '@storybook/addon-console'
|
||||
import type { DecoratorFn, Parameters } from '@storybook/react'
|
||||
|
||||
import { setLinkComponent, AnchorLink } from '@sourcegraph/wildcard'
|
||||
import { isChromatic } from '@sourcegraph/wildcard/src/stories'
|
||||
|
||||
import { withChromaticThemes } from './decorators/withChromaticThemes'
|
||||
import { themeDark, themeLight, THEME_DARK_CLASS, THEME_LIGHT_CLASS } from './themes'
|
||||
|
||||
const withConsoleDecorator: DecoratorFn = (storyFunc, context): ReactElement => withConsole()(storyFunc)(context)
|
||||
|
||||
export const decorators = [withConsoleDecorator].filter(Boolean)
|
||||
export const decorators = [withConsoleDecorator, isChromatic() && withChromaticThemes].filter(Boolean)
|
||||
|
||||
export const parameters: Parameters = {
|
||||
layout: 'fullscreen',
|
||||
@ -29,12 +31,36 @@ export const parameters: Parameters = {
|
||||
light: themeLight,
|
||||
dark: themeDark,
|
||||
},
|
||||
// disables snapshotting for all stories by default
|
||||
chromatic: { disableSnapshot: true },
|
||||
}
|
||||
|
||||
configureActions({ depth: 100, limit: 20 })
|
||||
|
||||
setLinkComponent(AnchorLink)
|
||||
|
||||
// Default to light theme for Chromatic and "Open canvas in new tab" button.
|
||||
// addon-dark-mode will override this if it's running.
|
||||
if (!document.body.classList.contains('theme-dark')) {
|
||||
document.body.classList.add('theme-light')
|
||||
}
|
||||
|
||||
// Default to light theme for Chromatic and "Open canvas in new tab" button.
|
||||
// addon-dark-mode will override this if it's running.
|
||||
if (!document.body.classList.contains('theme-dark')) {
|
||||
document.body.classList.add('theme-light')
|
||||
}
|
||||
|
||||
if (isChromatic()) {
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = `
|
||||
.monaco-editor .cursor {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
`
|
||||
document.head.append(style)
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
STORYBOOK_ENV?: string
|
||||
|
||||
@ -2,6 +2,7 @@ import ResizeObserver from 'resize-observer-polyfill'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
if ('ResizeObserver' in window === false) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.ResizeObserver = ResizeObserver
|
||||
}
|
||||
|
||||
@ -13,7 +13,9 @@ Apache
|
||||
## Feedback
|
||||
|
||||
Your feedback is important to us and is greatly appreciated. Please do not hesitate to submit your ideas or suggestions
|
||||
about how we can improve the extension to our [Code Search discussion community](https://community.sourcegraph.com/c/code-search/9).
|
||||
about how we can improve the extension to
|
||||
our [VS Code Extension Feedback Discussion Thread](https://github.com/sourcegraph/sourcegraph/discussions/34821) on
|
||||
GitHub.
|
||||
|
||||
## Issues / Bugs
|
||||
|
||||
@ -194,7 +196,7 @@ with us on the [Sourcegraph Community Slack group](https://about.sourcegraph.com
|
||||
- [Code of Conduct](https://handbook.sourcegraph.com/company-info-and-process/community/code_of_conduct/)
|
||||
- [Developing Sourcegraph guide](https://docs.sourcegraph.com/dev)
|
||||
- [Developing the web clients](https://docs.sourcegraph.com/dev/background-information/web)
|
||||
- [Feedback / Feature Request](https://community.sourcegraph.com/c/code-search/9)
|
||||
- [Feedback / Feature Request](https://github.com/sourcegraph/sourcegraph/discussions/34821)
|
||||
- [Issue Tracker](https://github.com/sourcegraph/sourcegraph/labels/vscode-extension)
|
||||
- [Report a bug](https://github.com/sourcegraph/sourcegraph/issues/new?labels=team/integrations,vscode-extension&title=VSCode+Bug+report:+&projects=Integrations%20Project%20Board)
|
||||
- [Troubleshooting docs](https://docs.sourcegraph.com/admin/how-to/troubleshoot-sg-extension#vs-code-extension)
|
||||
|
||||
@ -118,7 +118,9 @@ This extension contributes the following settings:
|
||||
|
||||
## Questions & Feedback
|
||||
|
||||
Feedback and feature requests can be submitted to our [Code Search discussion community](https://community.sourcegraph.com/c/code-search/9).
|
||||
Feedback and feature requests can be submitted to
|
||||
our [VS Code Extension Feedback Discussion Board](https://github.com/sourcegraph/sourcegraph/discussions/34821) on
|
||||
GitHub.
|
||||
|
||||
## Uninstallation
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ const currentAuthStateQuery = gql`
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
url
|
||||
settingsURL
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import { requestGraphQLFromVSCode } from './requestGraphQl'
|
||||
const blobContentQuery = gql`
|
||||
query BlobContent($repository: String!, $revision: String!, $path: String!) {
|
||||
repository(name: $repository) {
|
||||
id
|
||||
commit(rev: $revision) {
|
||||
blob(path: $path) {
|
||||
content
|
||||
|
||||
@ -34,7 +34,7 @@ const VSCE_LINK_PARAMS_UTM_SIDEBAR = {
|
||||
export const VSCE_LINK_MARKETPLACE = 'https://marketplace.visualstudio.com/items?itemName=sourcegraph.sourcegraph'
|
||||
export const VSCE_LINK_USER_DOCS =
|
||||
'https://docs.sourcegraph.com/cli/how-tos/creating_an_access_token' + VSCE_SIDEBAR_PARAMS
|
||||
export const VSCE_LINK_FEEDBACK = 'https://community.sourcegraph.com'
|
||||
export const VSCE_LINK_FEEDBACK = 'https://github.com/sourcegraph/sourcegraph/discussions/categories/feedback'
|
||||
export const VSCE_LINK_ISSUES =
|
||||
'https://github.com/sourcegraph/sourcegraph/issues/new?labels=team/integrations,vscode-extension&title=VSCode+Bug+report:+&projects=Integrations%20Project%20Board'
|
||||
export const VSCE_LINK_TROUBLESHOOT =
|
||||
|
||||
5
client/web-sveltekit/.env
Normal file
5
client/web-sveltekit/.env
Normal file
@ -0,0 +1,5 @@
|
||||
PUBLIC_DOTCOM=
|
||||
PUBLIC_CODY_ENABLED_ON_INSTANCE=true
|
||||
PUBLIC_CODY_ENABLED_FOR_CURRENT_USER=true
|
||||
PUBLIC_BATCH_CHANGES_ENABLED=true
|
||||
PUBLIC_CODE_INSIGHTS_ENABLED=true
|
||||
5
client/web-sveltekit/.env.dotcom
Normal file
5
client/web-sveltekit/.env.dotcom
Normal file
@ -0,0 +1,5 @@
|
||||
PUBLIC_DOTCOM=true
|
||||
PUBLIC_CODY_ENABLED_ON_INSTANCE=true
|
||||
PUBLIC_CODY_ENABLED_FOR_CURRENT_USER=true
|
||||
PUBLIC_BATCH_CHANGES_ENABLED=
|
||||
PUBLIC_CODE_INSIGHTS_ENABLED=
|
||||
@ -1,10 +1,7 @@
|
||||
import type { Preview } from '@storybook/svelte'
|
||||
import { initialize, mswLoader } from 'msw-storybook-addon'
|
||||
|
||||
// Global imports kept in sync with routes/+layout.svelte
|
||||
import '../src/routes/styles.scss'
|
||||
import '@fontsource-variable/roboto-mono'
|
||||
import '@fontsource-variable/inter'
|
||||
|
||||
// Initialize MSW
|
||||
initialize()
|
||||
|
||||
@ -18,6 +18,8 @@ SRCS = [
|
||||
".eslintignore",
|
||||
".eslintrc.cjs",
|
||||
".prettierignore",
|
||||
".env",
|
||||
".env.dotcom",
|
||||
"//client/wildcard:sass-breakpoints",
|
||||
"//client/wildcard:global-style-sources",
|
||||
"//client/web/dist/img:copy",
|
||||
@ -97,20 +99,24 @@ BUILD_DEPS = [
|
||||
":node_modules/@sentry/sveltekit",
|
||||
":node_modules/@sourcegraph/branded",
|
||||
":node_modules/@sourcegraph/client-api",
|
||||
":node_modules/@sourcegraph/cody-web",
|
||||
":node_modules/@sourcegraph/common",
|
||||
":node_modules/@sourcegraph/http-client",
|
||||
":node_modules/@sourcegraph/shared",
|
||||
":node_modules/@sourcegraph/telemetry",
|
||||
":node_modules/@sourcegraph/web",
|
||||
":node_modules/@sourcegraph/wildcard",
|
||||
":node_modules/@storybook/svelte",
|
||||
":node_modules/@sveltejs/adapter-static",
|
||||
":node_modules/@sveltejs/kit",
|
||||
":node_modules/@sveltejs/vite-plugin-svelte",
|
||||
":node_modules/@types/prismjs",
|
||||
":node_modules/@urql/core",
|
||||
":node_modules/cody-web-experimental",
|
||||
":node_modules/fzf",
|
||||
":node_modules/graphql",
|
||||
":node_modules/hotkeys-js",
|
||||
":node_modules/mermaid",
|
||||
":node_modules/prismjs",
|
||||
":node_modules/re2js",
|
||||
":node_modules/sass",
|
||||
":node_modules/signale",
|
||||
@ -192,21 +198,19 @@ copy_to_directory(
|
||||
|
||||
playwright_test_bin.playwright_test(
|
||||
name = "e2e_test",
|
||||
timeout = "long",
|
||||
timeout = "short",
|
||||
args = [
|
||||
"test",
|
||||
"--config $(location playwright.config.ts)",
|
||||
],
|
||||
data = glob(
|
||||
[
|
||||
"src/**/*.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/testing/*.ts",
|
||||
],
|
||||
) + [
|
||||
"playwright.config.ts",
|
||||
"tsconfig.json",
|
||||
":generate-graphql-types",
|
||||
":sveltekit-sync",
|
||||
":test_app_assets",
|
||||
"//cmd/frontend/graphqlbackend:graphql_schema",
|
||||
"//dev/tools:chromium",
|
||||
@ -216,6 +220,7 @@ playwright_test_bin.playwright_test(
|
||||
"BAZEL": "1",
|
||||
"ASSETS_DIR": "./client/web-sveltekit/test_app_assets/test_build/_sk/",
|
||||
},
|
||||
flaky = True,
|
||||
)
|
||||
|
||||
TESTS = glob([
|
||||
@ -240,13 +245,10 @@ TEST_BUILD_DEPS = [
|
||||
|
||||
vitest_test(
|
||||
name = "unit_tests",
|
||||
timeout = "moderate",
|
||||
bin = vitest_bin,
|
||||
chdir = package_name(),
|
||||
data = SRCS + BUILD_DEPS + CONFIGS + TESTS + TEST_BUILD_DEPS,
|
||||
tags = [
|
||||
TAG_SEARCHSUITE,
|
||||
],
|
||||
tags = [TAG_SEARCHSUITE],
|
||||
with_vitest_config = False,
|
||||
)
|
||||
|
||||
@ -330,10 +332,10 @@ svelte_check.svelte_check_test(
|
||||
args = [
|
||||
"--tsconfig",
|
||||
"tsconfig.json",
|
||||
# This causes only errors to be displayed, which is what we want
|
||||
# to keep noise down in CI
|
||||
"--threshold",
|
||||
"error",
|
||||
"--compiler-warnings",
|
||||
# missing-declaration is raised for our icon components. The Svelte compiler
|
||||
# does not take into account ambient declarations (will be fixed in Svelte 5).
|
||||
"missing-declaration:ignore",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = SRCS + BUILD_DEPS + CONFIGS + [
|
||||
@ -344,11 +346,6 @@ svelte_check.svelte_check_test(
|
||||
# Needed to properly extend vite's UserConfig type
|
||||
":node_modules/vitest",
|
||||
],
|
||||
env = {
|
||||
# It appears that svelte-check will start the vite dev server,
|
||||
# but in this case we don't want the proxy to be enabled.
|
||||
"SK_DISABLE_PROXY": "1",
|
||||
},
|
||||
)
|
||||
|
||||
filegroup(
|
||||
|
||||
@ -3,51 +3,28 @@
|
||||
This folder contains the experimental [SvelteKit](https://kit.svelte.dev/)
|
||||
implementation of the Sourcegraph app.
|
||||
|
||||
**NOTE:** This is a _very early_ prototype and it will change a lot.
|
||||
|
||||
## Developing
|
||||
|
||||
There are multiple ways to start the app:
|
||||
|
||||
1. Standalone and proxying to S2
|
||||
|
||||
```bash
|
||||
cd client/web-sveltekit
|
||||
pnpm dev
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
# Run dev server
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Then go to (usually) http://localhost:5173.
|
||||
The dev server can be accessed on http://localhost:5173. API requests and
|
||||
signin/signout are proxied to an actual Sourcegraph instance,
|
||||
https://sourcegraph.com by default (can be overwritten via the
|
||||
`SOURCEGRAPH_API_URL` environment variable.
|
||||
|
||||
Or via `sg`:
|
||||
If you're a Sourcegraph employee you should run this command to use the right auth instance:
|
||||
|
||||
```bash
|
||||
sg start web-sveltekit-standalone
|
||||
SOURCEGRAPH_API_URL=https://sourcegraph.sourcegraph.com pnpm run dev
|
||||
```
|
||||
|
||||
Then go to https://sourcegraph.test:5173.
|
||||
|
||||
2. Standalone and proxying to dotcom
|
||||
|
||||
```bash
|
||||
cd client/web-sveltekit
|
||||
pnpm dev:dotcom
|
||||
```
|
||||
|
||||
3. Standalone and proxying to another Sourcegraph instance
|
||||
|
||||
```bash
|
||||
cd client/web-sveltekit
|
||||
SOURCEGRAPH_API_URL=https://<instance> pnpm dev
|
||||
```
|
||||
|
||||
Then go to (usually) http://localhost:5173.
|
||||
|
||||
3. Against a local Sourcegraph instance
|
||||
|
||||
```bash
|
||||
sg start enterprise-sveltekit
|
||||
```
|
||||
|
||||
Then go to https://sourcegraph.test:5173.
|
||||
|
||||
### Using code from `@sourcegraph/*`
|
||||
|
||||
There are some things to consider when using code from other `@sourcegraph`
|
||||
@ -82,20 +59,7 @@ pnpm vitest # Run vitest tests
|
||||
pnpm test # Run playwright tests
|
||||
```
|
||||
|
||||
You can also run playwright tests against a running vite dev server. This is
|
||||
useful for debugging tests.
|
||||
|
||||
```sh
|
||||
# In one terminal
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh
|
||||
# In another terminal
|
||||
pnpm test:dev
|
||||
```
|
||||
|
||||
Both vitest and playwright tests are run in CI.
|
||||
In CI we run vitest tests. Playwright test support is currently being worked on.
|
||||
|
||||
### Formatting and linting
|
||||
|
||||
@ -123,34 +87,6 @@ This noise can be avoided by running the corresponding bazel command instead:
|
||||
bazel test //client/web-sveltekit:svelte-check
|
||||
```
|
||||
|
||||
### Icons
|
||||
|
||||
We use [unplugin-icons](https://github.com/unplugin/unplugin-icons) together
|
||||
with [unplugin-auto-import](https://github.com/unplugin/unplugin-auto-import)
|
||||
to manage icons. This allows us to use icons from multiple icon sets without
|
||||
having to import them manually.
|
||||
|
||||
For a list of currently available icon sets see the `@iconify-json/*` packages
|
||||
in the `package.json` file.
|
||||
|
||||
Icon references have the form `I<IconSetName><IconName>`. For example the
|
||||
[corner down left arrow from Lucide](https://lucide.dev/icons/corner-down-left)
|
||||
can be referenced as `ILucideCornerDownLeft`.
|
||||
|
||||
The icon reference is then used in the `Icon` component. Note that the icon
|
||||
doesn't have to be imported manually.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Icon } from '$lib/Icon.svelte';
|
||||
</script>
|
||||
|
||||
<Icon icon={ILucideCornerDownLeft} />
|
||||
```
|
||||
|
||||
When the development server is running, the icon will be automatically added to
|
||||
`auto-imports.d.ts` so TypeScript knows about it.
|
||||
|
||||
### Data loading with GraphQL
|
||||
|
||||
This project makes use of query composition, i.e. components define their own
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
<svg viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M30.8 51.8c-2.8.5-5.5-1.3-6-4.1L17.2 6.2c-.5-2.8 1.3-5.5 4.1-6s5.5 1.3 6 4.1l7.6 41.5c.5 2.8-1.4 5.5-4.1 6z"
|
||||
fill="var(--icon-color, #FF5543)"
|
||||
/>
|
||||
<path
|
||||
d="M10.9 44.7C9.1 45 7.3 44.4 6 43c-1.8-2.2-1.6-5.4.6-7.2L38.7 8.5c2.2-1.8 5.4-1.6 7.2.6 1.8 2.2 1.6 5.4-.6 7.2l-32 27.3c-.7.6-1.6 1-2.4 1.1z"
|
||||
fill="var(--icon-color, #A112FF)"
|
||||
/>
|
||||
<path
|
||||
d="M46.8 38.1c-.9.2-1.8.1-2.6-.2L4.4 23.8c-2.7-1-4.1-3.9-3.1-6.6 1-2.7 3.9-4.1 6.6-3.1l39.7 14.1c2.7 1 4.1 3.9 3.1 6.6-.6 1.8-2.2 3-3.9 3.3z"
|
||||
fill="var(--icon-color, #00CBEC)"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 679 B |
@ -1,13 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id=".Icon / Symbols" clip-path="url(#clip0_4465_142348)">
|
||||
<path id="Vector" d="M1.75 0.75H8.75C8.75 0.75 9.75 0.75 9.75 1.75V8.75C9.75 8.75 9.75 9.75 8.75 9.75H1.75C1.75 9.75 0.75 9.75 0.75 8.75V1.75C0.75 1.75 0.75 0.75 1.75 0.75Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path id="Vector_2" d="M14.25 5.25C14.25 5.84095 14.3664 6.42611 14.5925 6.97208C14.8187 7.51804 15.1502 8.01412 15.568 8.43198C15.9859 8.84984 16.482 9.18131 17.0279 9.40746C17.5739 9.6336 18.1591 9.75 18.75 9.75C19.3409 9.75 19.9261 9.6336 20.4721 9.40746C21.018 9.18131 21.5141 8.84984 21.932 8.43198C22.3498 8.01412 22.6813 7.51804 22.9075 6.97208C23.1336 6.42611 23.25 5.84095 23.25 5.25C23.25 4.65905 23.1336 4.07389 22.9075 3.52792C22.6813 2.98196 22.3498 2.48588 21.932 2.06802C21.5141 1.65016 21.018 1.31869 20.4721 1.09254C19.9261 0.866396 19.3409 0.75 18.75 0.75C18.1591 0.75 17.5739 0.866396 17.0279 1.09254C16.482 1.31869 15.9859 1.65016 15.568 2.06802C15.1502 2.48588 14.8187 2.98196 14.5925 3.52792C14.3664 4.07389 14.25 4.65905 14.25 5.25Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path id="Vector_3" d="M18.7859 13.9771C18.7111 13.8333 18.5982 13.7127 18.4596 13.6286C18.321 13.5445 18.162 13.5 17.9999 13.5C17.8378 13.5 17.6787 13.5445 17.5401 13.6286C17.4016 13.7127 17.2887 13.8333 17.2139 13.9771L12.8769 21.7841C12.7948 21.9334 12.7512 22.1008 12.75 22.2712C12.7488 22.4416 12.79 22.6096 12.8699 22.7601C12.9452 22.906 13.0587 23.0287 13.1984 23.1151C13.3381 23.2014 13.4987 23.2481 13.6629 23.2501H22.3369C22.5011 23.2481 22.6616 23.2014 22.8013 23.1151C22.941 23.0287 23.0546 22.906 23.1299 22.7601C23.2098 22.6096 23.251 22.4416 23.2497 22.2712C23.2485 22.1008 23.2049 21.9334 23.1229 21.7841L18.7859 13.9771Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path id="Vector_4" d="M5.85108 13.0347C5.77808 12.9463 5.68644 12.8751 5.58272 12.8262C5.47899 12.7773 5.36574 12.752 5.25108 12.752C5.13641 12.752 5.02316 12.7773 4.91944 12.8262C4.81571 12.8751 4.72407 12.9463 4.65108 13.0347L0.941077 17.4717C0.817448 17.6203 0.749756 17.8074 0.749756 18.0007C0.749756 18.194 0.817448 18.3811 0.941077 18.5297L4.64908 22.9667C4.72207 23.0551 4.81371 23.1263 4.91744 23.1752C5.02116 23.2241 5.13441 23.2494 5.24908 23.2494C5.36374 23.2494 5.47699 23.2241 5.58072 23.1752C5.68444 23.1263 5.77608 23.0551 5.84908 22.9667L9.55708 18.5297C9.68071 18.3811 9.7484 18.194 9.7484 18.0007C9.7484 17.8074 9.68071 17.6203 9.55708 17.4717L5.85108 13.0347Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4465_142348">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@ -1,276 +0,0 @@
|
||||
import type { IncomingMessage } from 'http'
|
||||
import { Transform } from 'stream'
|
||||
|
||||
import { createLogger, type Plugin, type ProxyOptions } from 'vite'
|
||||
|
||||
import { svelteKitRoutes, type SvelteKitRoute } from '../src/lib/routes'
|
||||
|
||||
interface Options {
|
||||
target: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin proxies certain requests to a real Sourcegraph instance. These include
|
||||
* - API and auth requests
|
||||
* - asset requests for the React app
|
||||
* - requests for pages that are not known by the SvelteKit app (e.g. code insights)
|
||||
*
|
||||
* It does this by first fetching the sign-in page from the real Sourcegraph instance
|
||||
* and extracting the JS context object from it. This object contains a list of
|
||||
* routes known by the server, some of which will be handled by the SvelteKit app.
|
||||
* (the other data in JS context is ignored)
|
||||
* Those that are not will be proxied to the real Sourcegraph instance.
|
||||
*
|
||||
* Additionally, the plugin injects the JS context provided by the origin server into
|
||||
* locally generated HTML pages.
|
||||
*
|
||||
* This plugin is only enabled in 'serve' mode.
|
||||
*/
|
||||
export function sgProxy(options: Options): Plugin {
|
||||
const name = 'sg:proxy'
|
||||
const logger = createLogger(undefined, { prefix: `[${name}]` })
|
||||
// Needs to be kept in sync with app.html
|
||||
const contextPlaceholder = '// ---window.context---'
|
||||
|
||||
// Additional endpoints that should be proxied to the real Sourcegraph instance.
|
||||
const additionalEndpoints = [
|
||||
// These are not part of the known routes list, but are required for the SvelteKit app to work
|
||||
// in development mode.
|
||||
'^/.api/',
|
||||
'^/.assets/',
|
||||
'^/.auth/',
|
||||
// Repo sub pages are also not part of the known routes list. They are listed here so that we
|
||||
// proxy them to the real Sourcegraph instance, for consistency with the production setup.
|
||||
'/-/raw/',
|
||||
'/-/batch-changes/?',
|
||||
'/-/settings/?',
|
||||
'/-/code-graph/?',
|
||||
'/-/own/?',
|
||||
]
|
||||
|
||||
// Routes known by the server that need to (potentially) be proxied to the real Sourcegraph instance.
|
||||
let knownServerRoutes: string[] = []
|
||||
|
||||
function extractContextRaw(body: string): string | null {
|
||||
const match = body.match(/window\.context\s*=\s*{.*}/)
|
||||
return match?.[0] ?? null
|
||||
}
|
||||
|
||||
function extractContext(body: string): Window['context'] | null {
|
||||
const context = extractContextRaw(body)
|
||||
if (!context) {
|
||||
return null
|
||||
}
|
||||
return new Function(`return ${context.match(/\{.*\}/)?.[0] ?? ''}`)()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request should be handled by the SvelteKit app. This uses similar
|
||||
* logic to the `isRouteEnabled` function in the SvelteKit app.
|
||||
* If the request
|
||||
*/
|
||||
function isHandledBySvelteKit(req: IncomingMessage, knownRoutes: string[]) {
|
||||
const url = new URL(req.url ?? '', `http://${req.headers.host}`)
|
||||
let foundRoute: SvelteKitRoute | undefined
|
||||
|
||||
for (const route of svelteKitRoutes) {
|
||||
if (route.pattern.test(url.pathname)) {
|
||||
foundRoute = route
|
||||
if (!route.isRepoRoot) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundRoute) {
|
||||
return foundRoute.isRepoRoot ? !knownRoutes.some(route => new RegExp(route).test(url.pathname)) : true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
apply: 'serve',
|
||||
async config() {
|
||||
if (!options.target) {
|
||||
logger.info('No target specified, not proxying requests', { timestamp: true })
|
||||
return
|
||||
}
|
||||
|
||||
let context: Window['context'] | null
|
||||
|
||||
// At startup we fetch the sign-in page from the real Sourcegraph instance to extract the `knownRoutes` array
|
||||
// from the JS context object. This is used to determine which requests should be proxied to the real Sourcegraph
|
||||
// instance.
|
||||
// We keep trying to connect to the origin server in case it is not yet available (e.g. when just starting up a
|
||||
// local Sourcegraph instance).
|
||||
let backoff = 1
|
||||
while (true) {
|
||||
try {
|
||||
logger.info(`Fetching JS context from ${options.target}`, { timestamp: true })
|
||||
// The /sign-in endpoint is always available on dotcom and enterprise instances.
|
||||
context = await fetch(`${options.target}/sign-in`)
|
||||
.then(response => response.text())
|
||||
.then(extractContext)
|
||||
break
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch JS context: ${(error as Error).message}`, { timestamp: true })
|
||||
logger.info(`Retrying in ${backoff} second(s)...`, { timestamp: true })
|
||||
await new Promise(resolve => setTimeout(resolve, backoff * 1000))
|
||||
backoff = Math.min(backoff * 2, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
logger.error('Failed to extract JS context from origin', { timestamp: true })
|
||||
return
|
||||
}
|
||||
|
||||
knownServerRoutes = context.svelteKit?.knownRoutes ?? []
|
||||
if (!knownServerRoutes.length) {
|
||||
logger.error('Failed to extract known routes from JS context', { timestamp: true })
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`Known routes from origin JS context\n - ${knownServerRoutes.join('\n - ')}\n`, {
|
||||
timestamp: true,
|
||||
})
|
||||
|
||||
const baseOptions: ProxyOptions = {
|
||||
target: options.target,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
headers: context.xhrHeaders,
|
||||
}
|
||||
|
||||
const proxyConfig: Record<string, ProxyOptions> = {
|
||||
// Proxy requests to specific endpoints to a real Sourcegraph instance.
|
||||
[`${additionalEndpoints.join('|')}`]: baseOptions,
|
||||
}
|
||||
|
||||
const dynamicOptions: ProxyOptions = {
|
||||
bypass(req) {
|
||||
if (!req.url) {
|
||||
return null
|
||||
}
|
||||
// If the request is for a SvelteKit route, we want to serve the SvelteKit app.
|
||||
return isHandledBySvelteKit(req, knownServerRoutes) ? req.url : null
|
||||
},
|
||||
...baseOptions,
|
||||
}
|
||||
|
||||
for (const route of knownServerRoutes) {
|
||||
// vite's proxy server matches full URL, including query parameters.
|
||||
// That means a route regex like `^/search[/]?$` (which the server provides)
|
||||
// would not match `/search?q=foo`. We extend every route regex to allow
|
||||
// for any query parameters
|
||||
proxyConfig[route.replace(/\$$/, '(\\?.*)?$')] = dynamicOptions
|
||||
}
|
||||
|
||||
return {
|
||||
server: {
|
||||
proxy: proxyConfig,
|
||||
},
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
if (!options.target) {
|
||||
return
|
||||
}
|
||||
|
||||
server.middlewares.use(function proxyHTML(req, res, next) {
|
||||
// When a request is made for an HTML page that is handled by the SvelteKit
|
||||
// we want to inject the same JS context object that we would have fetched
|
||||
// from the origin server.
|
||||
// The implementation is quite hacky but apparently but it seems there is no
|
||||
// better way to do this. It was inspired by the express compression middleware:
|
||||
// https://github.com/expressjs/compression/blob/f3e6f389cb87e090438e13c04d67cec9e22f8098/index.js
|
||||
if (req.headers.accept?.includes('html') && isHandledBySvelteKit(req, knownServerRoutes)) {
|
||||
const setHeader = res.setHeader
|
||||
const write = res.write
|
||||
const on = res.on
|
||||
const end = res.end
|
||||
|
||||
const context = fetch(`${options.target}${req.url}`, {
|
||||
headers: req.headers.cookie ? { cookie: req.headers.cookie } : {},
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(body => {
|
||||
const context = extractContextRaw(body)
|
||||
if (!context) {
|
||||
throw new Error('window.context not found in response from origin')
|
||||
}
|
||||
return context
|
||||
})
|
||||
|
||||
const transform = new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
context
|
||||
.then(context => {
|
||||
let body = Buffer.from(chunk).toString()
|
||||
if (body.includes(contextPlaceholder)) {
|
||||
body = body.replace(contextPlaceholder, context)
|
||||
logger.info(`${req.url} - injected JS context`, { timestamp: true })
|
||||
}
|
||||
callback(null, body)
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error(`Error fetching JS context: ${error.message}`, { timestamp: true })
|
||||
// We explicitly pass null to not cause the proxy to terminate
|
||||
callback(null, chunk)
|
||||
})
|
||||
},
|
||||
})
|
||||
transform
|
||||
.on('data', chunk => {
|
||||
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
|
||||
if (write.call(res, chunk) === false) {
|
||||
transform.pause()
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
// @ts-expect-error - the overload signature of end seems to prevent TS from recognizing the correct arguments
|
||||
end.call(res)
|
||||
})
|
||||
|
||||
res.on('drain', () => transform.resume())
|
||||
|
||||
let ended = false
|
||||
|
||||
res.setHeader = (name, value) => {
|
||||
// content-length is set and sent before we have a chance to modify the response
|
||||
// we need to ignore it, otherwise the browser will not render the page
|
||||
// properly
|
||||
return name === 'content-length' ? res : setHeader.call(res, name, value)
|
||||
}
|
||||
|
||||
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
|
||||
res.write = (chunk, encoding, cb) => {
|
||||
if (ended) {
|
||||
return false
|
||||
}
|
||||
return transform.write(chunk, encoding, cb)
|
||||
}
|
||||
|
||||
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
|
||||
res.end = (chunk, encoding, cb) => {
|
||||
if (ended) {
|
||||
return false
|
||||
}
|
||||
ended = true
|
||||
return transform.end(chunk, encoding, cb)
|
||||
}
|
||||
|
||||
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
|
||||
res.on = (type, listener) => {
|
||||
if (type === 'drain') {
|
||||
return transform.on(type, listener)
|
||||
}
|
||||
return on.call(res, type, listener)
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -8,15 +8,14 @@
|
||||
"build": "vite build",
|
||||
"build:preview": "vite build --mode=preview",
|
||||
"build:watch": "vite build --watch",
|
||||
"build:enterprise": "DEPLOY_TYPE=dev vite build",
|
||||
"preview": "vite preview",
|
||||
"install:browsers": "playwright install",
|
||||
"test": "DISABLE_APP_ASSETS_MOCKING=true playwright test",
|
||||
"test:dev": "DISABLE_APP_ASSETS_MOCKING=true PORT=5173 playwright test --ui",
|
||||
"test:svelte": "vitest --run",
|
||||
"sync": "svelte-kit sync",
|
||||
"check": "SK_DISABLE_PROXY=true svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "SK_DISABLE_PROXY=true svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --config ./prettier.config.cjs --write . --plugin prettier-plugin-svelte",
|
||||
"generate": "pnpm -w generate",
|
||||
@ -38,11 +37,15 @@
|
||||
"@iconify-json/mdi": "^1.1.67",
|
||||
"@iconify-json/ph": "^1.1.13",
|
||||
"@iconify-json/simple-icons": "^1.1.104",
|
||||
"@playwright/test": "1.46.0",
|
||||
"@playwright/test": "1.42.1",
|
||||
"@storybook/addon-essentials": "^8.0.5",
|
||||
"@storybook/addon-interactions": "^7.2.0",
|
||||
"@storybook/addon-links": "^7.2.0",
|
||||
"@storybook/addon-svelte-csf": "^4.1.2",
|
||||
"@storybook/blocks": "^8.0.5",
|
||||
"@storybook/svelte": "^8.0.5",
|
||||
"@storybook/sveltekit": "^8.0.5",
|
||||
"@storybook/testing-library": "0.2.0",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.5.17",
|
||||
@ -51,12 +54,13 @@
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/highlight.js": "^9.12.4",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"graphql": "^15.0.0",
|
||||
"msw": "^1.2.3",
|
||||
"msw-storybook-addon": "^1.10.0",
|
||||
"playwright": "1.46.0",
|
||||
"playwright": "1.42.1",
|
||||
"prettier": "2.8.1",
|
||||
"prettier-plugin-svelte": "^2.0.0",
|
||||
"sass": "^1.32.4",
|
||||
@ -81,17 +85,21 @@
|
||||
"@sentry/sveltekit": "^8.7.0",
|
||||
"@sourcegraph/branded": "workspace:*",
|
||||
"@sourcegraph/client-api": "workspace:*",
|
||||
"@sourcegraph/cody-web": "^0.4.0",
|
||||
"@sourcegraph/common": "workspace:*",
|
||||
"@sourcegraph/http-client": "workspace:*",
|
||||
"@sourcegraph/shared": "workspace:*",
|
||||
"@sourcegraph/telemetry": "^0.11.0",
|
||||
"@sourcegraph/web": "workspace:*",
|
||||
"@sourcegraph/wildcard": "workspace:*",
|
||||
"@storybook/test": "^8.0.5",
|
||||
"@urql/core": "^4.2.3",
|
||||
"cody-web-experimental": "^0.2.4",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"fzf": "^0.5.2",
|
||||
"highlight.js": "^10.0.0",
|
||||
"hotkeys-js": "^3.13.7",
|
||||
"mermaid": "^10.9.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"re2js": "^0.4.1",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"wonka": "^6.3.4",
|
||||
|
||||
@ -11,19 +11,11 @@ const config: PlaywrightTestConfig = {
|
||||
command: 'pnpm build:preview && pnpm preview',
|
||||
port: PORT,
|
||||
reuseExistingServer: true,
|
||||
env: {
|
||||
// Disable proxying to a real Sourcegraph instance in local testing
|
||||
SK_DISABLE_PROXY: 'true',
|
||||
},
|
||||
timeout: 5 * 60_000,
|
||||
}
|
||||
: undefined,
|
||||
reporter: 'list',
|
||||
// note: if you proxy into a locally running vite preview, you may have to raise this to 60 seconds
|
||||
timeout: process.env.BAZEL ? 60_000 : 30_000,
|
||||
expect: {
|
||||
timeout: process.env.BAZEL ? 20_000 : 5_000,
|
||||
},
|
||||
timeout: 5_000,
|
||||
use: {
|
||||
baseURL: `http://localhost:${PORT}`,
|
||||
},
|
||||
|
||||
@ -2,8 +2,5 @@ const baseConfig = require('../../prettier.config.js')
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
plugins: [...(baseConfig.plugins || []), 'prettier-plugin-svelte'],
|
||||
overrides: [
|
||||
...(baseConfig.overrides || []),
|
||||
{ files: '*.svelte', options: { parser: 'svelte', htmlWhitespaceSensitivity: 'strict' } },
|
||||
],
|
||||
overrides: [...(baseConfig.overrides || []), { files: '*.svelte', options: { parser: 'svelte' } }],
|
||||
}
|
||||
|
||||
@ -12,21 +12,22 @@
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<script ignore-csp>
|
||||
// The window.context object extracted from the origin server is injected here.
|
||||
// Needs to be kept in sync with the vite-sg-proxy.ts plugin
|
||||
// ---window.context---
|
||||
|
||||
window.context = Object.assign(
|
||||
{},
|
||||
// Injected window.context (via proxy) if available
|
||||
window.context,
|
||||
// Dev specific overwrites
|
||||
{
|
||||
sentryDSN: undefined,
|
||||
window.context = {
|
||||
// Necessary to make authenticated GraphQL requests in dev mode
|
||||
xhrHeaders: {
|
||||
'X-Requested-With': 'Sourcegraph',
|
||||
},
|
||||
// Playwright specific overwrites
|
||||
window.playwrightContext
|
||||
)
|
||||
// Local standalone dev server for dotcom can be started with
|
||||
// pnpm dev:dotcom
|
||||
sourcegraphDotComMode: !!'%sveltekit.env.PUBLIC_DOTCOM%',
|
||||
codyEnabledOnInstance: !!'%sveltekit.env.PUBLIC_CODY_ENABLED_ON_INSTANCE%',
|
||||
codyEnabledForCurrentUser: !!'%sveltekit.env.PUBLIC_CODY_ENABLED_FOR_CURRENT_USER%',
|
||||
batchChangesEnabled: !!'%sveltekit.env.PUBLIC_BATCH_CHANGES_ENABLED%',
|
||||
codeInsightsEnabled: !!'%sveltekit.env.PUBLIC_CODE_INSIGHTS_ENABLED%',
|
||||
|
||||
// The following are used to mock context in playwright tests
|
||||
...(typeof window.context === 'object' ? window.context : {}),
|
||||
}
|
||||
window.pageError = undefined
|
||||
</script>
|
||||
|
||||
|
||||
11
client/web-sveltekit/src/auto-imports.d.ts
vendored
11
client/web-sveltekit/src/auto-imports.d.ts
vendored
@ -11,7 +11,6 @@ declare global {
|
||||
const ILucideArchive: typeof import('~icons/lucide/archive')['default']
|
||||
const ILucideArrowDownFromLine: typeof import('~icons/lucide/arrow-down-from-line')['default']
|
||||
const ILucideArrowLeftFromLine: typeof import('~icons/lucide/arrow-left-from-line')['default']
|
||||
const ILucideArrowRight: typeof import('~icons/lucide/arrow-right')['default']
|
||||
const ILucideArrowRightFromLine: typeof import('~icons/lucide/arrow-right-from-line')['default']
|
||||
const ILucideBarChartBig: typeof import('~icons/lucide/bar-chart-big')['default']
|
||||
const ILucideBookOpen: typeof import('~icons/lucide/book-open')['default']
|
||||
@ -40,9 +39,9 @@ declare global {
|
||||
const ILucideEllipsis: typeof import('~icons/lucide/ellipsis')['default']
|
||||
const ILucideExternalLink: typeof import('~icons/lucide/external-link')['default']
|
||||
const ILucideEye: typeof import('~icons/lucide/eye')['default']
|
||||
const ILucideFIleText: typeof import('~icons/lucide/f-ile-text')['default']
|
||||
const ILucideFile: typeof import('~icons/lucide/file')['default']
|
||||
const ILucideFileCode: typeof import('~icons/lucide/file-code')['default']
|
||||
const ILucideFileDiff: typeof import('~icons/lucide/file-diff')['default']
|
||||
const ILucideFileJson: typeof import('~icons/lucide/file-json')['default']
|
||||
const ILucideFileSearch2: typeof import('~icons/lucide/file-search2')['default']
|
||||
const ILucideFileStack: typeof import('~icons/lucide/file-stack')['default']
|
||||
@ -62,14 +61,12 @@ declare global {
|
||||
const ILucideGitCompareArrows: typeof import('~icons/lucide/git-compare-arrows')['default']
|
||||
const ILucideGitFork: typeof import('~icons/lucide/git-fork')['default']
|
||||
const ILucideGitMerge: typeof import('~icons/lucide/git-merge')['default']
|
||||
const ILucideHelp: typeof import('~icons/lucide/help')['default']
|
||||
const ILucideHistory: typeof import('~icons/lucide/history')['default']
|
||||
const ILucideHome: typeof import('~icons/lucide/home')['default']
|
||||
const ILucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
const ILucideLink: typeof import('~icons/lucide/link')['default']
|
||||
const ILucideLock: typeof import('~icons/lucide/lock')['default']
|
||||
const ILucideMenu: typeof import('~icons/lucide/menu')['default']
|
||||
const ILucideNetwork: typeof import('~icons/lucide/network')['default']
|
||||
const ILucideOctagonX: typeof import('~icons/lucide/octagon-x')['default']
|
||||
const ILucidePanelBottomClose: typeof import('~icons/lucide/panel-bottom-close')['default']
|
||||
const ILucidePanelLeftClose: typeof import('~icons/lucide/panel-left-close')['default']
|
||||
@ -77,7 +74,6 @@ declare global {
|
||||
const ILucidePencil: typeof import('~icons/lucide/pencil')['default']
|
||||
const ILucideRegex: typeof import('~icons/lucide/regex')['default']
|
||||
const ILucideRepeat: typeof import('~icons/lucide/repeat')['default']
|
||||
const ILucideScanSearch: typeof import('~icons/lucide/scan-search')['default']
|
||||
const ILucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
const ILucideSearchX: typeof import('~icons/lucide/search-x')['default']
|
||||
const ILucideSettings: typeof import('~icons/lucide/settings')['default']
|
||||
@ -85,16 +81,15 @@ declare global {
|
||||
const ILucideSquareFunction: typeof import('~icons/lucide/square-function')['default']
|
||||
const ILucideSquareSlash: typeof import('~icons/lucide/square-slash')['default']
|
||||
const ILucideStar: typeof import('~icons/lucide/star')['default']
|
||||
const ILucideSymbols: typeof import('~icons/lucide/symbols')['default']
|
||||
const ILucideTag: typeof import('~icons/lucide/tag')['default']
|
||||
const ILucideText: typeof import('~icons/lucide/text')['default']
|
||||
const ILucideUser: typeof import('~icons/lucide/user')['default']
|
||||
const ILucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
const ILucideWrapText: typeof import('~icons/lucide/wrap-text')['default']
|
||||
const ILucideX: typeof import('~icons/lucide/x')['default']
|
||||
const ILucidehevronLeft: typeof import('~icons/lucide/hevron-left')['default']
|
||||
const IMdiFormatLetterCase: typeof import('~icons/mdi/format-letter-case')['default']
|
||||
const IMdiRegex: typeof import('~icons/mdi/regex')['default']
|
||||
const IMdiTools: typeof import('~icons/mdi/tools')['default']
|
||||
const IPhFileJpgLight: typeof import('~icons/ph/file-jpg-light')['default']
|
||||
const IPhFilePngLight: typeof import('~icons/ph/file-png-light')['default']
|
||||
const IPhGifFill: typeof import('~icons/ph/gif-fill')['default']
|
||||
@ -105,8 +100,6 @@ declare global {
|
||||
const IPhosphorePngLight: typeof import('~icons/ph/osphore-png-light')['default']
|
||||
const ISgBatchChanges: typeof import('~icons/sg/batch-changes')['default']
|
||||
const ISgCody: typeof import('~icons/sg/cody')['default']
|
||||
const ISgMark: typeof import('~icons/sg/mark')['default']
|
||||
const ISgSymbols: typeof import('~icons/sg/symbols')['default']
|
||||
const ISimpleIconsApachegroovy: typeof import('~icons/simple-icons/apachegroovy')['default']
|
||||
const ISimpleIconsBitbucket: typeof import('~icons/simple-icons/bitbucket')['default']
|
||||
const ISimpleIconsC: typeof import('~icons/simple-icons/c')['default']
|
||||
|
||||
@ -32,13 +32,14 @@
|
||||
{#if avatarURL}
|
||||
<img src={avatarURL} role="presentation" aria-hidden="true" alt="Avatar of {name}" data-avatar />
|
||||
{:else}
|
||||
<div data-avatar title={name}>
|
||||
<div data-avatar>
|
||||
<span>{getInitials(name)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
z-index: 1;
|
||||
color: var(--text-muted);
|
||||
font-size: calc(var(--size) * 0.5);
|
||||
font-weight: 500;
|
||||
@ -49,21 +50,29 @@
|
||||
--min-size: 1.25rem;
|
||||
--size: var(--avatar-size, var(--icon-inline-size, var(--min-size)));
|
||||
|
||||
flex: none;
|
||||
|
||||
min-width: var(--min-size);
|
||||
min-height: var(--min-size);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
|
||||
isolation: isolate;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 50%;
|
||||
text-transform: capitalize;
|
||||
color: var(--color-bg-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: var(--secondary);
|
||||
user-select: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
div::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
fragment Changelist on PerforceChangelist {
|
||||
cid
|
||||
canonicalURL
|
||||
commit {
|
||||
message
|
||||
oid
|
||||
body
|
||||
subject
|
||||
author {
|
||||
person {
|
||||
...Avatar_Person
|
||||
}
|
||||
date
|
||||
}
|
||||
parents {
|
||||
id
|
||||
oid
|
||||
abbreviatedOID
|
||||
parent: perforceChangelist {
|
||||
cid
|
||||
canonicalURL
|
||||
}
|
||||
}
|
||||
perforceChangelist {
|
||||
cid
|
||||
canonicalURL
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
<svelte:options immutable />
|
||||
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/Avatar.svelte'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import Timestamp from '$lib/Timestamp.svelte'
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
|
||||
import type { Changelist } from './Changelist.gql'
|
||||
import { isViewportMobile } from './stores'
|
||||
import Button from './wildcard/Button.svelte'
|
||||
|
||||
export let changelist: Changelist
|
||||
export let alwaysExpanded: boolean = false
|
||||
|
||||
$: expanded = alwaysExpanded
|
||||
|
||||
$: author = changelist.commit.author
|
||||
$: commitDate = new Date(author.date)
|
||||
$: authorAvatarTooltip = author.person.name + (author ? ' (author)' : '')
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="avatar">
|
||||
<Tooltip tooltip={authorAvatarTooltip}>
|
||||
<Avatar avatar={author.person} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="title">
|
||||
<!-- TODO need subject-->
|
||||
<a class="subject" href={changelist.canonicalURL}>{changelist.commit.subject}</a>
|
||||
{#if !alwaysExpanded && changelist.commit.body && !$isViewportMobile}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={() => (expanded = !expanded)}
|
||||
aria-label="{expanded ? 'Hide' : 'Show'} changelist message"
|
||||
>
|
||||
<Icon icon={ILucideEllipsis} inline aria-hidden />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="author">
|
||||
submitted by <strong>{author.person.name}</strong>
|
||||
<Timestamp date={commitDate} />
|
||||
</div>
|
||||
{#if changelist.commit.body}
|
||||
<div class="message" class:expanded>
|
||||
{#if $isViewportMobile}
|
||||
{#if expanded}
|
||||
<Button variant="secondary" size="lg" display="block" on:click={() => (expanded = false)}>
|
||||
Close
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="secondary" size="sm" display="block" on:click={() => (expanded = true)}>
|
||||
Show changelist message
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<pre>{changelist.commit.body}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas: 'avatar title' 'avatar author' '. message';
|
||||
column-gap: 1rem;
|
||||
|
||||
@media (--mobile) {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas: 'avatar title' 'author author' 'message message';
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
grid-area: avatar;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
align-self: center;
|
||||
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
.subject {
|
||||
font-weight: 600;
|
||||
flex: 0 1 auto;
|
||||
color: var(--body-color);
|
||||
min-width: 0;
|
||||
|
||||
@media (--sm-breakpoint-up) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
grid-area: author;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.message {
|
||||
grid-area: message;
|
||||
overflow: hidden;
|
||||
|
||||
@media (--mobile) {
|
||||
&.expanded {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
display: none;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
.expanded & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -71,7 +71,7 @@
|
||||
padding: '0 1.5ex',
|
||||
},
|
||||
'.cm-line': {
|
||||
lineHeight: 'var(--code-line-height)',
|
||||
lineHeight: '1.54',
|
||||
padding: '0',
|
||||
},
|
||||
'.selected-line': {
|
||||
@ -131,7 +131,6 @@
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import { goto } from '$app/navigation'
|
||||
import { getExplorePanelContext } from '$lib/codenav/ExplorePanel.svelte'
|
||||
import type { LineOrPositionOrRange } from '$lib/common'
|
||||
import { type CodeIntelAPI, Occurrence } from '$lib/shared'
|
||||
import {
|
||||
@ -167,7 +166,7 @@
|
||||
getScrollSnapshot as getScrollSnapshot_internal,
|
||||
} from './codemirror/utils'
|
||||
import { registerHotkey } from './Hotkey'
|
||||
import { goToDefinition } from './repo/blob'
|
||||
import { goToDefinition, openImplementations, openReferences } from './repo/blob'
|
||||
import { createLocalWritable } from './stores'
|
||||
|
||||
export let blobInfo: BlobInfo
|
||||
@ -230,23 +229,15 @@
|
||||
filePath: blobInfo.filePath,
|
||||
languages: blobInfo.languages,
|
||||
}
|
||||
const { openReferences, openDefinitions, openImplementations } = getExplorePanelContext()
|
||||
$: codeIntelExtension = codeIntelAPI
|
||||
? createCodeIntelExtension({
|
||||
api: {
|
||||
api: codeIntelAPI,
|
||||
documentInfo: documentInfo,
|
||||
goToDefinition: (view, definition, options) => {
|
||||
if (definition.type === 'multiple') {
|
||||
// Open the explore panel with the definitions
|
||||
openDefinitions({ documentInfo, occurrence: definition.occurrence })
|
||||
} else {
|
||||
goToDefinition(documentInfo, view, definition, options)
|
||||
}
|
||||
},
|
||||
openReferences: (_view, documentInfo, occurrence) => openReferences({ documentInfo, occurrence }),
|
||||
openImplementations: (_view, documentInfo, occurrence) =>
|
||||
openImplementations({ documentInfo, occurrence }),
|
||||
goToDefinition: (view, definition, options) =>
|
||||
goToDefinition(documentInfo, view, definition, options),
|
||||
openReferences,
|
||||
openImplementations,
|
||||
createTooltipView: options => new HovercardView(options.view, options.token, options.hovercardData),
|
||||
},
|
||||
// TODO(fkling): Support tooltip pinning
|
||||
|
||||
@ -7,8 +7,6 @@
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
|
||||
import type { Commit } from './Commit.gql'
|
||||
import { isViewportMobile } from './stores'
|
||||
import Button from './wildcard/Button.svelte'
|
||||
|
||||
export let commit: Commit
|
||||
export let alwaysExpanded: boolean = false
|
||||
@ -24,13 +22,12 @@
|
||||
return committer
|
||||
}
|
||||
|
||||
$: expanded = alwaysExpanded
|
||||
|
||||
$: author = commit.author
|
||||
$: committer = getCommitter(commit) ?? author
|
||||
$: committerIsAuthor = committer.person.email === author.person.email
|
||||
$: commitDate = new Date(committer.date)
|
||||
$: authorAvatarTooltip = author.person.name + (committer ? ' (author)' : '')
|
||||
let expanded = alwaysExpanded
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
@ -38,79 +35,54 @@
|
||||
<Tooltip tooltip={authorAvatarTooltip}>
|
||||
<Avatar avatar={author.person} />
|
||||
</Tooltip>
|
||||
{#if !committerIsAuthor}
|
||||
</div>
|
||||
{#if !committerIsAuthor}
|
||||
<div class="avatar">
|
||||
<Tooltip tooltip="{committer.person.name} (committer)">
|
||||
<Avatar avatar={committer.person} />
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="title">
|
||||
<a class="subject" href={commit.canonicalURL}>{commit.subject}</a>
|
||||
{#if !alwaysExpanded && commit.body && !$isViewportMobile}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={() => (expanded = !expanded)}
|
||||
aria-label="{expanded ? 'Hide' : 'Show'} commit message"
|
||||
>
|
||||
<Icon icon={ILucideEllipsis} inline aria-hidden />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="author">
|
||||
{#if !committerIsAuthor}authored by <strong>{author.person.name}</strong> and{/if}
|
||||
committed by <strong>{committer.person.name}</strong>
|
||||
<Timestamp date={commitDate} />
|
||||
</div>
|
||||
{#if commit.body}
|
||||
<div class="message" class:expanded>
|
||||
{#if $isViewportMobile}
|
||||
{#if expanded}
|
||||
<Button variant="secondary" size="lg" display="block" on:click={() => (expanded = false)}>
|
||||
Close
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="secondary" size="sm" display="block" on:click={() => (expanded = true)}>
|
||||
Show commit message
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<pre>{commit.body}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="info">
|
||||
<span class="title">
|
||||
<a class="subject" href={commit.canonicalURL}>{commit.subject}</a>
|
||||
{#if !alwaysExpanded && commit.body}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (expanded = !expanded)}
|
||||
aria-label="{expanded ? 'Hide' : 'Show'} commit message"
|
||||
>
|
||||
<Icon icon={ILucideEllipsis} inline aria-hidden />
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
<span>
|
||||
{#if !committerIsAuthor}authored by <strong>{author.person.name}</strong> and{/if}
|
||||
committed by <strong>{committer.person.name}</strong>
|
||||
<Timestamp date={commitDate} />
|
||||
</span>
|
||||
{#if expanded && commit.body}
|
||||
<pre>{commit.body}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas: 'avatar title' 'avatar author' '. message';
|
||||
column-gap: 1rem;
|
||||
|
||||
@media (--mobile) {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas: 'avatar title' 'author author' 'message message';
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
grid-area: avatar;
|
||||
.info {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-self: center;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
align-self: center;
|
||||
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
.subject {
|
||||
font-weight: 600;
|
||||
@ -126,50 +98,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
grid-area: author;
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.message {
|
||||
grid-area: message;
|
||||
overflow: hidden;
|
||||
button {
|
||||
color: var(--body-color);
|
||||
border: 1px solid var(--secondary);
|
||||
cursor: pointer;
|
||||
|
||||
@media (--mobile) {
|
||||
&.expanded {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
@media (--xs-breakpoint-down) {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
display: none;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
overflow: visible;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
.expanded & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,36 +4,30 @@ A component to display the keyboard shortcuts for the application.
|
||||
<script lang="ts">
|
||||
import { isMacPlatform } from '$lib/common'
|
||||
import { formatShortcutParts, type Keys } from '$lib/Hotkey'
|
||||
import { isViewportMobile } from './stores'
|
||||
|
||||
export let shortcut: Keys
|
||||
export let inline: boolean = false
|
||||
|
||||
const separator = isMacPlatform() ? '' : '+'
|
||||
|
||||
// No need to do this work if we are on a mobile device
|
||||
$: parts = $isViewportMobile
|
||||
? []
|
||||
: (() => {
|
||||
const result: string[] = []
|
||||
let parts = formatShortcutParts(shortcut)
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
result.push(separator)
|
||||
}
|
||||
result.push(parts[i])
|
||||
}
|
||||
return result
|
||||
})()
|
||||
$: parts = (() => {
|
||||
const result: string[] = []
|
||||
let parts = formatShortcutParts(shortcut)
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
result.push(separator)
|
||||
}
|
||||
result.push(parts[i])
|
||||
}
|
||||
return result
|
||||
})()
|
||||
</script>
|
||||
|
||||
{#if !$isViewportMobile}
|
||||
<kbd class:inline>
|
||||
{#each parts as part}
|
||||
<span>{part}</span>
|
||||
{/each}
|
||||
</kbd>
|
||||
{/if}
|
||||
<kbd class:inline>
|
||||
{#each parts as part}
|
||||
<span>{part}</span>
|
||||
{/each}
|
||||
</kbd>
|
||||
|
||||
<style lang="scss">
|
||||
kbd {
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
import { Story } from '@storybook/addon-svelte-csf'
|
||||
|
||||
import LoadingSpinner from './LoadingSpinner.svelte'
|
||||
|
||||
export const meta = {
|
||||
component: LoadingSpinner,
|
||||
}
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
<h3>Default</h3>
|
||||
<div class="wrapper">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
|
||||
<h3>--size="2rem"</h3>
|
||||
<div class="wrapper">
|
||||
<LoadingSpinner --size="2rem" />
|
||||
</div>
|
||||
|
||||
<h3>center={false}</h3>
|
||||
<div class="wrapper">
|
||||
<LoadingSpinner center={false} />
|
||||
</div>
|
||||
|
||||
<h3>inline={true}</h3>
|
||||
<div class="wrapper">
|
||||
<LoadingSpinner inline />
|
||||
</div>
|
||||
<button>
|
||||
<LoadingSpinner inline />
|
||||
<span>Loading...</span>
|
||||
</button>
|
||||
<br />
|
||||
<button style="font-size: 48px">
|
||||
<LoadingSpinner inline />
|
||||
<span>Loading...</span>
|
||||
</button>
|
||||
</Story>
|
||||
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
margin: 1rem;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
button span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
export let center = true
|
||||
</script>
|
||||
|
||||
<div class:center class:inline>
|
||||
<div class="loading-spinner" aria-label="loading" aria-live="polite" />
|
||||
<div class:center>
|
||||
<div class="loading-spinner" class:inline aria-label="loading" aria-live="polite" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@ -14,11 +14,6 @@
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
@ -33,12 +28,13 @@
|
||||
|
||||
width: var(--size, 1rem);
|
||||
height: var(--size, 1rem);
|
||||
.inline & {
|
||||
&.inline {
|
||||
width: #{(16 / 14)}em;
|
||||
height: #{(16 / 14)}em;
|
||||
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
import { createHotkey } from '$lib/Hotkey'
|
||||
|
||||
import { popover, onClickOutside, portal } from './dom'
|
||||
import { isViewportMobile } from './stores'
|
||||
|
||||
/**
|
||||
* Show the popover when hovering over the trigger.
|
||||
@ -16,7 +15,6 @@
|
||||
export let hoverDelay: number = 500
|
||||
export let hoverCloseDelay: number = 150
|
||||
export let closeOnEsc: boolean = true
|
||||
export let flip: boolean = true
|
||||
export let trigger: HTMLElement | null = null
|
||||
export let target: HTMLElement | undefined = undefined
|
||||
|
||||
@ -96,6 +94,7 @@
|
||||
trigger.addEventListener('mouseenter', handleMouseEnterTrigger)
|
||||
trigger.addEventListener('mouseleave', handleMouseLeaveTrigger)
|
||||
trigger.addEventListener('mousemove', handleMouseMoveTrigger)
|
||||
trigger.addEventListener('click', close)
|
||||
window.addEventListener('blur', close)
|
||||
}
|
||||
|
||||
@ -103,6 +102,7 @@
|
||||
trigger.removeEventListener('mouseenter', handleMouseEnterTrigger)
|
||||
trigger.removeEventListener('mouseleave', handleMouseLeaveTrigger)
|
||||
trigger.removeEventListener('mousemove', handleMouseMoveTrigger)
|
||||
trigger.removeEventListener('click', close)
|
||||
window.removeEventListener('blur', close)
|
||||
}
|
||||
|
||||
@ -111,9 +111,7 @@
|
||||
let oldTrigger: HTMLElement | null
|
||||
$: {
|
||||
oldTrigger && showOnHover && unwatchTrigger(oldTrigger)
|
||||
if (!$isViewportMobile) {
|
||||
trigger && showOnHover && watchTrigger(trigger)
|
||||
}
|
||||
trigger && showOnHover && watchTrigger(trigger)
|
||||
oldTrigger = trigger
|
||||
}
|
||||
|
||||
@ -157,9 +155,6 @@
|
||||
placement,
|
||||
offset,
|
||||
shift: { padding: 4 },
|
||||
flip: flip ? {
|
||||
fallbackAxisSideDirection: 'start',
|
||||
} : undefined,
|
||||
},
|
||||
}}
|
||||
on:click-outside={handleClickOutside}
|
||||
@ -183,7 +178,7 @@
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
border-radius: var(--popover-border-radius);
|
||||
// Ensure child elements do not overflow the border radius
|
||||
overflow-y: scroll;
|
||||
overflow: hidden;
|
||||
|
||||
// We always display the popover on hover, but there may not be anything
|
||||
// inside until something we load something. This ensures we do not
|
||||
|
||||
@ -8,11 +8,9 @@
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
|
||||
export let margin: number
|
||||
export let viewport: HTMLElement | undefined = undefined
|
||||
export let scroller: HTMLElement | undefined = undefined
|
||||
|
||||
export function capture(): Capture {
|
||||
return { scroll: scroller?.scrollTop || 0 }
|
||||
return { scroll: scroller.scrollTop }
|
||||
}
|
||||
|
||||
export function restore(data?: Capture) {
|
||||
@ -33,13 +31,14 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{ more: void }>()
|
||||
|
||||
function handleScroll() {
|
||||
if (scroller && viewport) {
|
||||
const remaining = scroller.scrollHeight - (scroller.scrollTop + (viewport?.clientHeight ?? 0))
|
||||
let viewport: HTMLElement
|
||||
let scroller: HTMLElement
|
||||
|
||||
if (remaining < margin) {
|
||||
dispatch('more')
|
||||
}
|
||||
function handleScroll() {
|
||||
const remaining = scroller.scrollHeight - (scroller.scrollTop + viewport.clientHeight)
|
||||
|
||||
if (remaining < margin) {
|
||||
dispatch('more')
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,13 +46,13 @@
|
||||
// This premptively triggers a 'more' event when the scrollable content is smaller than than
|
||||
// scroller. Without this, the 'more' event would not be triggered because there is nothing
|
||||
// to scroll.
|
||||
if (scroller && scroller.scrollHeight <= scroller.clientHeight) {
|
||||
if (scroller.scrollHeight <= scroller.clientHeight) {
|
||||
dispatch('more')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="viewport" bind:this={viewport} data-viewport>
|
||||
<div class="viewport" bind:this={viewport}>
|
||||
<div class="scroller" bind:this={scroller} on:scroll={handleScroll} data-scroller>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
45
client/web-sveltekit/src/lib/Separator.stories.svelte
Normal file
45
client/web-sveltekit/src/lib/Separator.stories.svelte
Normal file
@ -0,0 +1,45 @@
|
||||
<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>
|
||||
|
||||
<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 {
|
||||
display: flex;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.left {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
</style>
|
||||
106
client/web-sveltekit/src/lib/Separator.svelte
Normal file
106
client/web-sveltekit/src/lib/Separator.svelte
Normal file
@ -0,0 +1,106 @@
|
||||
<script lang="ts" context="module">
|
||||
import { derived, type Writable } from 'svelte/store'
|
||||
|
||||
import { createLocalWritable } from '$lib/stores'
|
||||
|
||||
const dividerStore = createLocalWritable<Record<string, number>>('dividers', {})
|
||||
|
||||
export function getSeparatorPosition(name: string, defaultValue: number): Writable<number> {
|
||||
const { subscribe } = derived(dividerStore, dividers => dividers[name] ?? defaultValue)
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set(value) {
|
||||
dividerStore.update(dividers => ({ ...dividers, [name]: value }))
|
||||
},
|
||||
update(updater) {
|
||||
dividerStore.update(dividers => ({ ...dividers, [name]: updater(dividers[name]) }))
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Store to write current position (0-1) to.
|
||||
*/
|
||||
export let currentPosition: Writable<number>
|
||||
|
||||
let divider: HTMLElement | null = null
|
||||
let offset = 0
|
||||
let dragging = false
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
if (divider?.parentElement) {
|
||||
let width = (event.x - offset) / divider.parentElement.clientWidth
|
||||
if (width < 0) {
|
||||
width = 0
|
||||
} else if (width > 1) {
|
||||
width = 1
|
||||
}
|
||||
$currentPosition = width
|
||||
}
|
||||
}
|
||||
|
||||
function endResize() {
|
||||
dragging = false
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', endResize)
|
||||
}
|
||||
|
||||
function startResize(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
if (divider?.parentElement) {
|
||||
dragging = true
|
||||
offset = divider.parentElement.getBoundingClientRect().x + divider.clientWidth
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', endResize)
|
||||
}
|
||||
}
|
||||
</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"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={$currentPosition}
|
||||
class:dragging
|
||||
on:mousedown={startResize}
|
||||
>
|
||||
<!-- spacer is used to increase the interactable surface-->
|
||||
<div class="spacer" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div[role='separator'] {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
width: 1px;
|
||||
background-color: var(--border-color);
|
||||
cursor: col-resize;
|
||||
|
||||
.spacer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -5px;
|
||||
margin-left: -50%;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
z-index: 1;
|
||||
outline: 1px solid var(--oc-blue-3);
|
||||
background-color: var(--oc-blue-3);
|
||||
}
|
||||
|
||||
&:hover:not(.dragging) {
|
||||
z-index: 1;
|
||||
outline: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -26,10 +26,6 @@
|
||||
*/
|
||||
export let selected: number | null = 0
|
||||
export let toggable = false
|
||||
/**
|
||||
* Whether or not to show the tab header when there is only one tab.
|
||||
*/
|
||||
export let showSingleTabHeader = false
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: number | null }>()
|
||||
const id = uuid.v4()
|
||||
@ -70,14 +66,12 @@
|
||||
</script>
|
||||
|
||||
<div class="tabs" data-tabs>
|
||||
{#if $tabs.length > 1 || showSingleTabHeader}
|
||||
<header>
|
||||
<TabsHeader {id} tabs={$tabs} selected={$selectedTab} on:select={selectTab} />
|
||||
<div class="actions">
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
<header>
|
||||
<TabsHeader {id} tabs={$tabs} selected={$selectedTab} on:select={selectTab} />
|
||||
<div class="actions">
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</header>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -93,12 +87,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 2rem;
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
margin-right: var(--tabs-horizontal-spacing);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +66,8 @@
|
||||
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: var(--tabs-header-align, flex-start);
|
||||
justify-content: var(--align-tabs, center);
|
||||
gap: var(--tabs-gap, 0);
|
||||
}
|
||||
|
||||
[role='tab'] {
|
||||
@ -76,7 +77,7 @@
|
||||
align-items: center;
|
||||
min-height: 2rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
color: var(--text-body);
|
||||
color: var(--text-muted);
|
||||
display: inline-flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
@ -113,15 +114,13 @@
|
||||
span {
|
||||
display: inline-block;
|
||||
|
||||
&[data-tab-title] {
|
||||
// Hidden rendering of the bold tab title to prevent
|
||||
// shifting when the tab is selected.
|
||||
&::before {
|
||||
content: attr(data-tab-title);
|
||||
display: block;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
// Hidden rendering of the bold tab title to prevent
|
||||
// shifting when the tab is selected.
|
||||
&::before {
|
||||
content: attr(data-tab-title);
|
||||
display: block;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte'
|
||||
|
||||
import { type PopoverOptions, popover, portal, uniqueID } from './dom'
|
||||
import { popover, portal, uniqueID } from './dom'
|
||||
|
||||
/**
|
||||
* The content of the tooltip.
|
||||
@ -43,24 +41,8 @@
|
||||
shift: {
|
||||
padding: 4,
|
||||
},
|
||||
onSize(element, { availableWidth, availableHeight }) {
|
||||
Object.assign(element.style, {
|
||||
maxWidth: `min(var(--tooltip-max-width), ${availableWidth}px)`,
|
||||
maxHeight: `${availableHeight}px`,
|
||||
})
|
||||
},
|
||||
} satisfies PopoverOptions
|
||||
|
||||
$: if (target && tooltip) {
|
||||
target.setAttribute('aria-label', tooltip)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// We need to wait for the element to be rendered before we can check whether it
|
||||
// is part of the layout.
|
||||
// (this fixes and issue where the tooltip would not show up in hovercards)
|
||||
await tick()
|
||||
|
||||
$: {
|
||||
let node = wrapper?.firstElementChild
|
||||
// Use `getClientRects` to check if the element is part of the layout.
|
||||
// For example, an element with `display: contents` will not be part of the layout.
|
||||
@ -72,7 +54,10 @@
|
||||
if (node) {
|
||||
target = node
|
||||
}
|
||||
})
|
||||
}
|
||||
$: if (target && tooltip) {
|
||||
target.setAttribute('aria-label', tooltip)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: close tooltip on escape -->
|
||||
@ -88,16 +73,12 @@
|
||||
on:mouseleave={hide}
|
||||
on:focusin={show}
|
||||
on:focusout={hide}
|
||||
data-tooltip-root><!--
|
||||
--><slot /><!--
|
||||
--></div
|
||||
><!--
|
||||
-->{#if (alwaysVisible || visible) && target && tooltip}<div
|
||||
role="tooltip"
|
||||
{id}
|
||||
use:popover={{ reference: target, options }}
|
||||
use:portal
|
||||
>
|
||||
data-tooltip-root
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{#if (alwaysVisible || visible) && target && tooltip}
|
||||
<div role="tooltip" {id} use:popover={{ reference: target, options }} use:portal>
|
||||
<div class="content">{tooltip}</div>
|
||||
<div data-arrow />
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
$: selected = $treeState.selected === nodeID
|
||||
$: tabindex = $treeState.focused === nodeID ? 0 : -1
|
||||
$: children = expandable && expanded ? treeProvider.fetchChildren(entry) : null
|
||||
$: disableScope = $treeState.disableScope
|
||||
|
||||
let level = getContext<TreeNodeContext>('tree-node-nesting')?.level ?? 0
|
||||
setContext('tree-node-nesting', { level: level + 1 })
|
||||
@ -77,37 +76,30 @@
|
||||
{tabindex}
|
||||
data-treeitem
|
||||
data-node-id={nodeID}
|
||||
class:disable-scope={disableScope}
|
||||
style="--tree-node-nested-level: {level}"
|
||||
>
|
||||
<div bind:this={label} class="label" data-treeitem-label class:expandable>
|
||||
<!-- TODO: scoping is an operation specific to the file tree, but this
|
||||
is intended to be a generic tree component. We should not add a scope
|
||||
button here. -->
|
||||
<span bind:this={label} class="label" data-treeitem-label class:expandable>
|
||||
<Button variant="icon" on:click={handleScopeChange} data-scope-button>
|
||||
<Icon icon={ILucideFocus} inline aria-hidden="true" />
|
||||
</Button>
|
||||
<!-- hide the open/close button to preserve alignment with expandable entries -->
|
||||
<div class="indented">
|
||||
{#if expandable}
|
||||
<!-- We have to stop even propagation because the tree root
|
||||
listens for click events for selecting items. We don't want the
|
||||
item to be selected when the open/close button is pressed. -->
|
||||
<Button
|
||||
variant="icon"
|
||||
on:click={event => {
|
||||
event.stopPropagation()
|
||||
toggleOpen()
|
||||
}}
|
||||
tabindex={-1}
|
||||
aria-label="{expanded ? 'Collapse' : 'Expand'} subtree"
|
||||
>
|
||||
<Icon icon={expanded ? ILucideChevronDown : ILucideChevronRight} inline aria-hidden="true" />
|
||||
</Button>
|
||||
{/if}
|
||||
<slot {entry} {expanded} toggle={toggleOpen} {label} />
|
||||
</div>
|
||||
</div>
|
||||
{#if expandable}
|
||||
<!-- We have to stop even propagation because the tree root listens for click events for
|
||||
selecting items. We don't want the item to be selected when the open/close button is pressed.
|
||||
-->
|
||||
<Button
|
||||
variant="icon"
|
||||
on:click={event => {
|
||||
event.stopPropagation()
|
||||
toggleOpen()
|
||||
}}
|
||||
tabindex={-1}
|
||||
>
|
||||
<Icon icon={expanded ? ILucideChevronDown : ILucideChevronRight} inline />
|
||||
</Button>
|
||||
{/if}
|
||||
<slot {entry} {expanded} toggle={toggleOpen} {label} />
|
||||
</span>
|
||||
{#if expanded && children}
|
||||
{#await children}
|
||||
<div class="loading">
|
||||
@ -130,74 +122,68 @@
|
||||
<style lang="scss">
|
||||
$shiftWidth: 1.25rem;
|
||||
$gap: 0.25rem;
|
||||
$indentSize: calc(var(--tree-node-nested-level) * #{$shiftWidth});
|
||||
|
||||
li[role='treeitem'] {
|
||||
--scope-size: calc(var(--icon-inline-size) + #{$gap} - 1px);
|
||||
&.disable-scope {
|
||||
--scope-size: 0px;
|
||||
:global([data-scope-button]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
[role='treeitem'] {
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&[tabindex='0']:focus {
|
||||
box-shadow: none;
|
||||
|
||||
> .label {
|
||||
box-shadow: var(--focus-shadow-inset);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
padding: 0.2rem $gap;
|
||||
align-items: center;
|
||||
|
||||
// Change icon color based on selected item state
|
||||
--icon-color: var(--tree-node-expand-icon-color);
|
||||
color: var(--tree-node-label-color, var(--text-body));
|
||||
|
||||
:global([data-scope-button]) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.expandable:hover,
|
||||
&.expandable:focus {
|
||||
:global([data-scope-button]) {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.indented {
|
||||
display: inherit;
|
||||
gap: inherit;
|
||||
margin-left: $indentSize;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
// Indent with two rem since loading represents next nested level
|
||||
margin-left: calc(var(--scope-size) + #{$indentSize} + 2 * #{$gap});
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
// The visual guide line for expanded subtrees
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-left: 1px solid var(--secondary);
|
||||
height: 100%;
|
||||
transform: translateX(
|
||||
calc(#{$gap} + var(--scope-size) + #{$indentSize} + var(--icon-inline-size) / 2 - 1px)
|
||||
);
|
||||
z-index: 1;
|
||||
box-shadow: var(--focus-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
// Indent with two rem since loading represents next nested level
|
||||
margin-left: calc(var(--tree-node-nested-level) * #{$shiftWidth} + 2 * var(--icon-inline-size) + 2 * #{$gap});
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
padding: 0.2rem $gap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
// Change icon color based on selected item state
|
||||
--icon-color: var(--tree-node-expand-icon-color);
|
||||
color: var(--tree-node-label-color, var(--text-body));
|
||||
|
||||
li[data-treeitem][aria-selected='true'] > & {
|
||||
--icon-color: currentColor;
|
||||
--file-icon-color: currentColor;
|
||||
|
||||
color: var(--tree-node-label-color, var(--body-bg));
|
||||
}
|
||||
|
||||
:global([data-scope-button]) {
|
||||
visibility: hidden;
|
||||
margin-right: calc(var(--tree-node-nested-level) * #{$shiftWidth});
|
||||
}
|
||||
|
||||
&.expandable:hover,
|
||||
&.expandable:focus {
|
||||
:global([data-scope-button]) {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-left: 1px solid var(--secondary);
|
||||
height: 100%;
|
||||
transform: translateX(
|
||||
calc(var(--tree-node-nested-level) * #{$shiftWidth} + var(--icon-inline-size) * 1.5 + #{$gap} + 2px)
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,7 +7,7 @@ export interface TreeProvider<T> {
|
||||
*/
|
||||
getEntries(): T[]
|
||||
/**
|
||||
* Whether or not the provided entry has (possibly) children or not.
|
||||
* Whether or not the provided entry is has (possibly) children or not.
|
||||
*/
|
||||
isExpandable(entry: T): boolean
|
||||
/**
|
||||
@ -29,7 +29,6 @@ export interface SingleSelectTreeState {
|
||||
focused: string
|
||||
selected: string
|
||||
expandedNodes: Set<string>
|
||||
disableScope: boolean
|
||||
}
|
||||
|
||||
export type TreeState = SingleSelectTreeState
|
||||
@ -39,7 +38,6 @@ export function createEmptySingleSelectTreeState(): SingleSelectTreeState {
|
||||
focused: '',
|
||||
selected: '',
|
||||
expandedNodes: new Set(),
|
||||
disableScope: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user