Search Results: Support tablet/mobile layout for the new search filters panel (#59537)

* Support tablet/mobile layout for the new search filters panel

* Update bazel build

* Add missing files

* Fix eslint
This commit is contained in:
Vova Kulikov 2024-01-12 16:13:50 -03:00 committed by GitHub
parent b5937ed32f
commit a282dbc33f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 158 additions and 42 deletions

View File

@ -22,7 +22,7 @@ import {
toSearchSyntaxTypeFilter,
} from './components/filter-type-list/FilterTypeList'
import { FiltersDocFooter } from './components/filters-doc-footer/FiltersDocFooter'
import { useFilterQuery } from './hooks'
import { useUrlFilters } from './hooks'
import { SearchFilterType } from './types'
import styles from './NewSearchFilters.module.scss'
@ -34,7 +34,7 @@ interface NewSearchFiltersProps {
}
export const NewSearchFilters: FC<NewSearchFiltersProps> = ({ query, filters, onQueryChange }) => {
const [selectedFilters, setSelectedFilters] = useFilterQuery()
const [selectedFilters, setSelectedFilters] = useUrlFilters()
const type = useMemo(() => {
const tokens = scanSearchQuery(query)

View File

@ -198,12 +198,12 @@ export const languageFilter = (filter: Filter): ReactNode => (
)
export const repoFilter = (filter: Filter): ReactNode => {
const { svgPath, color } = codeHostIcon(filter.label)
const { svgPath } = codeHostIcon(filter.label)
return (
<Tooltip content={filter.label}>
<span ref={null}>
<Icon aria-hidden={true} svgPath={svgPath ?? mdiSourceRepository} color={color} />{' '}
{displayRepoName(filter.label)}
<span>
<Icon aria-hidden={true} svgPath={svgPath ?? mdiSourceRepository} /> {displayRepoName(filter.label)}
</span>
</Tooltip>
)

View File

@ -18,7 +18,7 @@ export function deserializeURLQueryFilters(serialized: string | null): URLQueryF
return parsed.map(([kind, label, value]) => ({ kind, label, value }))
}
export function useFilterQuery(): [URLQueryFilter[], (newFilters: URLQueryFilter[]) => void] {
export function useUrlFilters(): [URLQueryFilter[], (newFilters: URLQueryFilter[]) => void] {
const [filterQuery, setFilterQuery] = useSyncedWithURLState<URLQueryFilter[], string>({
urlKey: 'filters',
serializer: serializeURLQueryFilters,

View File

@ -1391,6 +1391,7 @@ ts_project(
"src/search/results/components/aggregation/index.ts",
"src/search/results/components/aggregation/pings.ts",
"src/search/results/components/aggregation/types.ts",
"src/search/results/components/filters-panel/SearchFiltersPanel.tsx",
"src/search/results/components/new-search-content/NewSearchContent.tsx",
"src/search/results/components/search-content/SearchContent.tsx",
"src/search/results/components/search-results-info-bar/SearchResultsInfoBar.tsx",

View File

@ -42,7 +42,7 @@ interface CachedSearchResultsInput {
* Filter query, different from the search query since new filters
* don't modify the main search query
*/
filterQuery: URLQueryFilter[]
urlFilters: URLQueryFilter[]
/**
* Options to pass on to `streamSeach`.
@ -64,7 +64,7 @@ interface CachedSearchResultsInput {
* (updated as new streaming results come in).
*/
export function useCachedSearchResults(props: CachedSearchResultsInput): AggregateStreamingSearchResults | undefined {
const { query, filterQuery: selectedFilters, options, streamSearch, telemetryService } = props
const { query, urlFilters: selectedFilters, options, streamSearch, telemetryService } = props
const cachedResults = useContext(SearchResultsCacheContext)
const location = useLocation()

View File

@ -3,7 +3,7 @@ import { type FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import type { Observable } from 'rxjs'
import { limitHit, useFilterQuery } from '@sourcegraph/branded'
import { limitHit, useUrlFilters } from '@sourcegraph/branded'
import type { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
@ -76,7 +76,7 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
const liveQuery = useNavbarQueryState(state => state.queryState.query)
const submittedURLQuery = useNavbarQueryState(state => state.searchQueryFromURL)
const queryState = useNavbarQueryState(state => state.queryState)
const [filterQuery] = useFilterQuery()
const [urlFilters] = useUrlFilters()
const setQueryState = useNavbarQueryState(state => state.setQueryState)
const submitQuerySearch = useNavbarQueryState(state => state.submitSearch)
@ -101,7 +101,7 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
)
const results = useCachedSearchResults({
query: submittedURLQuery,
filterQuery,
urlFilters,
options,
streamSearch,
telemetryService,

View File

@ -0,0 +1,15 @@
.modal {
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
width: 18.75rem;
max-width: 80%;
height: 100%;
transform: unset;
border: none;
padding: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View File

@ -0,0 +1,106 @@
import { FC } from 'react'
import create from 'zustand'
import { NewSearchFilters, useUrlFilters } from '@sourcegraph/branded'
import { Filter } from '@sourcegraph/shared/src/search/stream'
import { Badge, Button, Modal, Panel, useWindowSize } from '@sourcegraph/wildcard'
import styles from './SearchFiltersPanel.module.scss'
interface SearchFiltersStore {
isOpen: boolean
setFiltersPanel: (open: boolean) => void
}
export const useSearchFiltersStore = create<SearchFiltersStore>(set => ({
isOpen: false,
setFiltersPanel: (open: boolean) => set({ isOpen: open }),
}))
export interface SearchFiltersPanelProps {
query: string
filters: Filter[] | undefined
className?: string
onQueryChange: (nextQuery: string) => void
}
/**
* Search result page filters sidebar, this components renders
* filters panel in different UI mode, sidebar for desktop and modal-like
* UI for tablets and mobile layout.
*
* NOTE: This is a specific component to search result page, do not reuse it
* as it is, use consumer agnostic NewSearchFilters component instead.
*/
export const SearchFiltersPanel: FC<SearchFiltersPanelProps> = props => {
const { query, filters, className, onQueryChange } = props
const { isOpen, setFiltersPanel } = useSearchFiltersStore()
const uiMode = useSearchFiltersPanelUIMode()
if (uiMode === SearchFiltersPanelUIMode.Sidebar) {
return (
<Panel
defaultSize={250}
minSize={200}
position="left"
storageKey="filter-sidebar"
ariaLabel="Filters sidebar"
className={className}
>
<NewSearchFilters query={query} filters={filters} onQueryChange={onQueryChange} />
</Panel>
)
}
return (
<Modal
isOpen={isOpen}
aria-label="Filters modal"
className={styles.modal}
onDismiss={() => setFiltersPanel(false)}
>
<NewSearchFilters query={query} filters={filters} onQueryChange={onQueryChange} />
</Modal>
)
}
export enum SearchFiltersPanelUIMode {
Sidebar = 'sidebar',
Modal = 'modal',
}
export function useSearchFiltersPanelUIMode(): SearchFiltersPanelUIMode {
const { width } = useWindowSize()
// Hardcoded media query value in order to switch between desktop and mobile
// filter UI versions
const hasTabletLayout = width <= 992
return hasTabletLayout ? SearchFiltersPanelUIMode.Modal : SearchFiltersPanelUIMode.Sidebar
}
export const SearchFiltersTabletButton: FC = props => {
const mode = useSearchFiltersPanelUIMode()
const [urlFilters] = useUrlFilters()
const { setFiltersPanel } = useSearchFiltersStore()
// There is no point to render action filter button when we're in
// sidebar mode since filters are always visible in this mode
// Render it only when we're in tablet/mobile layout
if (mode === SearchFiltersPanelUIMode.Sidebar) {
return null
}
return (
<Button variant="secondary" outline={true} size="sm" onClick={() => setFiltersPanel(true)}>
Filters{' '}
{urlFilters.length > 0 && (
<Badge small={true} variant="primary" className="ml-1">
{urlFilters.length}
</Badge>
)}
</Button>
)
}

View File

@ -14,12 +14,7 @@ import { mdiClose } from '@mdi/js'
import classNames from 'classnames'
import { Observable } from 'rxjs'
import {
NewSearchFilters,
StreamingProgress,
StreamingSearchResultsList,
useSearchResultState,
} from '@sourcegraph/branded'
import { StreamingProgress, StreamingSearchResultsList, useSearchResultState } from '@sourcegraph/branded'
import { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
import { FilePrefetcher } from '@sourcegraph/shared/src/components/PrefetchableFile'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
@ -52,6 +47,7 @@ import { DidYouMean } from '../../../suggestion/DidYouMean'
import { SmartSearch } from '../../../suggestion/SmartSearch'
import { SearchFiltersSidebar } from '../../sidebar/SearchFiltersSidebar'
import { AggregationUIMode, SearchAggregationResult } from '../aggregation'
import { SearchFiltersPanel, SearchFiltersTabletButton } from '../filters-panel/SearchFiltersPanel'
import { SearchResultsInfoBar } from '../search-results-info-bar/SearchResultsInfoBar'
import { SearchAlert } from '../SearchAlert'
import { UnownedResultsAlert } from '../UnownedResultsAlert'
@ -134,17 +130,18 @@ export const NewSearchContent: FC<NewSearchContentProps> = props => {
onLogSearchResultClick,
} = props
const newFiltersEnabled = useExperimentalFeatures(features => features.newSearchResultFiltersPanel)
const submittedURLQueryRef = useRef(submittedURLQuery)
const containerRef = useRef<HTMLDivElement>(null)
const { previewBlob, clearPreview } = useSearchResultState()
const newFiltersEnabled = useExperimentalFeatures(features => features.newSearchResultFiltersPanel)
const [sidebarCollapsed, setSidebarCollapsed] = useLocalStorage('search.sidebar.collapsed', true)
useScrollManager('SearchResultsContainer', containerRef)
// Clean up hook, close the preview panel if search result page
// have been closed/unmount
useEffect(() => clearPreview, [clearPreview])
useEffect(clearPreview, [clearPreview])
// File preview clean up hook, close the preview panel every time when we
// re-search / re-submit the query.
@ -174,20 +171,12 @@ export const NewSearchContent: FC<NewSearchContentProps> = props => {
return (
<div className={classNames(styles.root, { [styles.rootWithNewFilters]: newFiltersEnabled })}>
{newFiltersEnabled && (
<Panel
defaultSize={250}
minSize={200}
position="left"
storageKey="filter-sidebar"
ariaLabel="Filters sidebar"
<SearchFiltersPanel
query={submittedURLQuery}
filters={results?.filters}
onQueryChange={handleFilterPanelQueryChange}
className={styles.newFilters}
>
<NewSearchFilters
query={submittedURLQuery}
filters={results?.filters}
onQueryChange={handleFilterPanelQueryChange}
/>
</Panel>
/>
)}
{!newFiltersEnabled && !sidebarCollapsed && (
@ -228,15 +217,18 @@ export const NewSearchContent: FC<NewSearchContentProps> = props => {
onExpandAllResultsToggle={onExpandAllResultsToggle}
onShowMobileFiltersChanged={setSidebarCollapsed}
stats={
<StreamingProgress
showTrace={trace}
query={`${submittedURLQuery} patterntype:${patternType}`}
progress={results?.progress || { durationMs: 0, matchCount: 0, skipped: [] }}
state={results?.state || 'loading'}
onSearchAgain={onSearchAgain}
isSearchJobsEnabled={isSearchJobsEnabled()}
telemetryService={props.telemetryService}
/>
<>
<StreamingProgress
showTrace={trace}
query={`${submittedURLQuery} patterntype:${patternType}`}
progress={results?.progress || { durationMs: 0, matchCount: 0, skipped: [] }}
state={results?.state || 'loading'}
onSearchAgain={onSearchAgain}
isSearchJobsEnabled={isSearchJobsEnabled()}
telemetryService={props.telemetryService}
/>
<SearchFiltersTabletButton />
</>
}
/>

View File

@ -191,6 +191,8 @@ export const Tooltip: FC<TooltipProps> = props => {
returnTargetFocus={false}
className={styles.tooltipContent}
onOpenChange={handleOpenChange}
overflowToScrollParents={false}
constrainToScrollParents={false}
>
{content}
</PopoverContent>