mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:51:43 +00:00
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:
parent
b5937ed32f
commit
a282dbc33f
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -191,6 +191,8 @@ export const Tooltip: FC<TooltipProps> = props => {
|
||||
returnTargetFocus={false}
|
||||
className={styles.tooltipContent}
|
||||
onOpenChange={handleOpenChange}
|
||||
overflowToScrollParents={false}
|
||||
constrainToScrollParents={false}
|
||||
>
|
||||
{content}
|
||||
</PopoverContent>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user