Search: restore snippets to filters sidebar (#63587)

This adds snippets back to the search sidebar, which got missed when it
was redesigned.

This includes some refactoring of the Svelte version to account for
filter types that do not match 1:1 with the backend types. We initially
tried to tie them tightly with the backend types so the backend is the
source of truth, but I think we want to have the ability to introduce
client-side-only filters, which we already sorta hackily do with the
`type:` filters. And it's even more hacky with the `count:` filter in
the React webapp (which doesn't look like it was ever implemented in the
Svelte version).

In the React app, I made the minimum changes to get this working (no
associated refactoring).
This commit is contained in:
Camden Cheek 2024-07-08 10:39:21 -06:00 committed by GitHub
parent aa0be2e68e
commit 3a76ec8348
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 291 additions and 187 deletions

View File

@ -8,6 +8,7 @@ import { scanSearchQuery, succeedScan } from '@sourcegraph/shared/src/search/que
import type { Filter as QueryFilter } from '@sourcegraph/shared/src/search/query/token'
import { omitFilter } from '@sourcegraph/shared/src/search/query/transformer'
import { TELEMETRY_FILTER_TYPES, type Filter } from '@sourcegraph/shared/src/search/stream'
import { useSettings } from '@sourcegraph/shared/src/settings/settings'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Button, H1, H3, Icon, Tooltip } from '@sourcegraph/wildcard'
@ -19,7 +20,6 @@ import {
repoFilter,
SearchDynamicFilter,
symbolFilter,
utilityFilter,
} from './components/dynamic-filter/SearchDynamicFilter'
import { SearchFilterSkeleton } from './components/filter-skeleton/SearchFilterSkeleton'
import { FilterTypeList } from './components/filter-type-list/FilterTypeList'
@ -92,7 +92,7 @@ export const NewSearchFilters: FC<NewSearchFiltersProps> = ({
)
const handleFilterChange = useCallback(
(filterKind: Filter['kind'], filters: URLQueryFilter[]) => {
(filterKind: FilterKind, filters: URLQueryFilter[]) => {
setSelectedFilters(filters)
telemetryService.log('SearchFiltersSelectFilter', { filterKind }, { filterKind })
telemetryRecorder.recordEvent('search.filters', 'select', {
@ -112,6 +112,17 @@ export const NewSearchFilters: FC<NewSearchFiltersProps> = ({
onQueryChange(`${query} ${filter}`)
}
const settings = useSettings()
const snippetFilters = settings?.['search.scopes']?.map(
(scope): Filter => ({
label: scope.name,
value: scope.value,
count: 0,
exhaustive: true,
kind: 'snippet' as any,
})
)
return (
<div className={styles.scrollWrapper}>
<div className={styles.filterPanelHeader}>
@ -204,11 +215,11 @@ export const NewSearchFilters: FC<NewSearchFiltersProps> = ({
/>
<SearchDynamicFilter
title="Utility"
filterKind={FilterKind.Utility}
filters={filters}
title="Snippets"
filterKind={FilterKind.Snippet}
filters={snippetFilters}
selectedFilters={selectedFilters}
renderItem={utilityFilter}
renderItem={commitDateFilter}
onSelectedFilterChange={handleFilterChange}
onAddFilterToQuery={onAddFilterToQuery}
/>
@ -300,7 +311,7 @@ const SyntheticCountFilter: FC<SyntheticCountFilterProps> = props => {
return []
}, [query])
const handleCountAllFilter = (filterKind: Filter['kind'], countFilters: URLQueryFilter[]): void => {
const handleCountAllFilter = (filterKind: FilterKind, countFilters: URLQueryFilter[]): void => {
telemetryService.log('SearchFiltersSelectFilter', { filterKind }, { filterKind })
telemetryRecorder.recordEvent('search.filters', 'select', {
metadata: { filterKind: TELEMETRY_FILTER_TYPES[filterKind] },
@ -323,7 +334,7 @@ const SyntheticCountFilter: FC<SyntheticCountFilterProps> = props => {
return (
<SearchDynamicFilter
filterKind={FilterKind.Count as any}
filterKind={FilterKind.Count}
filters={STATIC_COUNT_FILTER}
selectedFilters={selectedCountFilter}
renderItem={commitDateFilter}

View File

@ -13,6 +13,7 @@ import { Button, Icon, H2, H4, Input, LanguageIcon, Code, Tooltip } from '@sourc
import { codeHostIcon } from '../../../../components'
import { SyntaxHighlightedSearchQuery } from '../../../../components/SyntaxHighlightedSearchQuery'
import type { URLQueryFilter } from '../../hooks'
import { FilterKind } from '../../types'
import { DynamicFilterBadge } from '../DynamicFilterBadge'
import styles from './SearchDynamicFilter.module.scss'
@ -28,7 +29,7 @@ interface SearchDynamicFilterProps {
* Specifies which type filter we want to render in this particular
* filter section, it could be lang filter, repo filter, or file filters.
*/
filterKind: Filter['kind']
filterKind: FilterKind
/**
* The set of filters that are selected. This is the state that is stored
@ -48,7 +49,7 @@ interface SearchDynamicFilterProps {
* It's called whenever user changes (pick/reset) any filters in the filter panel.
* @param nextQuery
*/
onSelectedFilterChange: (filterKind: Filter['kind'], filters: URLQueryFilter[]) => void
onSelectedFilterChange: (filterKind: FilterKind, filters: URLQueryFilter[]) => void
onAddFilterToQuery: (newFilter: string) => void
}
@ -221,7 +222,7 @@ const DynamicFilterItem: FC<DynamicFilterItemProps> = props => {
)
}
function filterForSearchTerm(input: string, filterKind: Filter['kind']): string | null {
function filterForSearchTerm(input: string, filterKind: FilterKind): string | null {
switch (filterKind) {
case 'repo': {
return `repo:${maybeQuoteString(input)}`

View File

@ -10,6 +10,7 @@ export enum FilterKind {
// Synthetic filters, lives only on the client
Count = 'count',
Type = 'type',
Snippet = 'snippet',
}
export const DYNAMIC_FILTER_KINDS = [

View File

@ -284,7 +284,7 @@ export interface Filter {
kind: 'file' | 'repo' | 'lang' | 'utility' | 'author' | 'commit date' | 'symbol type' | 'type'
}
export const TELEMETRY_FILTER_TYPES: { [key in Filter['kind']]: number } = {
export const TELEMETRY_FILTER_TYPES = {
file: 1,
repo: 2,
lang: 3,
@ -293,6 +293,8 @@ export const TELEMETRY_FILTER_TYPES: { [key in Filter['kind']]: number } = {
'commit date': 6,
'symbol type': 7,
type: 8,
snippet: 9,
count: 10,
}
export type SmartSearchAlertKind = 'smart-search-additional-results' | 'smart-search-pure-results'

View File

@ -15,28 +15,14 @@
import Tooltip from '$lib/Tooltip.svelte'
import { Badge } from '$lib/wildcard'
export let count: number | undefined
export let count: number
export let exhaustive: boolean
</script>
{#if count !== undefined}
<span>
{#if exhaustive}
<Badge variant="secondary">{count}</Badge>
{:else}
<Tooltip placement="right" tooltip="At least {count} {pluralize('result', count)} match this filter.">
<Badge variant="secondary">{roundCount(count)}+</Badge>
</Tooltip>
{/if}
</span>
{#if exhaustive}
<Badge variant="secondary">{count}</Badge>
{:else}
<Tooltip placement="right" tooltip="At least {count} {pluralize('result', count)} match this filter.">
<Badge variant="secondary">{roundCount(count)}+</Badge>
</Tooltip>
{/if}
<style lang="scss">
span {
display: contents;
:global(span) {
background-color: var(--secondary-2);
color: var(--text-body);
}
}
</style>

View File

@ -1,14 +1,14 @@
<script lang="ts">
import type { ComponentProps } from 'svelte'
import { Button } from '$lib/wildcard'
import type { SectionItemData } from './index.ts'
import SectionItem from './SectionItem.svelte'
export let items: SectionItemData[]
export let items: ComponentProps<SectionItem>[]
export let title: string
export let filterPlaceholder: string = ''
export let showAll: boolean = false
export let onFilterSelect: (kind: SectionItem['kind']) => void = () => {}
let filterText = ''
$: processedFilterText = filterText.trim().toLowerCase()
@ -32,7 +32,7 @@
{#each limitedItems as item}
<li>
<slot name="item" {item}>
<SectionItem {item} {onFilterSelect} />
<SectionItem {...item} on:select />
</slot>
</li>
{/each}

View File

@ -1,30 +1,32 @@
<script lang="ts">
import { page } from '$app/stores'
import { createEventDispatcher, type ComponentProps } from 'svelte'
import Icon from '$lib/Icon.svelte'
import CountBadge from './CountBadge.svelte'
import { updateFilterInURL, type SectionItemData } from './index'
export let item: SectionItemData
export let onFilterSelect: (kind: SectionItemData['kind']) => void = () => {}
export let label: string
export let value: string
export let href: URL
export let count: ComponentProps<CountBadge> | undefined = undefined
export let selected: boolean
const dispatch = createEventDispatcher<{ select: { label: string; value: string } }>()
</script>
<a
href={updateFilterInURL($page.url, item, item.selected).toString()}
class:selected={item.selected}
on:click={() => onFilterSelect(item.kind)}
>
<!-- TODO: a11y. This should expose the aria selected state and use the proper roles -->
<a href={href.toString()} class:selected on:click={() => dispatch('select', { label, value })}>
<slot name="icon" />
<span class="label">
<slot name="label" label={item.label} value={item.value}>
{item.label}
<slot name="label" {label} {value}>
{label}
</slot>
</span>
<CountBadge count={item.count} exhaustive={item.exhaustive} />
{#if item.selected}
<span class="close">
<Icon icon={ILucideX} inline aria-hidden />
</span>
{#if count}
<CountBadge {...count} />
{/if}
{#if selected}
<Icon icon={ILucideX} inline aria-hidden />
{/if}
</a>
@ -74,9 +76,5 @@
color: var(--light-text);
}
}
.close {
flex-shrink: 0;
}
}
</style>

View File

@ -7,17 +7,116 @@
const filters = tokens.term.filter((token): token is QueryFilter => token.type === 'filter')
return filters.some(filter => filter.field.value === 'type')
}
const sectionKinds = [
'file',
'repo',
'lang',
'utility',
'author',
'commit date',
'symbol type',
'type',
'snippet',
] as const
type SectionKind = typeof sectionKinds[number]
// A statically-defined filter
type StaticFilter = {
label: string
value: string
}
// A selected filter
type SelectedFilter = StaticFilter
// A filter sourced from the stream API
type StreamFilter = StaticFilter & {
count: number
exhaustive: boolean
}
// Everything needed to render a SectionItem except the href, which
// can be calculated from the current URL and the other props.
type MergedFilter = Omit<ComponentProps<SectionItem>, 'href'>
const typeFilterIcons: Record<string, IconComponent> = {
Code: ILucideBraces,
Repositories: ILucideGitFork,
Paths: ILucideFile,
Symbols: ILucideSquareFunction,
Commits: ILucideGitCommitVertical,
Diffs: ILucideDiff,
}
const staticTypeFilters: StaticFilter[] = [
{ label: 'Code', value: 'type:file' },
{ label: 'Repositories', value: 'type:repo' },
{ label: 'Paths', value: 'type:path' },
{ label: 'Symbols', value: 'type:symbol' },
{ label: 'Commits', value: 'type:commit' },
{ label: 'Diffs', value: 'type:diff' },
]
// mergeFilterSources merges the filters of a shared kind from different sources.
function mergeFilterSources(
staticFilters: readonly StaticFilter[],
selectedFilters: readonly SelectedFilter[],
streamFilters: readonly StreamFilter[]
): MergedFilter[] {
// Start with static filters, which are well-ordered
const merged: MergedFilter[] = staticFilters.map(filter => ({
...filter,
selected: false,
count: undefined,
}))
// Then merge in the selected filters
for (const selectedFilter of selectedFilters) {
const found = merged.find(filter => filter.label === selectedFilter.label)
if (found !== undefined) {
// If we found a matching static filter, update it to be selected
found.selected = true
} else {
// Othersie, add it to the end of the list
merged.push({
...selectedFilter,
selected: true,
count: undefined,
})
}
}
// Finally, merge in the filters from the search stream
for (const streamFilter of streamFilters) {
const found = merged.find(filter => filter.label === streamFilter.label)
if (found !== undefined) {
// If we found a matching filter, update its count
found.count = { count: streamFilter.count, exhaustive: streamFilter.exhaustive }
} else {
// Otherwise, add it to the end of the list
merged.push({
...streamFilter,
count: { count: streamFilter.count, exhaustive: streamFilter.exhaustive },
selected: false,
})
}
}
return merged
}
</script>
<script lang="ts">
import { onMount } from 'svelte'
import { onMount, type ComponentProps } from 'svelte'
import type { Filter as QueryFilter } from '@sourcegraph/shared/src/search/query/token'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { getGraphQLClient } from '$lib/graphql'
import Icon from '$lib/Icon.svelte'
import Icon, { type IconComponent } from '$lib/Icon.svelte'
import KeyboardShortcut from '$lib/KeyboardShortcut.svelte'
import LanguageIcon from '$lib/LanguageIcon.svelte'
import Popover from '$lib/Popover.svelte'
@ -25,21 +124,14 @@
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
import SymbolKindIcon from '$lib/search/SymbolKindIcon.svelte'
import { TELEMETRY_FILTER_TYPES, displayRepoName, scanSearchQuery, type Filter } from '$lib/shared'
import { settings } from '$lib/stores'
import { TELEMETRY_RECORDER } from '$lib/telemetry'
import { delay } from '$lib/utils'
import { Alert } from '$lib/wildcard'
import Button from '$lib/wildcard/Button.svelte'
import HelpFooter from './HelpFooter.svelte'
import {
type URLQueryFilter,
type SectionItemData,
staticTypeFilters,
typeFilterIcons,
groupFilters,
moveFiltersToQuery,
resetFilters,
} from './index'
import { type URLQueryFilter, moveFiltersToQuery, resetFilters, updateFilterInURL } from './index'
import LoadingSkeleton from './LoadingSkeleton.svelte'
import Section from './Section.svelte'
import SectionItem from './SectionItem.svelte'
@ -49,18 +141,51 @@
export let selectedFilters: URLQueryFilter[]
export let state: 'complete' | 'error' | 'loading'
$: groupedFilters = groupFilters(streamFilters, selectedFilters)
$: typeFilters = staticTypeFilters.map((staticTypeFilter): SectionItemData => {
const selectedOrStreamFilter = groupedFilters.type.find(
typeFilter => typeFilter.label === staticTypeFilter.label
)
return {
...staticTypeFilter,
count: selectedOrStreamFilter?.count,
exhaustive: selectedOrStreamFilter?.exhaustive || false,
selected: selectedOrStreamFilter?.selected || false,
}
})
// We have three potential sources for filters:
// - Static filters (types, snippets)
// - Selected filters (stored in the URL)
// - Stream filters (generated from search results)
//
// First, we group each source of filters by kind which is only relevant
// for grouping and not for rendering individual items.
let groupedStaticFilters: Partial<Record<SectionKind, StaticFilter[]>>
$: groupedStaticFilters = {
type: staticTypeFilters,
snippet:
$settings?.['search.scopes']?.map(
(scope): StaticFilter => ({
label: scope.name,
value: scope.value,
})
) ?? [],
}
let groupedSelectedFilters: Partial<Record<SectionKind, SelectedFilter[]>>
$: groupedSelectedFilters = Object.groupBy(selectedFilters, ({ kind }) => kind)
let groupedStreamFilters: Partial<Record<SectionKind, StreamFilter[]>>
$: groupedStreamFilters = Object.groupBy(streamFilters, ({ kind }) => kind)
// Then we merge the groups together. Different sources provide different
// information (see mergeFilterSources for details). After we've merged, add
// an href to the results. At that point, we have everything we need to render
// a SectionItem.
let sectionItems: Record<SectionKind, ComponentProps<SectionItem>[]>
$: sectionItems = Object.fromEntries(
sectionKinds.map(sectionKind => [
sectionKind satisfies SectionKind,
mergeFilterSources(
groupedStaticFilters[sectionKind] ?? [],
groupedSelectedFilters[sectionKind] ?? [],
groupedStreamFilters[sectionKind] ?? []
).map(mergedFilter => ({
...mergedFilter,
href: updateFilterInURL($page.url, { ...mergedFilter, kind: sectionKind }, mergedFilter.selected),
})) satisfies ComponentProps<SectionItem>[],
])
) as Record<SectionKind, ComponentProps<SectionItem>[]> // Safe assertion because of internal `satisfies`
$: resetURL = resetFilters($page.url).toString()
$: enableReset = selectedFilters.length > 0
@ -71,12 +196,13 @@
}
}
function handleFilterSelect(kind: SectionItemData['kind']): void {
function handleFilterSelect(kind: keyof typeof TELEMETRY_FILTER_TYPES): void {
TELEMETRY_RECORDER.recordEvent('search.filters', 'select', {
metadata: { filterKind: TELEMETRY_FILTER_TYPES[kind] },
})
}
// TODO: use registerHotkey
onMount(() => {
window.addEventListener('keydown', handleResetKeydown)
return () => window.removeEventListener('keydown', handleResetKeydown)
@ -96,25 +222,20 @@
</div>
{#if !queryHasTypeFilter(searchQuery)}
<Section items={typeFilters} title="By type" showAll onFilterSelect={handleFilterSelect}>
<SectionItem slot="item" let:item {item}>
<Section items={sectionItems.type} title="By type" showAll>
<SectionItem slot="item" let:item {...item} on:select={() => handleFilterSelect('type')}>
<Icon slot="icon" icon={typeFilterIcons[item.label]} inline />
</SectionItem>
</Section>
{/if}
<Section
items={groupedFilters.repo}
title="By repository"
filterPlaceholder="Filter repositories"
onFilterSelect={handleFilterSelect}
>
<Section items={sectionItems.repo} title="By repository" filterPlaceholder="Filter repositories">
<svelte:fragment slot="item" let:item>
<Popover showOnHover let:registerTrigger placement="right-start">
<div use:registerTrigger>
<SectionItem {item}>
<SectionItem {...item} on:select={() => handleFilterSelect('repo')}>
<CodeHostIcon slot="icon" disableTooltip repository={item.label} />
<span slot="label" let:label>{displayRepoName(label)}</span>
<span slot="label">{displayRepoName(item.label)}</span>
</SectionItem>
</div>
<svelte:fragment slot="content">
@ -127,42 +248,41 @@
</Popover>
</svelte:fragment>
</Section>
<Section
items={groupedFilters.lang}
title="By language"
filterPlaceholder="Filter languages"
onFilterSelect={handleFilterSelect}
>
<SectionItem slot="item" let:item {item}>
<Section items={sectionItems.lang} title="By language" filterPlaceholder="Filter languages">
<SectionItem slot="item" let:item {...item} on:select={() => handleFilterSelect('lang')}>
<LanguageIcon slot="icon" language={item.label} inline />
</SectionItem>
</Section>
<Section
items={groupedFilters['symbol type']}
title="By symbol type"
filterPlaceholder="Filter symbol types"
onFilterSelect={handleFilterSelect}
>
<SectionItem slot="item" let:item {item}>
<Section items={sectionItems['symbol type']} title="By symbol type" filterPlaceholder="Filter symbol types">
<SectionItem slot="item" let:item {...item} on:select={() => handleFilterSelect('symbol type')}>
<SymbolKindIcon slot="icon" symbolKind={item.label.toUpperCase()} />
</SectionItem>
</Section>
<Section
items={groupedFilters.author}
items={sectionItems.author}
title="By author"
filterPlaceholder="Filter authors"
onFilterSelect={handleFilterSelect}
on:select={() => handleFilterSelect('author')}
/>
<Section items={groupedFilters['commit date']} title="By commit date" onFilterSelect={handleFilterSelect}>
<SectionItem slot="item" let:item {item}>
<span class="commit-date-label" slot="label">
<Section items={sectionItems['commit date']} title="By commit date">
<SectionItem slot="item" let:item {...item} on:select={() => handleFilterSelect('commit date')}>
<svelte:fragment slot="label">
{item.label}
<small><pre>{item.value}</pre></small>
</span>
</svelte:fragment>
</SectionItem>
</Section>
<Section items={sectionItems.file} title="By file" showAll on:select={() => handleFilterSelect('file')} />
<Section items={sectionItems.utility} title="Utility" showAll on:select={() => handleFilterSelect('utility')} />
<Section items={sectionItems.snippet} title="Snippets">
<SectionItem slot="item" let:item {...item} on:select={() => handleFilterSelect('snippet')}>
<svelte:fragment slot="label">
{item.label}
<small><pre>{item.value}</pre></small>
</svelte:fragment>
</SectionItem>
</Section>
<Section items={groupedFilters.file} title="By file" showAll onFilterSelect={handleFilterSelect} />
<Section items={groupedFilters.utility} title="Utility" showAll onFilterSelect={handleFilterSelect} />
{#if state === 'loading'}
<LoadingSkeleton />
@ -175,10 +295,8 @@
{#if selectedFilters.length > 0}
<div class="move-button">
<Button variant="secondary" display="block" outline on:click={() => goto(moveFiltersToQuery($page.url))}>
<svelte:fragment>
Move filters to query&nbsp;
<Icon icon={ILucideCornerRightDown} aria-hidden inline />
</svelte:fragment>
Move filters to query&nbsp;
<Icon icon={ILucideCornerRightDown} aria-hidden inline />
</Button>
</div>
{/if}

View File

@ -1,22 +1,19 @@
import type { Filter } from '@sourcegraph/shared/src/search/stream'
import type { IconComponent } from '$lib/Icon.svelte'
import type { Filter } from '$lib/shared'
import { parseExtendedSearchURL } from '..'
import { SearchCachePolicy, setCachePolicyInURL } from '../state'
export type SectionItemData = Omit<Filter, 'count'> & {
count?: Filter['count']
selected: boolean
}
/**
* URLQueryFilter is the subset of a filter that is stored in the URL query
* when a dynamic filter is selected. This is the minimum amount of state
* necessary to render the selected filter before the backend streams back
* any filters.
*/
export type URLQueryFilter = Pick<Filter, 'kind' | 'label' | 'value'>
export type URLQueryFilter = {
kind: string
label: string
value: string
}
const DYNAMIC_FILTER_URL_QUERY_KEY = 'df'
@ -72,61 +69,3 @@ export function resetFilters(url: URL): URL {
export function filtersFromParams(params: URLSearchParams): URLQueryFilter[] {
return params.getAll(DYNAMIC_FILTER_URL_QUERY_KEY).map(deserializeURLFilter)
}
export const staticTypeFilters: URLQueryFilter[] = [
{ kind: 'type', label: 'Code', value: 'type:file' },
{ kind: 'type', label: 'Repositories', value: 'type:repo' },
{ kind: 'type', label: 'Paths', value: 'type:path' },
{ kind: 'type', label: 'Symbols', value: 'type:symbol' },
{ kind: 'type', label: 'Commits', value: 'type:commit' },
{ kind: 'type', label: 'Diffs', value: 'type:diff' },
]
export const typeFilterIcons: Record<string, IconComponent> = {
Code: ILucideBraces,
Repositories: ILucideGitFork,
Paths: ILucideFile,
Symbols: ILucideSquareFunction,
Commits: ILucideGitCommitVertical,
Diffs: ILucideDiff,
}
export type FilterGroups = Record<Filter['kind'], SectionItemData[]>
export function groupFilters(streamFilters: Filter[], selectedFilters: URLQueryFilter[]): FilterGroups {
const groupedFilters: FilterGroups = {
type: [],
repo: [],
lang: [],
utility: [],
author: [],
file: [],
'commit date': [],
'symbol type': [],
}
for (const selectedFilter of selectedFilters) {
const streamFilter = streamFilters.find(
streamFilter => streamFilter.kind === selectedFilter.kind && streamFilter.value === selectedFilter.value
)
groupedFilters[selectedFilter.kind].push({
value: selectedFilter.value,
label: selectedFilter.label,
kind: selectedFilter.kind,
selected: true,
// Use count and exhaustiveness from the stream filter if it exists
count: streamFilter?.count,
exhaustive: streamFilter?.exhaustive || false,
})
}
for (const filter of streamFilters) {
if (groupedFilters[filter.kind].some(existingFilter => existingFilter.value === filter.value)) {
// Skip any filters that were already added by the seleced loop above
continue
}
groupedFilters[filter.kind].push({
...filter,
selected: false,
})
}
return groupedFilters
}

View File

@ -52,9 +52,9 @@
}
.secondary {
--badge-base: var(--secondary);
--badge-light: var(--secondary-2);
--badge-dark: var(--secondary-3);
--badge-base: var(--secondary-2);
--badge-light: var(--secondary-3);
--badge-dark: var(--secondary-4);
--badge-text: var(--body-color);
}

View File

@ -260,3 +260,51 @@ test.describe('search results', async () => {
await expect(alert).toBeVisible()
})
})
test.describe('search filters', async () => {
test('type filters are always visible', async ({ page, sg }) => {
const stream = await sg.mockSearchStream()
await page.goto('/search?q=test')
await page.getByRole('heading', { name: 'Filter results' }).waitFor()
await stream.publish(
{
type: 'matches',
data: [chunkMatch],
},
createProgressEvent(),
createDoneEvent()
)
await stream.close()
for (const typeFilter of ['Code', 'Repositories', 'Paths', 'Symbols', 'Commits', 'Diffs']) {
await expect(page.getByRole('link', { name: typeFilter })).toBeVisible()
}
})
test('snippets are shown', async ({ page, sg }) => {
sg.mockOperations({
Init: () => ({
currentUser: null,
viewerSettings: {
final: '{"search.scopes":[{"name":"Test snippet", "value": "repo:testsnippet"}]}',
},
}),
})
const stream = await sg.mockSearchStream()
await page.goto('/search?q=test')
await page.getByRole('heading', { name: 'Filter results' }).waitFor()
await stream.publish(
{
type: 'matches',
data: [chunkMatch],
},
createProgressEvent(),
createDoneEvent()
)
await stream.close()
await page.getByRole('link', { name: 'Test snippet' }).click()
await page.waitForURL(/Test\+snippet/)
})
})