mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 18:51:59 +00:00
search contexts: backend for setting as default and starring (#44624)
* db schema and graphql schema updates * default contexts backend * query for stars * backend for settings stars and defaults * sg generate * fixes? * add foreign keys * idempotent constraints * again * search contexts: remove autodefined contexts from frontend, add to normal query (#44875) * search contexts: remove autodefined contexts from frontend, add to normal query * generate * remove real global context, use union with dummy row instead * fix some tests * fix CountSearchContexts * add test for CountSearchContexts * search contexts: order contexts list by default first, then starred, then others (#44876) * search contexts: order contexts list by default first, then starred, then others * generate mocks * Add test for new order * explicit select and remove unused scan var * simplify getting authenticated user * fix go lint * unit test for default contexts * tests for starred contexts * change graphql api to take user as param, add tests for this * update db schema to make contraints prettier * clean up db keys * search contexts: update header in list page (#44966) * search contexts: update header * add back tab * fix spacing * fix action button width * search contexts: table view in management page (#45001) * search contexts: update header * search contexts: table view in management page * add menu * fix build failure * add back tab * fix spacing * fix action button width * address minor issues from figma * search contexts: card view for mobile (#45040) * search contexts: card view for mobile * fix build failure * give up on sr-only mixin
This commit is contained in:
parent
d04f2f605a
commit
ff6f03a5f7
@ -3,13 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Observable, of, Subscription } from 'rxjs'
|
||||
|
||||
import { requestGraphQLCommon } from '@sourcegraph/http-client'
|
||||
import {
|
||||
fetchAutoDefinedSearchContexts,
|
||||
fetchSearchContexts,
|
||||
getUserSearchContextNamespaces,
|
||||
QueryState,
|
||||
SearchPatternType,
|
||||
} from '@sourcegraph/search'
|
||||
import { fetchSearchContexts, getUserSearchContextNamespaces, QueryState, SearchPatternType } from '@sourcegraph/search'
|
||||
import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
|
||||
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
import {
|
||||
@ -269,7 +263,6 @@ export const App: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
setSelectedSearchContextSpec={contextSpec => onSubmit({ contextSpec })}
|
||||
selectedSearchContextSpec={lastSearch.selectedSearchContextSpec}
|
||||
fetchSearchContexts={fetchSearchContexts}
|
||||
fetchAutoDefinedSearchContexts={fetchAutoDefinedSearchContexts}
|
||||
getUserSearchContextNamespaces={getUserSearchContextNamespaces}
|
||||
fetchStreamSuggestions={fetchStreamSuggestionsWithStaticUrl}
|
||||
settingsCascade={settingsCascade}
|
||||
|
||||
@ -65,7 +65,6 @@ export const JetBrainsSearchBoxStory: Story = () => {
|
||||
fetchSearchContexts={() => {
|
||||
throw new Error('fetchSearchContexts')
|
||||
}}
|
||||
fetchAutoDefinedSearchContexts={() => NEVER}
|
||||
getUserSearchContextNamespaces={() => []}
|
||||
fetchStreamSuggestions={() => NEVER}
|
||||
settingsCascade={EMPTY_SETTINGS_CASCADE}
|
||||
|
||||
@ -103,7 +103,6 @@ export const JetBrainsSearchBox: React.FunctionComponent<React.PropsWithChildren
|
||||
setSelectedSearchContextSpec={props.setSelectedSearchContextSpec}
|
||||
selectedSearchContextSpec={props.selectedSearchContextSpec}
|
||||
fetchSearchContexts={props.fetchSearchContexts}
|
||||
fetchAutoDefinedSearchContexts={props.fetchAutoDefinedSearchContexts}
|
||||
getUserSearchContextNamespaces={props.getUserSearchContextNamespaces}
|
||||
telemetryService={props.telemetryService}
|
||||
platformContext={props.platformContext}
|
||||
|
||||
@ -4,7 +4,6 @@ import { BrandedStory } from '@sourcegraph/branded/src/components/BrandedStory'
|
||||
import { SearchMode, SearchPatternType } from '@sourcegraph/search'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -47,7 +46,6 @@ const defaultProps: SearchBoxProps = {
|
||||
defaultSearchContextSpec: 'global',
|
||||
onChange: () => {},
|
||||
onSubmit: () => {},
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
authenticatedUser: null,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
|
||||
@ -7,7 +7,6 @@ import { assertAriaDisabled, assertAriaEnabled } from '@sourcegraph/shared/dev/a
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { MockIntersectionObserver } from '@sourcegraph/shared/src/testing/MockIntersectionObserver'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -20,7 +19,6 @@ describe('SearchContextDropdown', () => {
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
query: '',
|
||||
showSearchContextManagement: false,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(1),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
defaultSearchContextSpec: '',
|
||||
|
||||
@ -50,7 +50,6 @@ export const SearchContextDropdown: FC<SearchContextDropdownProps> = props => {
|
||||
showSearchContextManagement,
|
||||
selectedSearchContextSpec,
|
||||
setSelectedSearchContextSpec,
|
||||
fetchAutoDefinedSearchContexts,
|
||||
fetchSearchContexts,
|
||||
submitSearch,
|
||||
className,
|
||||
@ -168,7 +167,6 @@ export const SearchContextDropdown: FC<SearchContextDropdownProps> = props => {
|
||||
{...props}
|
||||
showSearchContextManagement={showSearchContextManagement}
|
||||
selectSearchContextSpec={selectSearchContextSpec}
|
||||
fetchAutoDefinedSearchContexts={fetchAutoDefinedSearchContexts}
|
||||
fetchSearchContexts={fetchSearchContexts}
|
||||
className={menuClassName}
|
||||
onMenuClose={handleMenuClose}
|
||||
|
||||
@ -5,7 +5,6 @@ import { BrandedStory } from '@sourcegraph/branded/src/components/BrandedStory'
|
||||
import { ListSearchContextsResult } from '@sourcegraph/search'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -37,7 +36,6 @@ const defaultProps: SearchContextMenuProps = {
|
||||
authenticatedUser: null,
|
||||
isSourcegraphDotCom: false,
|
||||
showSearchContextManagement: false,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(2),
|
||||
fetchSearchContexts: ({
|
||||
first,
|
||||
query,
|
||||
@ -64,6 +62,8 @@ const defaultProps: SearchContextMenuProps = {
|
||||
description: 'Only code in version 1.5',
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: true,
|
||||
viewerHasStarred: false,
|
||||
query: '',
|
||||
repositories: [],
|
||||
},
|
||||
@ -85,7 +85,6 @@ const defaultProps: SearchContextMenuProps = {
|
||||
}
|
||||
|
||||
const emptySearchContexts = {
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { Observable, of, throwError } from 'rxjs'
|
||||
import { of } from 'rxjs'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import { ListSearchContextsResult, SearchContextMinimalFields } from '@sourcegraph/search'
|
||||
@ -12,44 +12,24 @@ import { NOOP_PLATFORM_CONTEXT } from '@sourcegraph/shared/src/testing/searchTes
|
||||
|
||||
import { SearchContextMenu, SearchContextMenuProps } from './SearchContextMenu'
|
||||
|
||||
const mockFetchAutoDefinedSearchContexts = () =>
|
||||
of([
|
||||
const mockFetchSearchContexts = ({ query }: { first: number; query?: string; after?: string }) => {
|
||||
const nodes = [
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '1',
|
||||
id: '0',
|
||||
spec: 'global',
|
||||
name: 'global',
|
||||
namespace: null,
|
||||
autoDefined: true,
|
||||
description: 'All repositories on Sourcegraph',
|
||||
query: '',
|
||||
repositories: [],
|
||||
public: true,
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
viewerCanManage: false,
|
||||
},
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '2',
|
||||
spec: '@username',
|
||||
name: 'username',
|
||||
namespace: {
|
||||
__typename: 'User',
|
||||
id: 'u1',
|
||||
namespaceName: 'username',
|
||||
},
|
||||
autoDefined: true,
|
||||
description: 'Your repositories on Sourcegraph',
|
||||
description: 'All code on Sourcegraph',
|
||||
query: '',
|
||||
repositories: [],
|
||||
public: true,
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
viewerCanManage: false,
|
||||
viewerHasAsDefault: true,
|
||||
viewerHasStarred: false,
|
||||
},
|
||||
] as SearchContextMinimalFields[])
|
||||
|
||||
const mockFetchSearchContexts = ({ query }: { first: number; query?: string; after?: string }) => {
|
||||
const nodes = [
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '3',
|
||||
@ -67,6 +47,8 @@ const mockFetchSearchContexts = ({ query }: { first: number; query?: string; aft
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
},
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
@ -85,6 +67,8 @@ const mockFetchSearchContexts = ({ query }: { first: number; query?: string; aft
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
},
|
||||
].filter(
|
||||
context => !query || context.spec.toLowerCase().includes(query.toLowerCase())
|
||||
@ -108,7 +92,6 @@ describe('SearchContextMenu', () => {
|
||||
defaultSearchContextSpec: 'global',
|
||||
selectedSearchContextSpec: 'global',
|
||||
selectSearchContextSpec: () => {},
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts,
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
onMenuClose: () => {},
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
@ -140,11 +123,11 @@ describe('SearchContextMenu', () => {
|
||||
clock.tick(50)
|
||||
})
|
||||
|
||||
const item = screen.getAllByTestId('search-context-menu-item')[1]
|
||||
const item = screen.getAllByTestId('search-context-menu-item')[0]
|
||||
userEvent.click(item)
|
||||
|
||||
sinon.assert.calledOnce(selectSearchContextSpec)
|
||||
sinon.assert.calledWithExactly(selectSearchContextSpec, '@username')
|
||||
sinon.assert.calledWithExactly(selectSearchContextSpec, 'global')
|
||||
})
|
||||
|
||||
it('should filter list by spec when searching', () => {
|
||||
@ -159,9 +142,8 @@ describe('SearchContextMenu', () => {
|
||||
})
|
||||
|
||||
const items = screen.getAllByTestId('search-context-menu-item')
|
||||
expect(items.length).toBe(2)
|
||||
expect(items[0]).toHaveTextContent('@username, Your repositories on Sourcegraph')
|
||||
expect(items[1]).toHaveTextContent('@username/test-version-1.5, Only code in version 1.5')
|
||||
expect(items.length).toBe(1)
|
||||
expect(items[0]).toHaveTextContent('@username/test-version-1.5, Only code in version 1.5')
|
||||
|
||||
expect(items).toMatchSnapshot()
|
||||
})
|
||||
@ -209,22 +191,4 @@ describe('SearchContextMenu', () => {
|
||||
const items = screen.getAllByTestId('search-context-menu-item')
|
||||
expect(items[items.length - 1]).toHaveTextContent('Error occurred while loading search contexts')
|
||||
})
|
||||
|
||||
it('should default to empty array if fetching auto-defined contexts fails', () => {
|
||||
const errorFetchAutoDefinedSearchContexts: () => Observable<SearchContextMinimalFields[]> = () =>
|
||||
throwError(new Error('unknown error'))
|
||||
|
||||
render(
|
||||
<SearchContextMenu {...defaultProps} fetchAutoDefinedSearchContexts={errorFetchAutoDefinedSearchContexts} />
|
||||
)
|
||||
|
||||
act(() => {
|
||||
// Wait for debounce
|
||||
clock.tick(50)
|
||||
})
|
||||
|
||||
const items = screen.getAllByTestId('search-context-menu-item')
|
||||
// With no auto-defined contexts, the first context should be a user-defined context
|
||||
expect(items[0]).toHaveTextContent('@username/test-version-1.5, Only code in version 1.5')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useEffect, FormEvent, useMemo, useState, FC } from 'react'
|
||||
import { useCallback, useRef, useEffect, FormEvent, useState, FC } from 'react'
|
||||
|
||||
import { mdiClose, mdiArrowRight } from '@mdi/js'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
@ -14,7 +14,6 @@ import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryServi
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
useObservable,
|
||||
Icon,
|
||||
ButtonLink,
|
||||
Link,
|
||||
@ -62,7 +61,6 @@ export const SearchContextMenu: FC<SearchContextMenuProps> = props => {
|
||||
defaultSearchContextSpec,
|
||||
selectSearchContextSpec,
|
||||
getUserSearchContextNamespaces,
|
||||
fetchAutoDefinedSearchContexts,
|
||||
fetchSearchContexts,
|
||||
onMenuClose,
|
||||
showSearchContextManagement,
|
||||
@ -161,16 +159,6 @@ export const SearchContextMenu: FC<SearchContextMenuProps> = props => {
|
||||
platformContext,
|
||||
])
|
||||
|
||||
const autoDefinedSearchContexts = useObservable(
|
||||
useMemo(
|
||||
() =>
|
||||
fetchAutoDefinedSearchContexts({ platformContext, useMinimalFields: true }).pipe(
|
||||
catchError(error => [asError(error)])
|
||||
),
|
||||
[fetchAutoDefinedSearchContexts, platformContext]
|
||||
)
|
||||
)
|
||||
|
||||
const reset = useCallback(() => {
|
||||
selectSearchContextSpec(defaultSearchContextSpec)
|
||||
onMenuClose()
|
||||
@ -185,22 +173,6 @@ export const SearchContextMenu: FC<SearchContextMenuProps> = props => {
|
||||
[onMenuClose, selectSearchContextSpec, telemetryService]
|
||||
)
|
||||
|
||||
const filteredAutoDefinedSearchContexts = useMemo(
|
||||
() =>
|
||||
autoDefinedSearchContexts && !isErrorLike(autoDefinedSearchContexts)
|
||||
? autoDefinedSearchContexts.filter(context =>
|
||||
context.spec.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
)
|
||||
: [],
|
||||
[autoDefinedSearchContexts, searchFilter]
|
||||
)
|
||||
|
||||
// Merge auto-defined contexts and user-defined contexts
|
||||
const filteredList = useMemo(() => filteredAutoDefinedSearchContexts.concat(searchContexts), [
|
||||
filteredAutoDefinedSearchContexts,
|
||||
searchContexts,
|
||||
])
|
||||
|
||||
return (
|
||||
<Combobox openOnFocus={true} className={classNames(styles.container, className)} onSelect={handleContextSelect}>
|
||||
<div className={styles.title}>
|
||||
@ -225,7 +197,7 @@ export const SearchContextMenu: FC<SearchContextMenuProps> = props => {
|
||||
</div>
|
||||
<ComboboxList ref={infiniteScrollList} data-testid="search-context-menu-list" className={styles.list}>
|
||||
{loadingState !== 'LOADING' &&
|
||||
filteredList.map(context => (
|
||||
searchContexts.map(context => (
|
||||
<SearchContextMenuItem
|
||||
key={context.id}
|
||||
spec={context.spec}
|
||||
@ -245,7 +217,7 @@ export const SearchContextMenu: FC<SearchContextMenuProps> = props => {
|
||||
<small>Error occurred while loading search contexts</small>
|
||||
</div>
|
||||
)}
|
||||
{loadingState === 'DONE' && filteredList.length === 0 && (
|
||||
{loadingState === 'DONE' && searchContexts.length === 0 && (
|
||||
<div data-testid="search-context-menu-item" className={styles.item}>
|
||||
<small>No contexts found</small>
|
||||
</div>
|
||||
|
||||
@ -2,51 +2,6 @@
|
||||
|
||||
exports[`SearchContextMenu should filter list by spec when searching 1`] = `
|
||||
Array [
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="item"
|
||||
data-reach-combobox-option=""
|
||||
data-search-context-spec="@username"
|
||||
data-testid="search-context-menu-item"
|
||||
id="21317910"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
>
|
||||
<small
|
||||
class="itemName"
|
||||
data-testid="search-context-menu-item-name"
|
||||
>
|
||||
<span
|
||||
data-reach-combobox-option-text=""
|
||||
data-suggested-value="true"
|
||||
>
|
||||
@u
|
||||
</span>
|
||||
<span
|
||||
data-reach-combobox-option-text=""
|
||||
data-user-value="true"
|
||||
>
|
||||
ser
|
||||
</span>
|
||||
<span
|
||||
data-reach-combobox-option-text=""
|
||||
data-suggested-value="true"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</small>
|
||||
<span
|
||||
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; word-wrap: normal;"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
|
||||
<small
|
||||
class="itemDescription"
|
||||
>
|
||||
Your repositories on Sourcegraph
|
||||
</small>
|
||||
</li>,
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="item"
|
||||
|
||||
@ -11,8 +11,6 @@ import {
|
||||
EventLogsDataVariables,
|
||||
ListSearchContextsResult,
|
||||
ListSearchContextsVariables,
|
||||
AutoDefinedSearchContextsResult,
|
||||
AutoDefinedSearchContextsVariables,
|
||||
IsSearchContextAvailableResult,
|
||||
IsSearchContextAvailableVariables,
|
||||
Scalars,
|
||||
@ -49,6 +47,8 @@ const searchContextFragment = gql`
|
||||
autoDefined
|
||||
updatedAt
|
||||
viewerCanManage
|
||||
viewerHasStarred
|
||||
viewerHasAsDefault
|
||||
query
|
||||
repositories {
|
||||
...SearchContextRepositoryRevisionsFields
|
||||
@ -75,6 +75,8 @@ const searchContextWithSkippableFieldsFragment = gql`
|
||||
autoDefined
|
||||
updatedAt
|
||||
viewerCanManage
|
||||
viewerHasStarred
|
||||
viewerHasAsDefault
|
||||
namespace @skip(if: $useMinimalFields) {
|
||||
__typename
|
||||
id
|
||||
@ -90,34 +92,6 @@ const searchContextWithSkippableFieldsFragment = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export function fetchAutoDefinedSearchContexts({
|
||||
platformContext,
|
||||
useMinimalFields,
|
||||
}: {
|
||||
platformContext: Pick<PlatformContext, 'requestGraphQL'>
|
||||
useMinimalFields?: boolean
|
||||
}): Observable<AutoDefinedSearchContextsResult['autoDefinedSearchContexts']> {
|
||||
return platformContext
|
||||
.requestGraphQL<AutoDefinedSearchContextsResult, AutoDefinedSearchContextsVariables>({
|
||||
request: gql`
|
||||
query AutoDefinedSearchContexts($useMinimalFields: Boolean!) {
|
||||
autoDefinedSearchContexts {
|
||||
...SearchContextMinimalFields
|
||||
}
|
||||
}
|
||||
${searchContextWithSkippableFieldsFragment}
|
||||
`,
|
||||
variables: {
|
||||
useMinimalFields: useMinimalFields ?? false,
|
||||
},
|
||||
mightContainPrivateInfo: false,
|
||||
})
|
||||
.pipe(
|
||||
map(dataOrThrowErrors),
|
||||
map(({ autoDefinedSearchContexts }) => autoDefinedSearchContexts)
|
||||
)
|
||||
}
|
||||
|
||||
export function getUserSearchContextNamespaces(
|
||||
authenticatedUser: Pick<AuthenticatedUser, 'id' | 'organizations'> | null
|
||||
): Maybe<Scalars['ID']>[] {
|
||||
|
||||
@ -4,7 +4,6 @@ import { memoizeObservable } from '@sourcegraph/common'
|
||||
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
|
||||
import {
|
||||
fetchAutoDefinedSearchContexts,
|
||||
fetchSearchContexts,
|
||||
getUserSearchContextNamespaces,
|
||||
fetchSearchContext,
|
||||
@ -48,7 +47,6 @@ export interface SearchContextProps {
|
||||
selectedSearchContextSpec?: string
|
||||
setSelectedSearchContextSpec: (spec: string) => void
|
||||
getUserSearchContextNamespaces: typeof getUserSearchContextNamespaces
|
||||
fetchAutoDefinedSearchContexts: typeof fetchAutoDefinedSearchContexts
|
||||
fetchSearchContexts: typeof fetchSearchContexts
|
||||
isSearchContextSpecAvailable: typeof isSearchContextSpecAvailable
|
||||
fetchSearchContext: typeof fetchSearchContext
|
||||
@ -64,7 +62,6 @@ export type SearchContextInputProps = Pick<
|
||||
| 'defaultSearchContextSpec'
|
||||
| 'selectedSearchContextSpec'
|
||||
| 'setSelectedSearchContextSpec'
|
||||
| 'fetchAutoDefinedSearchContexts'
|
||||
| 'fetchSearchContexts'
|
||||
| 'getUserSearchContextNamespaces'
|
||||
>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { subDays } from 'date-fns'
|
||||
import { range } from 'lodash'
|
||||
import { Observable, of } from 'rxjs'
|
||||
|
||||
import { Maybe, Scalars } from '../../graphql-operations'
|
||||
import { ISearchContext } from '../../schema'
|
||||
|
||||
interface SearchContextFields {
|
||||
__typename: 'SearchContext'
|
||||
@ -15,6 +13,8 @@ interface SearchContextFields {
|
||||
autoDefined: boolean
|
||||
updatedAt: string
|
||||
viewerCanManage: boolean
|
||||
viewerHasAsDefault: boolean
|
||||
viewerHasStarred: boolean
|
||||
namespace: Maybe<
|
||||
| { __typename: 'User'; id: string; namespaceName: string }
|
||||
| { __typename: 'Org'; id: string; namespaceName: string }
|
||||
@ -33,26 +33,6 @@ interface ListSearchContexts {
|
||||
pageInfo: { hasNextPage: boolean; endCursor: Maybe<string> }
|
||||
}
|
||||
|
||||
export function mockFetchAutoDefinedSearchContexts(numberContexts = 0): () => Observable<ISearchContext[]> {
|
||||
return () =>
|
||||
of(
|
||||
range(0, numberContexts).map(index => ({
|
||||
__typename: 'SearchContext',
|
||||
id: index.toString(),
|
||||
spec: `auto-defined-${index}`,
|
||||
name: `auto-defined-${index}`,
|
||||
namespace: null,
|
||||
public: true,
|
||||
autoDefined: true,
|
||||
viewerCanManage: false,
|
||||
description: 'Repositories on Sourcegraph',
|
||||
repositories: [],
|
||||
query: '',
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
})) as ISearchContext[]
|
||||
)
|
||||
}
|
||||
|
||||
export function mockFetchSearchContexts({
|
||||
first,
|
||||
query,
|
||||
@ -63,7 +43,24 @@ export function mockFetchSearchContexts({
|
||||
after?: string
|
||||
}): Observable<ListSearchContexts> {
|
||||
const result: ListSearchContexts = {
|
||||
nodes: [],
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '0',
|
||||
spec: 'global',
|
||||
name: 'global',
|
||||
namespace: null,
|
||||
public: true,
|
||||
autoDefined: true,
|
||||
viewerCanManage: false,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
description: 'All code on Sourcegraph',
|
||||
repositories: [],
|
||||
query: '',
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
|
||||
@ -4,13 +4,7 @@ import classNames from 'classnames'
|
||||
import { Observable } from 'rxjs'
|
||||
import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect'
|
||||
|
||||
import {
|
||||
SearchPatternType,
|
||||
getUserSearchContextNamespaces,
|
||||
fetchAutoDefinedSearchContexts,
|
||||
QueryState,
|
||||
SearchMode,
|
||||
} from '@sourcegraph/search'
|
||||
import { SearchPatternType, getUserSearchContextNamespaces, QueryState, SearchMode } from '@sourcegraph/search'
|
||||
import { SearchBox } from '@sourcegraph/search-ui'
|
||||
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
|
||||
import { collectMetrics } from '@sourcegraph/shared/src/search/query/metrics'
|
||||
@ -188,7 +182,6 @@ export const SearchHomeView: React.FunctionComponent<React.PropsWithChildren<Sea
|
||||
setSelectedSearchContextSpec={setSelectedSearchContextSpec}
|
||||
selectedSearchContextSpec={context.selectedSearchContextSpec}
|
||||
fetchSearchContexts={fetchSearchContexts}
|
||||
fetchAutoDefinedSearchContexts={fetchAutoDefinedSearchContexts}
|
||||
getUserSearchContextNamespaces={getUserSearchContextNamespaces}
|
||||
fetchStreamSuggestions={fetchStreamSuggestions}
|
||||
settingsCascade={settingsCascade}
|
||||
|
||||
@ -4,13 +4,7 @@ import classNames from 'classnames'
|
||||
import { Observable } from 'rxjs'
|
||||
import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect'
|
||||
|
||||
import {
|
||||
SearchPatternType,
|
||||
fetchAutoDefinedSearchContexts,
|
||||
getUserSearchContextNamespaces,
|
||||
QueryState,
|
||||
SearchMode,
|
||||
} from '@sourcegraph/search'
|
||||
import { SearchPatternType, getUserSearchContextNamespaces, QueryState, SearchMode } from '@sourcegraph/search'
|
||||
import { IEditor, SearchBox, StreamingProgress, StreamingSearchResultsList } from '@sourcegraph/search-ui'
|
||||
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
|
||||
import { FetchFileParameters, fetchHighlightedFileLineRanges } from '@sourcegraph/shared/src/backend/file'
|
||||
@ -350,7 +344,6 @@ export const SearchResultsView: React.FunctionComponent<React.PropsWithChildren<
|
||||
setSelectedSearchContextSpec={setSelectedSearchContextSpec}
|
||||
selectedSearchContextSpec={context.selectedSearchContextSpec}
|
||||
fetchSearchContexts={fetchSearchContexts}
|
||||
fetchAutoDefinedSearchContexts={fetchAutoDefinedSearchContexts}
|
||||
getUserSearchContextNamespaces={getUserSearchContextNamespaces}
|
||||
fetchStreamSuggestions={fetchStreamSuggestions}
|
||||
settingsCascade={settingsCascade}
|
||||
|
||||
@ -54,6 +54,8 @@ const searchContextFragment = gql`
|
||||
autoDefined
|
||||
updatedAt
|
||||
viewerCanManage
|
||||
viewerHasStarred
|
||||
viewerHasAsDefault
|
||||
repositories {
|
||||
__typename
|
||||
repository {
|
||||
@ -138,6 +140,8 @@ export interface SearchContextFields {
|
||||
autoDefined: boolean
|
||||
updatedAt: string
|
||||
viewerCanManage: boolean
|
||||
viewerHasAsDefault: boolean
|
||||
viewerHasStarred: boolean
|
||||
query: string
|
||||
namespace: Maybe<
|
||||
| { __typename: 'User'; id: string; namespaceName: string }
|
||||
@ -150,13 +154,6 @@ export interface SearchContextFields {
|
||||
}[]
|
||||
}
|
||||
|
||||
export type AutoDefinedSearchContextsVariables = Exact<{ [key: string]: never }>
|
||||
|
||||
export interface AutoDefinedSearchContextsResult {
|
||||
__typename?: 'Query'
|
||||
autoDefinedSearchContexts: ({ __typename?: 'SearchContext' } & SearchContextFields)[]
|
||||
}
|
||||
|
||||
export type ListSearchContextsVariables = Exact<{
|
||||
first: Scalars['Int']
|
||||
after: Maybe<Scalars['String']>
|
||||
|
||||
@ -26,42 +26,6 @@ export const commonVSCodeGraphQlResults: Partial<
|
||||
pageInfo: { hasNextPage: false, endCursor: null },
|
||||
},
|
||||
}),
|
||||
AutoDefinedSearchContexts: () => ({
|
||||
autoDefinedSearchContexts: [
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '1',
|
||||
spec: 'global',
|
||||
name: 'global',
|
||||
namespace: null,
|
||||
autoDefined: true,
|
||||
public: true,
|
||||
description: 'All repositories on Sourcegraph',
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
query: '',
|
||||
viewerCanManage: false,
|
||||
},
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '2',
|
||||
spec: '@test',
|
||||
name: 'test',
|
||||
namespace: {
|
||||
__typename: 'User',
|
||||
id: 'u1',
|
||||
namespaceName: 'test',
|
||||
},
|
||||
autoDefined: true,
|
||||
public: true,
|
||||
description: 'Your repositories on Sourcegraph',
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
query: '',
|
||||
viewerCanManage: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
IsSearchContextAvailable: () => ({
|
||||
isSearchContextAvailable: true,
|
||||
}),
|
||||
|
||||
@ -15,7 +15,6 @@ import { logger } from '@sourcegraph/common'
|
||||
import { GraphQLClient, HTTPStatusError } from '@sourcegraph/http-client'
|
||||
import { SharedSpanName, TraceSpanProvider } from '@sourcegraph/observability-client'
|
||||
import {
|
||||
fetchAutoDefinedSearchContexts,
|
||||
getUserSearchContextNamespaces,
|
||||
SearchContextProps,
|
||||
fetchSearchContexts,
|
||||
@ -399,7 +398,6 @@ export class SourcegraphWebApp extends React.Component<
|
||||
selectedSearchContextSpec={this.getSelectedSearchContextSpec()}
|
||||
setSelectedSearchContextSpec={this.setSelectedSearchContextSpec}
|
||||
getUserSearchContextNamespaces={getUserSearchContextNamespaces}
|
||||
fetchAutoDefinedSearchContexts={fetchAutoDefinedSearchContexts}
|
||||
fetchSearchContexts={fetchSearchContexts}
|
||||
fetchSearchContextBySpec={fetchSearchContextBySpec}
|
||||
fetchSearchContext={fetchSearchContext}
|
||||
|
||||
@ -7,7 +7,6 @@ import { subtypeOf } from '@sourcegraph/common'
|
||||
import { SearchContextFields } from '@sourcegraph/search'
|
||||
import { ActionItemComponentProps } from '@sourcegraph/shared/src/actions/ActionItem'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -110,6 +109,8 @@ const fetchCommunitySearchContext = (): Observable<SearchContextFields> =>
|
||||
repositories,
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
})
|
||||
|
||||
const commonProps = () =>
|
||||
@ -134,7 +135,6 @@ const commonProps = () =>
|
||||
authenticatedUser: authUser,
|
||||
communitySearchContextMetadata: temporal,
|
||||
globbing: false,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
fetchSearchContextBySpec: fetchCommunitySearchContext,
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@ -85,8 +85,8 @@ export const FilterControl: React.FunctionComponent<React.PropsWithChildren<Filt
|
||||
const filterLabelId = `filtered-select-label-${filter.id}`
|
||||
return (
|
||||
<div key={filter.id} className="d-inline-flex flex-row align-center flex-wrap">
|
||||
<div className="d-inline-flex flex-row mr-3 align-items-baseline">
|
||||
<Text className="text-xl-center text-nowrap mr-2" id={filterLabelId}>
|
||||
<div className="d-inline-flex flex-row align-items-baseline">
|
||||
<Text className="text-xl-center text-nowrap mr-2 mb-0" id={filterLabelId}>
|
||||
{filter.label}:
|
||||
</Text>
|
||||
<Select
|
||||
|
||||
@ -84,6 +84,12 @@ interface FilteredConnectionDisplayProps extends ConnectionNodesDisplayProps, Co
|
||||
* the list to screen reader users (e.g. reading out nodes after they have finished loading).
|
||||
*/
|
||||
ariaLive?: 'polite' | 'off'
|
||||
|
||||
/**
|
||||
* A component that wraps around everything after the connection form. This is useful
|
||||
* for adding additional padding/background to the list, errors, or loading indicators.
|
||||
*/
|
||||
contentWrapperComponent?: React.ComponentType<{ children: React.ReactNode }>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -524,6 +530,8 @@ export class FilteredConnection<
|
||||
|
||||
const inputPlaceholder = this.props.inputPlaceholder || `Search ${this.props.pluralNoun}...`
|
||||
|
||||
const ContentWrapperComponent = this.props.contentWrapperComponent || React.Fragment
|
||||
|
||||
return (
|
||||
<ConnectionContainer
|
||||
compact={this.props.compact}
|
||||
@ -534,6 +542,7 @@ export class FilteredConnection<
|
||||
<ConnectionForm
|
||||
ref={this.setFilterRef}
|
||||
hideSearch={this.props.hideSearch}
|
||||
showSearchFirst={this.props.showSearchFirst}
|
||||
inputClassName={this.props.inputClassName}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
inputAriaLabel={this.props.inputAriaLabel || inputPlaceholder}
|
||||
@ -547,40 +556,43 @@ export class FilteredConnection<
|
||||
formClassName={this.props.formClassName}
|
||||
/>
|
||||
)}
|
||||
{errors.length > 0 && <ConnectionError errors={errors} compact={this.props.compact} />}
|
||||
|
||||
{this.state.connectionOrError && !isErrorLike(this.state.connectionOrError) && (
|
||||
<ConnectionNodes
|
||||
connection={this.state.connectionOrError}
|
||||
loading={this.state.loading}
|
||||
connectionQuery={this.state.connectionQuery}
|
||||
first={this.state.first}
|
||||
query={this.state.query}
|
||||
noun={this.props.noun}
|
||||
pluralNoun={this.props.pluralNoun}
|
||||
listComponent={this.props.listComponent}
|
||||
listClassName={this.props.listClassName}
|
||||
summaryClassName={this.props.summaryClassName}
|
||||
headComponent={this.props.headComponent}
|
||||
headComponentProps={this.props.headComponentProps}
|
||||
footComponent={this.props.footComponent}
|
||||
showMoreClassName={this.props.showMoreClassName}
|
||||
nodeComponent={this.props.nodeComponent}
|
||||
nodeComponentProps={this.props.nodeComponentProps}
|
||||
noShowMore={this.props.noShowMore}
|
||||
noSummaryIfAllNodesVisible={this.props.noSummaryIfAllNodesVisible}
|
||||
onShowMore={this.onClickShowMore}
|
||||
location={this.props.location}
|
||||
emptyElement={this.props.emptyElement}
|
||||
totalCountSummaryComponent={this.props.totalCountSummaryComponent}
|
||||
withCenteredSummary={this.props.withCenteredSummary}
|
||||
ariaLabelFunction={this.props.ariaLabelFunction}
|
||||
/>
|
||||
)}
|
||||
<ContentWrapperComponent>
|
||||
{errors.length > 0 && <ConnectionError errors={errors} compact={this.props.compact} />}
|
||||
|
||||
{this.state.loading && (
|
||||
<ConnectionLoading compact={this.props.compact} className={this.props.loaderClassName} />
|
||||
)}
|
||||
{this.state.connectionOrError && !isErrorLike(this.state.connectionOrError) && (
|
||||
<ConnectionNodes
|
||||
connection={this.state.connectionOrError}
|
||||
loading={this.state.loading}
|
||||
connectionQuery={this.state.connectionQuery}
|
||||
first={this.state.first}
|
||||
query={this.state.query}
|
||||
noun={this.props.noun}
|
||||
pluralNoun={this.props.pluralNoun}
|
||||
listComponent={this.props.listComponent}
|
||||
listClassName={this.props.listClassName}
|
||||
summaryClassName={this.props.summaryClassName}
|
||||
headComponent={this.props.headComponent}
|
||||
headComponentProps={this.props.headComponentProps}
|
||||
footComponent={this.props.footComponent}
|
||||
showMoreClassName={this.props.showMoreClassName}
|
||||
nodeComponent={this.props.nodeComponent}
|
||||
nodeComponentProps={this.props.nodeComponentProps}
|
||||
noShowMore={this.props.noShowMore}
|
||||
noSummaryIfAllNodesVisible={this.props.noSummaryIfAllNodesVisible}
|
||||
onShowMore={this.onClickShowMore}
|
||||
location={this.props.location}
|
||||
emptyElement={this.props.emptyElement}
|
||||
totalCountSummaryComponent={this.props.totalCountSummaryComponent}
|
||||
withCenteredSummary={this.props.withCenteredSummary}
|
||||
ariaLabelFunction={this.props.ariaLabelFunction}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.loading && (
|
||||
<ConnectionLoading compact={this.props.compact} className={this.props.loaderClassName} />
|
||||
)}
|
||||
</ContentWrapperComponent>
|
||||
</ConnectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,9 +11,12 @@ import { FilterControl, FilteredConnectionFilter, FilteredConnectionFilterValue
|
||||
import styles from './ConnectionForm.module.scss'
|
||||
|
||||
export interface ConnectionFormProps {
|
||||
/** Hides the filter input field. */
|
||||
/** Hides the search input field. */
|
||||
hideSearch?: boolean
|
||||
|
||||
/** Shows the search input field before the filter controls */
|
||||
showSearchFirst?: boolean
|
||||
|
||||
/** CSS class name for the <input> element */
|
||||
inputClassName?: string
|
||||
|
||||
@ -60,6 +63,7 @@ export const ConnectionForm = React.forwardRef<HTMLInputElement, ConnectionFormP
|
||||
(
|
||||
{
|
||||
hideSearch,
|
||||
showSearchFirst,
|
||||
formClassName,
|
||||
inputClassName,
|
||||
inputPlaceholder,
|
||||
@ -84,34 +88,37 @@ export const ConnectionForm = React.forwardRef<HTMLInputElement, ConnectionFormP
|
||||
|
||||
useAutoFocus({ autoFocus, reference: localReference })
|
||||
|
||||
const searchControl = !hideSearch && (
|
||||
<Input
|
||||
className={classNames(styles.input, inputClassName)}
|
||||
type="search"
|
||||
placeholder={inputPlaceholder}
|
||||
name="query"
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
autoFocus={autoFocus}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
ref={mergedReference}
|
||||
spellCheck={false}
|
||||
aria-label={inputAriaLabel}
|
||||
variant={compact ? 'small' : 'regular'}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={classNames(styles.form, !compact && styles.noncompact, formClassName)}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{showSearchFirst && searchControl}
|
||||
{filters && onFilterSelect && filterValues && (
|
||||
<FilterControl filters={filters} onValueSelect={onFilterSelect} values={filterValues}>
|
||||
{additionalFilterElement}
|
||||
</FilterControl>
|
||||
)}
|
||||
{!hideSearch && (
|
||||
<Input
|
||||
className={classNames(styles.input, inputClassName)}
|
||||
type="search"
|
||||
placeholder={inputPlaceholder}
|
||||
name="query"
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
autoFocus={autoFocus}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
ref={mergedReference}
|
||||
spellCheck={false}
|
||||
aria-label={inputAriaLabel}
|
||||
variant={compact ? 'small' : 'regular'}
|
||||
/>
|
||||
)}
|
||||
{!showSearchFirst && searchControl}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,6 +38,8 @@ const onSubmit = (): Observable<SearchContextFields> =>
|
||||
query: '',
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
})
|
||||
|
||||
const searchContextToEdit: SearchContextFields = {
|
||||
@ -59,6 +61,8 @@ const searchContextToEdit: SearchContextFields = {
|
||||
],
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
}
|
||||
|
||||
const authUser: AuthenticatedUser = {
|
||||
|
||||
@ -1,38 +1,74 @@
|
||||
@import 'wildcard/src/global-styles/breakpoints';
|
||||
|
||||
.search-context-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.row {
|
||||
td {
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid var(--border-color-2);
|
||||
|
||||
&:first-child {
|
||||
border-top: 1px solid var(--border-color-2);
|
||||
.spec {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
border-bottom: 1px solid var(--border-color-2);
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@media (--sm-breakpoint-down) {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-template-areas:
|
||||
'star name name actions'
|
||||
'star tags tags actions'
|
||||
'star description description description'
|
||||
'star contents updated updated';
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
|
||||
&.star {
|
||||
grid-area: star;
|
||||
}
|
||||
|
||||
&.name {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
&.description {
|
||||
grid-area: description;
|
||||
}
|
||||
|
||||
&.last-updated {
|
||||
grid-area: updated;
|
||||
}
|
||||
|
||||
&.contents {
|
||||
grid-area: contents;
|
||||
}
|
||||
|
||||
&.tags {
|
||||
grid-area: tags;
|
||||
}
|
||||
|
||||
&.actions {
|
||||
grid-area: actions;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
/* Enables responsive ellipsis text truncation */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
.button {
|
||||
color: var(--icon-color);
|
||||
font-size: 1rem;
|
||||
|
||||
&-description {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
font-size: 0.75rem;
|
||||
margin-left: 1rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
margin-left: 0;
|
||||
&:hover {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
import { mdiDotsHorizontal } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import * as H from 'history'
|
||||
|
||||
import { pluralize } from '@sourcegraph/common'
|
||||
import { SearchContextMinimalFields } from '@sourcegraph/search'
|
||||
import { SyntaxHighlightedSearchQuery } from '@sourcegraph/search-ui'
|
||||
import { Badge, Link } from '@sourcegraph/wildcard'
|
||||
import { Badge, Icon, Link, Menu, MenuButton, MenuLink, MenuList, Tooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { Timestamp } from '../../components/time/Timestamp'
|
||||
|
||||
@ -19,36 +20,91 @@ export interface SearchContextNodeProps {
|
||||
|
||||
export const SearchContextNode: React.FunctionComponent<React.PropsWithChildren<SearchContextNodeProps>> = ({
|
||||
node,
|
||||
}: SearchContextNodeProps) => (
|
||||
<li className={classNames('py-3', styles.searchContextNode)}>
|
||||
<div className={classNames('flex-grow-1', styles.left)}>
|
||||
<div>
|
||||
<Link to={`/contexts/${node.spec}`}>
|
||||
<strong>{node.spec}</strong>
|
||||
</Link>
|
||||
{!node.public && (
|
||||
<Badge variant="secondary" pill={true} className="ml-1" as="div">
|
||||
Private
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{node.query.length > 0 && (
|
||||
<small>
|
||||
<SyntaxHighlightedSearchQuery query={node.query} key={node.name} />
|
||||
</small>
|
||||
)}
|
||||
}: SearchContextNodeProps) => {
|
||||
const contents =
|
||||
node.repositories && node.repositories.length > 0 ? (
|
||||
<>
|
||||
{node.repositories.length} {pluralize('repository', node.repositories.length, 'repositories')}
|
||||
|
||||
</>
|
||||
) : node.query ? (
|
||||
<>Query based </>
|
||||
) : null
|
||||
|
||||
{node.description.length > 0 && (
|
||||
<div className={classNames('text-muted mt-1', styles.leftDescription)}>{node.description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames('text-muted d-flex', styles.right)}>
|
||||
{node.repositories && node.repositories.length > 0 && (
|
||||
<div className="mr-2">{node.repositories.length} repositories</div>
|
||||
)}
|
||||
<div>
|
||||
Updated <Timestamp date={node.updatedAt} noAbout={true} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
const tags = (
|
||||
<>
|
||||
{!node.public ? (
|
||||
<Badge variant="secondary" pill={true}>
|
||||
Private
|
||||
</Badge>
|
||||
) : null}{' '}
|
||||
{node.autoDefined ? (
|
||||
<Badge variant="outlineSecondary" pill={true}>
|
||||
Auto
|
||||
</Badge>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
|
||||
const timestamp = node.autoDefined ? null : (
|
||||
<>
|
||||
<span className="d-md-none" aria-hidden={true}>
|
||||
Updated{' '}
|
||||
</span>{' '}
|
||||
<Timestamp date={node.updatedAt} noAbout={true} />
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<tr className={styles.row}>
|
||||
<td className={styles.star} />
|
||||
<td className={styles.name}>
|
||||
<Link to={`/contexts/${node.spec}`}>{node.spec}</Link>{' '}
|
||||
<span className="d-none d-md-inline-block">{tags}</span>
|
||||
</td>
|
||||
<td className={styles.description}>
|
||||
{node.description ? <div className="text-muted">{node.description}</div> : null}
|
||||
</td>
|
||||
<td className={classNames(styles.contents, 'text-muted')}>{contents}</td>
|
||||
<td className={classNames(styles.lastUpdated, 'text-muted')}>
|
||||
{contents && timestamp && (
|
||||
<span className="d-md-none" aria-hidden={true}>
|
||||
{' '}
|
||||
•{' '}
|
||||
</span>
|
||||
)}
|
||||
{timestamp}
|
||||
</td>
|
||||
<td className={styles.tags}>
|
||||
{node.viewerHasAsDefault ? (
|
||||
<Badge variant="secondary" className="text-uppercase">
|
||||
Default
|
||||
</Badge>
|
||||
) : null}
|
||||
<span className="d-md-none">{tags}</span>
|
||||
</td>
|
||||
<td className={styles.actions}>
|
||||
<Menu>
|
||||
<MenuButton variant="icon" className={styles.button}>
|
||||
<Icon svgPath={mdiDotsHorizontal} aria-label="Actions" />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<Tooltip
|
||||
content={
|
||||
node.autoDefined
|
||||
? "Auto-defined contexts can't be edited."
|
||||
: !node.viewerCanManage
|
||||
? "You don't have permissions to edit this context."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<MenuLink as={Link} to={`/contexts/${node.spec}/edit`} disabled={!node.viewerCanManage}>
|
||||
Edit...
|
||||
</MenuLink>
|
||||
</Tooltip>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
@ -53,6 +53,8 @@ const mockContext: SearchContextFields = {
|
||||
repositories,
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
}
|
||||
|
||||
const fetchPublicContext = (): Observable<SearchContextFields> => of(mockContext)
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
@import 'wildcard/src/global-styles/breakpoints';
|
||||
|
||||
.badge {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
max-width: 30%;
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--color-bg-1);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
th {
|
||||
border-bottom: 2px solid var(--border-color-2);
|
||||
padding-bottom: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (--sm-breakpoint-down) {
|
||||
table {
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead {
|
||||
// Header should still be visible to screen readers, otherwise the table is not accessible.
|
||||
// Because this style is conditional, we can't simply use "sr-only" class.
|
||||
// So this is the same as the "sr-only" class, but copied here to work with the media query.
|
||||
position: absolute;
|
||||
width: 0.0625rem;
|
||||
height: 0.0625rem;
|
||||
padding: 0;
|
||||
margin: -0.0625rem; // Fix for https://github.com/twbs/bootstrap/issues/25686
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { DecoratorFn, Meta, Story } from '@storybook/react'
|
||||
|
||||
import {
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
import { NOOP_PLATFORM_CONTEXT } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { SearchContextsList, SearchContextsListProps } from './SearchContextsList'
|
||||
|
||||
const decorator: DecoratorFn = story => (
|
||||
<div className="p-3 container" style={{ position: 'static' }}>
|
||||
{story()}
|
||||
</div>
|
||||
)
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/enterprise/searchContexts/SearchContextsListTab',
|
||||
decorators: [decorator],
|
||||
parameters: {
|
||||
chromatic: { viewports: [1200], disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const defaultProps: SearchContextsListProps = {
|
||||
authenticatedUser: null,
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
platformContext: NOOP_PLATFORM_CONTEXT,
|
||||
}
|
||||
|
||||
export const Default: Story = () => <WebStory>{() => <SearchContextsList {...defaultProps} />}</WebStory>
|
||||
210
client/web/src/enterprise/searchContexts/SearchContextsList.tsx
Normal file
210
client/web/src/enterprise/searchContexts/SearchContextsList.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import React, { PropsWithChildren, useCallback, useMemo } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
|
||||
import {
|
||||
SearchContextProps,
|
||||
ListSearchContextsResult,
|
||||
ListSearchContextsVariables,
|
||||
SearchContextsOrderBy,
|
||||
SearchContextMinimalFields,
|
||||
} from '@sourcegraph/search'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import {
|
||||
FilteredConnection,
|
||||
FilteredConnectionFilter,
|
||||
FilteredConnectionFilterValue,
|
||||
} from '../../components/FilteredConnection'
|
||||
|
||||
import { SearchContextNode, SearchContextNodeProps } from './SearchContextNode'
|
||||
|
||||
import styles from './SearchContextsList.module.scss'
|
||||
|
||||
export interface SearchContextsListProps
|
||||
extends Pick<SearchContextProps, 'fetchSearchContexts' | 'getUserSearchContextNamespaces'>,
|
||||
PlatformContextProps<'requestGraphQL'> {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
}
|
||||
|
||||
export const SearchContextsList: React.FunctionComponent<SearchContextsListProps> = ({
|
||||
authenticatedUser,
|
||||
getUserSearchContextNamespaces,
|
||||
fetchSearchContexts,
|
||||
platformContext,
|
||||
}) => {
|
||||
const queryConnection = useCallback(
|
||||
(args: Partial<ListSearchContextsVariables>) => {
|
||||
const { namespace, orderBy, descending } = args as {
|
||||
namespace: string | undefined
|
||||
orderBy: SearchContextsOrderBy
|
||||
descending: boolean
|
||||
}
|
||||
const namespaces = namespace
|
||||
? [namespace === 'global' ? null : namespace]
|
||||
: getUserSearchContextNamespaces(authenticatedUser)
|
||||
|
||||
return fetchSearchContexts({
|
||||
first: args.first ?? 10,
|
||||
query: args.query ?? undefined,
|
||||
after: args.after ?? undefined,
|
||||
namespaces,
|
||||
orderBy,
|
||||
descending,
|
||||
platformContext,
|
||||
})
|
||||
},
|
||||
[authenticatedUser, fetchSearchContexts, getUserSearchContextNamespaces, platformContext]
|
||||
)
|
||||
|
||||
const ownerNamespaceFilterValues: FilteredConnectionFilterValue[] = useMemo(
|
||||
() =>
|
||||
authenticatedUser
|
||||
? [
|
||||
{
|
||||
value: authenticatedUser.id,
|
||||
label: authenticatedUser.username,
|
||||
args: {
|
||||
namespace: authenticatedUser.id,
|
||||
},
|
||||
},
|
||||
...authenticatedUser.organizations.nodes.map(org => ({
|
||||
value: org.id,
|
||||
label: org.displayName || org.name,
|
||||
args: {
|
||||
namespace: org.id,
|
||||
},
|
||||
})),
|
||||
]
|
||||
: [],
|
||||
[authenticatedUser]
|
||||
)
|
||||
|
||||
const filters: FilteredConnectionFilter[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Sort',
|
||||
type: 'select',
|
||||
id: 'order',
|
||||
tooltip: 'Order search contexts',
|
||||
values: [
|
||||
{
|
||||
value: 'spec-asc',
|
||||
label: 'A-Z',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_SPEC,
|
||||
descending: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'spec-desc',
|
||||
label: 'Z-A',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_SPEC,
|
||||
descending: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'updated-at-asc',
|
||||
label: 'Oldest updates',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_UPDATED_AT,
|
||||
descending: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'updated-at-desc',
|
||||
label: 'Newest updates',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_UPDATED_AT,
|
||||
descending: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Owner',
|
||||
type: 'select',
|
||||
id: 'owner',
|
||||
tooltip: 'Search context owner',
|
||||
values: [
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
value: 'global-owner',
|
||||
label: 'Global',
|
||||
args: {
|
||||
namespace: 'global',
|
||||
},
|
||||
},
|
||||
...ownerNamespaceFilterValues,
|
||||
],
|
||||
},
|
||||
],
|
||||
[ownerNamespaceFilterValues]
|
||||
)
|
||||
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<FilteredConnection<
|
||||
SearchContextMinimalFields,
|
||||
Omit<SearchContextNodeProps, 'node'>,
|
||||
ListSearchContextsResult['searchContexts']
|
||||
>
|
||||
listComponent="table"
|
||||
contentWrapperComponent={SearchContextsTableWrapper}
|
||||
headComponent={SearchContextsTableHeader}
|
||||
history={history}
|
||||
location={location}
|
||||
defaultFirst={10}
|
||||
compact={false}
|
||||
queryConnection={queryConnection}
|
||||
filters={filters}
|
||||
hideSearch={false}
|
||||
showSearchFirst={true}
|
||||
nodeComponent={SearchContextNode}
|
||||
nodeComponentProps={{
|
||||
location,
|
||||
history,
|
||||
}}
|
||||
noun="search context"
|
||||
pluralNoun="search contexts"
|
||||
cursorPaging={true}
|
||||
inputClassName={classNames(styles.filterInput)}
|
||||
inputPlaceholder="Find a context"
|
||||
inputAriaLabel="Find a context"
|
||||
formClassName={styles.filtersForm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchContextsTableWrapper: React.FunctionComponent<PropsWithChildren<{}>> = ({ children }) => (
|
||||
<div className={styles.tableWrapper}>{children}</div>
|
||||
)
|
||||
|
||||
const SearchContextsTableHeader: React.FunctionComponent = () => (
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span className="sr-only">Starred</span>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Contents</th>
|
||||
<th>Last updated</th>
|
||||
<th>
|
||||
<span className="sr-only">Tags</span>
|
||||
</th>
|
||||
<th>
|
||||
<span className="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
@ -0,0 +1,5 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { mdiMagnify, mdiPlus } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import * as H from 'history'
|
||||
|
||||
import { SearchContextProps } from '@sourcegraph/search'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
@ -12,123 +10,88 @@ import { AuthenticatedUser } from '../../auth'
|
||||
import { Page } from '../../components/Page'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
|
||||
import { SearchContextsListTab } from './SearchContextsListTab'
|
||||
import { SearchContextsList } from './SearchContextsList'
|
||||
|
||||
import styles from './SearchContextsListPage.module.scss'
|
||||
|
||||
export interface SearchContextsListPageProps
|
||||
extends Pick<
|
||||
SearchContextProps,
|
||||
'fetchSearchContexts' | 'fetchAutoDefinedSearchContexts' | 'getUserSearchContextNamespaces'
|
||||
>,
|
||||
extends Pick<SearchContextProps, 'fetchSearchContexts' | 'getUserSearchContextNamespaces'>,
|
||||
PlatformContextProps<'requestGraphQL'> {
|
||||
location: H.Location
|
||||
history: H.History
|
||||
isSourcegraphDotCom: boolean
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
}
|
||||
|
||||
type SelectedTab = 'list'
|
||||
|
||||
function getSelectedTabFromLocation(locationSearch: string): SelectedTab {
|
||||
const urlParameters = new URLSearchParams(locationSearch)
|
||||
switch (urlParameters.get('tab')) {
|
||||
case 'list':
|
||||
return 'list'
|
||||
}
|
||||
return 'list'
|
||||
}
|
||||
|
||||
function setSelectedLocationTab(location: H.Location, history: H.History, selectedTab: SelectedTab): void {
|
||||
const urlParameters = new URLSearchParams(location.search)
|
||||
urlParameters.set('tab', selectedTab)
|
||||
if (location.search !== urlParameters.toString()) {
|
||||
history.replace({ ...location, search: urlParameters.toString() })
|
||||
}
|
||||
}
|
||||
|
||||
export const SearchContextsListPage: React.FunctionComponent<
|
||||
React.PropsWithChildren<SearchContextsListPageProps>
|
||||
> = props => {
|
||||
const [selectedTab, setSelectedTab] = useState<SelectedTab>(getSelectedTabFromLocation(props.location.search))
|
||||
|
||||
const setTab = useCallback(
|
||||
(tab: SelectedTab) => {
|
||||
setSelectedTab(tab)
|
||||
setSelectedLocationTab(props.location, props.history, tab)
|
||||
},
|
||||
[props.location, props.history]
|
||||
)
|
||||
|
||||
const onSelectSearchContextsList = useCallback<React.MouseEventHandler>(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
setTab('list')
|
||||
},
|
||||
[setTab]
|
||||
)
|
||||
|
||||
return (
|
||||
<div data-testid="search-contexts-list-page" className="w-100">
|
||||
<Page>
|
||||
<PageHeader
|
||||
actions={
|
||||
<>
|
||||
<Button to="/contexts/new" variant="primary" as={Link}>
|
||||
<Icon aria-hidden={true} svgPath={mdiPlus} />
|
||||
Create search context
|
||||
export const SearchContextsListPage: React.FunctionComponent<SearchContextsListPageProps> = ({
|
||||
authenticatedUser,
|
||||
getUserSearchContextNamespaces,
|
||||
fetchSearchContexts,
|
||||
platformContext,
|
||||
isSourcegraphDotCom,
|
||||
}) => (
|
||||
<div data-testid="search-contexts-list-page" className="w-100">
|
||||
<Page>
|
||||
<PageHeader
|
||||
actions={
|
||||
<div className={styles.actions}>
|
||||
<Button to="/contexts/new" variant="primary" as={Link}>
|
||||
<Icon aria-hidden={true} svgPath={mdiPlus} />
|
||||
Create search context
|
||||
</Button>
|
||||
{isSourcegraphDotCom && (
|
||||
<Button
|
||||
to="https://signup.sourcegraph.com/?p=context"
|
||||
className="mt-2"
|
||||
as={Link}
|
||||
variant="secondary"
|
||||
onClick={() => eventLogger.log('ClickedOnCloudCTA')}
|
||||
>
|
||||
Search private code
|
||||
</Button>
|
||||
{props.isSourcegraphDotCom && (
|
||||
<Button
|
||||
to="https://signup.sourcegraph.com/?p=context"
|
||||
className="d-block mt-2"
|
||||
as={Link}
|
||||
variant="secondary"
|
||||
onClick={() => eventLogger.log('ClickedOnCloudCTA')}
|
||||
>
|
||||
Search private code
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
description={
|
||||
<span className="text-muted">
|
||||
Search code you care about with search contexts.{' '}
|
||||
<Link
|
||||
to="/help/code_search/explanations/features#search-contexts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
className="align-items-center mb-3"
|
||||
>
|
||||
<PageHeader.Heading as="h2" styleAs="h1">
|
||||
<PageHeader.Breadcrumb icon={mdiMagnify} to="/search" aria-label="Code Search" />
|
||||
<PageHeader.Breadcrumb>Contexts</PageHeader.Breadcrumb>
|
||||
</PageHeader.Heading>
|
||||
</PageHeader>
|
||||
<div className="mb-4">
|
||||
<div id="search-context-tabs-list" className="nav nav-tabs">
|
||||
<div className="nav-item">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<Link
|
||||
to=""
|
||||
role="tab"
|
||||
aria-selected={selectedTab === 'list'}
|
||||
aria-controls="search-context-tabs-list"
|
||||
onClick={onSelectSearchContextsList}
|
||||
className={classNames('nav-link', selectedTab === 'list' && 'active')}
|
||||
>
|
||||
<span className="text-content" data-tab-content="Your search contexts">
|
||||
Your search contexts
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<span className="text-muted">
|
||||
Search code you care about with search contexts.{' '}
|
||||
<Link
|
||||
to="/help/code_search/explanations/features#search-contexts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
className="mb-3"
|
||||
>
|
||||
<PageHeader.Heading as="h2" styleAs="h1">
|
||||
<PageHeader.Breadcrumb icon={mdiMagnify} to="/search" aria-label="Code Search" />
|
||||
<PageHeader.Breadcrumb>Contexts</PageHeader.Breadcrumb>
|
||||
</PageHeader.Heading>
|
||||
</PageHeader>
|
||||
<div id="search-context-tabs-list" className="nav nav-tabs">
|
||||
<div className="nav-item" role="tablist">
|
||||
<Link
|
||||
to="/contexts"
|
||||
role="tab"
|
||||
aria-selected={true}
|
||||
aria-controls="search-context-list"
|
||||
className="nav-link active"
|
||||
>
|
||||
<span className="text-content" data-tab-content="Your search contexts">
|
||||
Available contexts
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
{selectedTab === 'list' && <SearchContextsListTab {...props} />}
|
||||
</Page>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div role="tabpanel" id="search-context-list">
|
||||
<SearchContextsList
|
||||
authenticatedUser={authenticatedUser}
|
||||
getUserSearchContextNamespaces={getUserSearchContextNamespaces}
|
||||
fetchSearchContexts={fetchSearchContexts}
|
||||
platformContext={platformContext}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
@import 'wildcard/src/global-styles/breakpoints';
|
||||
|
||||
.badge {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
@media (--xs-breakpoint-down) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
max-width: 30%;
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
import { DecoratorFn, Meta, Story } from '@storybook/react'
|
||||
import { subDays } from 'date-fns'
|
||||
import { Observable, of } from 'rxjs'
|
||||
|
||||
import { ListSearchContextsResult } from '@sourcegraph/search'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
import { NOOP_PLATFORM_CONTEXT } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { SearchContextsListTab, SearchContextsListTabProps } from './SearchContextsListTab'
|
||||
|
||||
const decorator: DecoratorFn = story => (
|
||||
<div className="p-3 container" style={{ position: 'static' }}>
|
||||
{story()}
|
||||
</div>
|
||||
)
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/enterprise/searchContexts/SearchContextsListTab',
|
||||
decorators: [decorator],
|
||||
parameters: {
|
||||
chromatic: { viewports: [1200], disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const defaultProps: SearchContextsListTabProps = {
|
||||
authenticatedUser: null,
|
||||
isSourcegraphDotCom: true,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
platformContext: NOOP_PLATFORM_CONTEXT,
|
||||
}
|
||||
|
||||
const propsWithContexts: SearchContextsListTabProps = {
|
||||
...defaultProps,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(1),
|
||||
fetchSearchContexts: ({
|
||||
first,
|
||||
query,
|
||||
after,
|
||||
}: {
|
||||
first: number
|
||||
query?: string
|
||||
after?: string
|
||||
}): Observable<ListSearchContextsResult['searchContexts']> =>
|
||||
of({
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '3',
|
||||
spec: '@username/test-version-1.5',
|
||||
name: 'test-version-1.5',
|
||||
namespace: {
|
||||
__typename: 'User',
|
||||
id: 'u1',
|
||||
namespaceName: 'username',
|
||||
},
|
||||
autoDefined: false,
|
||||
public: true,
|
||||
description: 'Only code in version 1.5',
|
||||
query: '',
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
repositories: [],
|
||||
viewerCanManage: true,
|
||||
},
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '4',
|
||||
spec: '@username/test-version-1.6',
|
||||
namespace: {
|
||||
__typename: 'User',
|
||||
id: 'u1',
|
||||
namespaceName: 'username',
|
||||
},
|
||||
name: 'test-version-1.6',
|
||||
autoDefined: false,
|
||||
public: false,
|
||||
description: 'Only code in version 1.6',
|
||||
query: '',
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
repositories: [],
|
||||
viewerCanManage: true,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 1,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Default: Story = () => <WebStory>{() => <SearchContextsListTab {...defaultProps} />}</WebStory>
|
||||
|
||||
export const WithSourcegraphDotComDisabled: Story = () => (
|
||||
<WebStory>{() => <SearchContextsListTab {...propsWithContexts} isSourcegraphDotCom={false} />}</WebStory>
|
||||
)
|
||||
|
||||
WithSourcegraphDotComDisabled.storyName = 'with SourcegraphDotCom disabled'
|
||||
|
||||
export const With1AutoDefinedContext: Story = () => (
|
||||
<WebStory>{() => <SearchContextsListTab {...propsWithContexts} />}</WebStory>
|
||||
)
|
||||
|
||||
With1AutoDefinedContext.storyName = 'with 1 auto-defined context'
|
||||
|
||||
export const With2AutoDefinedContexts: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<SearchContextsListTab
|
||||
{...propsWithContexts}
|
||||
fetchAutoDefinedSearchContexts={mockFetchAutoDefinedSearchContexts(2)}
|
||||
/>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
With2AutoDefinedContexts.storyName = 'with 2 auto-defined contexts'
|
||||
|
||||
export const With3AutoDefinedContexts: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<SearchContextsListTab
|
||||
{...propsWithContexts}
|
||||
fetchAutoDefinedSearchContexts={mockFetchAutoDefinedSearchContexts(3)}
|
||||
/>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
With3AutoDefinedContexts.storyName = 'with 3 auto-defined contexts'
|
||||
|
||||
export const With4AutoDefinedContexts: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<SearchContextsListTab
|
||||
{...propsWithContexts}
|
||||
fetchAutoDefinedSearchContexts={mockFetchAutoDefinedSearchContexts(4)}
|
||||
/>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
With4AutoDefinedContexts.storyName = 'with 4 auto-defined contexts'
|
||||
@ -1,183 +0,0 @@
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
|
||||
import {
|
||||
SearchContextProps,
|
||||
ListSearchContextsResult,
|
||||
ListSearchContextsVariables,
|
||||
SearchContextsOrderBy,
|
||||
SearchContextMinimalFields,
|
||||
} from '@sourcegraph/search'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import {
|
||||
FilteredConnection,
|
||||
FilteredConnectionFilter,
|
||||
FilteredConnectionFilterValue,
|
||||
} from '../../components/FilteredConnection'
|
||||
|
||||
import { SearchContextNode, SearchContextNodeProps } from './SearchContextNode'
|
||||
|
||||
import styles from './SearchContextsListTab.module.scss'
|
||||
|
||||
export interface SearchContextsListTabProps
|
||||
extends Pick<
|
||||
SearchContextProps,
|
||||
'fetchSearchContexts' | 'fetchAutoDefinedSearchContexts' | 'getUserSearchContextNamespaces'
|
||||
>,
|
||||
PlatformContextProps<'requestGraphQL'> {
|
||||
isSourcegraphDotCom: boolean
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
}
|
||||
|
||||
export const SearchContextsListTab: React.FunctionComponent<React.PropsWithChildren<SearchContextsListTabProps>> = ({
|
||||
isSourcegraphDotCom,
|
||||
authenticatedUser,
|
||||
getUserSearchContextNamespaces,
|
||||
fetchSearchContexts,
|
||||
fetchAutoDefinedSearchContexts,
|
||||
platformContext,
|
||||
}) => {
|
||||
const queryConnection = useCallback(
|
||||
(args: Partial<ListSearchContextsVariables>) => {
|
||||
const { namespace, orderBy, descending } = args as {
|
||||
namespace: string | undefined
|
||||
orderBy: SearchContextsOrderBy
|
||||
descending: boolean
|
||||
}
|
||||
const namespaces = namespace
|
||||
? [namespace === 'global' ? null : namespace]
|
||||
: getUserSearchContextNamespaces(authenticatedUser)
|
||||
|
||||
return fetchSearchContexts({
|
||||
first: args.first ?? 10,
|
||||
query: args.query ?? undefined,
|
||||
after: args.after ?? undefined,
|
||||
namespaces,
|
||||
orderBy,
|
||||
descending,
|
||||
platformContext,
|
||||
})
|
||||
},
|
||||
[authenticatedUser, fetchSearchContexts, getUserSearchContextNamespaces, platformContext]
|
||||
)
|
||||
|
||||
const ownerNamespaceFilterValues: FilteredConnectionFilterValue[] = authenticatedUser
|
||||
? [
|
||||
{
|
||||
value: authenticatedUser.id,
|
||||
label: authenticatedUser.username,
|
||||
args: {
|
||||
namespace: authenticatedUser.id,
|
||||
},
|
||||
},
|
||||
...authenticatedUser.organizations.nodes.map(org => ({
|
||||
value: org.id,
|
||||
label: org.displayName || org.name,
|
||||
args: {
|
||||
namespace: org.id,
|
||||
},
|
||||
})),
|
||||
]
|
||||
: []
|
||||
|
||||
const filters: FilteredConnectionFilter[] = [
|
||||
{
|
||||
label: 'Owner',
|
||||
type: 'select',
|
||||
id: 'owner',
|
||||
tooltip: 'Search context owner',
|
||||
values: [
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
value: 'global-owner',
|
||||
label: 'Global',
|
||||
args: {
|
||||
namespace: 'global',
|
||||
},
|
||||
},
|
||||
...ownerNamespaceFilterValues,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Sort by',
|
||||
type: 'select',
|
||||
id: 'order',
|
||||
tooltip: 'Order search contexts',
|
||||
values: [
|
||||
{
|
||||
value: 'spec-asc',
|
||||
label: 'A-Z',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_SPEC,
|
||||
descending: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'spec-desc',
|
||||
label: 'Z-A',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_SPEC,
|
||||
descending: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'updated-at-asc',
|
||||
label: 'Oldest updates',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_UPDATED_AT,
|
||||
descending: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'updated-at-desc',
|
||||
label: 'Newest updates',
|
||||
args: {
|
||||
orderBy: SearchContextsOrderBy.SEARCH_CONTEXT_UPDATED_AT,
|
||||
descending: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
return (
|
||||
<>
|
||||
<FilteredConnection<
|
||||
SearchContextMinimalFields,
|
||||
Omit<SearchContextNodeProps, 'node'>,
|
||||
ListSearchContextsResult['searchContexts']
|
||||
>
|
||||
history={history}
|
||||
location={location}
|
||||
defaultFirst={10}
|
||||
compact={false}
|
||||
queryConnection={queryConnection}
|
||||
filters={filters}
|
||||
hideSearch={false}
|
||||
nodeComponent={SearchContextNode}
|
||||
nodeComponentProps={{
|
||||
location,
|
||||
history,
|
||||
}}
|
||||
noun="search context"
|
||||
pluralNoun="search contexts"
|
||||
noSummaryIfAllNodesVisible={true}
|
||||
cursorPaging={true}
|
||||
inputClassName={classNames(styles.filterInput)}
|
||||
inputPlaceholder="Filter search contexts..."
|
||||
inputAriaLabel="Filter search contexts"
|
||||
formClassName={styles.filtersForm}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -27,9 +27,6 @@ describe('Code monitoring', () => {
|
||||
})
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
AutoDefinedSearchContexts: () => ({
|
||||
autoDefinedSearchContexts: [],
|
||||
}),
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
__typename: 'SettingsCascade',
|
||||
|
||||
@ -134,42 +134,6 @@ export const commonWebGraphQlResults: Partial<
|
||||
alwaysNil: null,
|
||||
},
|
||||
}),
|
||||
AutoDefinedSearchContexts: () => ({
|
||||
autoDefinedSearchContexts: [
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '1',
|
||||
spec: 'global',
|
||||
name: 'global',
|
||||
namespace: null,
|
||||
autoDefined: true,
|
||||
public: true,
|
||||
description: 'All repositories on Sourcegraph',
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
query: '',
|
||||
viewerCanManage: false,
|
||||
},
|
||||
{
|
||||
__typename: 'SearchContext',
|
||||
id: '2',
|
||||
spec: '@test',
|
||||
name: 'test',
|
||||
namespace: {
|
||||
__typename: 'User',
|
||||
id: 'u1',
|
||||
namespaceName: 'test',
|
||||
},
|
||||
autoDefined: true,
|
||||
public: true,
|
||||
description: 'Your repositories on Sourcegraph',
|
||||
updatedAt: '2021-03-15T19:39:11Z',
|
||||
repositories: [],
|
||||
query: '',
|
||||
viewerCanManage: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
ListSearchContexts: () => ({
|
||||
searchContexts: {
|
||||
nodes: [],
|
||||
|
||||
@ -142,6 +142,8 @@ describe('Search contexts', () => {
|
||||
autoDefined: false,
|
||||
updatedAt: '',
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
query: searchContext.query,
|
||||
repositories: repositories.map(repository => ({
|
||||
__typename: 'SearchContextRepositoryRevisions',
|
||||
@ -215,6 +217,8 @@ describe('Search contexts', () => {
|
||||
autoDefined: false,
|
||||
updatedAt: '',
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
query: searchContext.query,
|
||||
repositories: repositories.map(repository => ({
|
||||
__typename: 'SearchContextRepositoryRevisions',
|
||||
@ -291,6 +295,8 @@ describe('Search contexts', () => {
|
||||
autoDefined: false,
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
query: '',
|
||||
repositories: repositories.map(repository => ({
|
||||
__typename: 'SearchContextRepositoryRevisions',
|
||||
@ -315,6 +321,8 @@ describe('Search contexts', () => {
|
||||
autoDefined: false,
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
query: '',
|
||||
repositories: [
|
||||
{
|
||||
@ -386,6 +394,8 @@ describe('Search contexts', () => {
|
||||
autoDefined: false,
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: false,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
query: '',
|
||||
repositories: [],
|
||||
},
|
||||
@ -425,6 +435,8 @@ describe('Search contexts', () => {
|
||||
autoDefined: false,
|
||||
updatedAt: subDays(new Date(), 1).toISOString(),
|
||||
viewerCanManage: true,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
query: '',
|
||||
repositories: [
|
||||
{
|
||||
@ -457,9 +469,6 @@ describe('Search contexts', () => {
|
||||
|
||||
testContext.overrideGraphQL({
|
||||
...testContextForSearchContexts,
|
||||
AutoDefinedSearchContexts: () => ({
|
||||
autoDefinedSearchContexts: [],
|
||||
}),
|
||||
ListSearchContexts: ({ after }) => {
|
||||
const searchContexts = range(0, searchContextsCount).map(index => ({
|
||||
__typename: 'SearchContext',
|
||||
@ -470,6 +479,8 @@ describe('Search contexts', () => {
|
||||
public: true,
|
||||
autoDefined: false,
|
||||
viewerCanManage: false,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
description: '',
|
||||
repositories: [],
|
||||
query: '',
|
||||
@ -537,9 +548,6 @@ describe('Search contexts', () => {
|
||||
IsSearchContextAvailable: () => ({
|
||||
isSearchContextAvailable: true,
|
||||
}),
|
||||
AutoDefinedSearchContexts: () => ({
|
||||
autoDefinedSearchContexts: [],
|
||||
}),
|
||||
ListSearchContexts: () => {
|
||||
const nodes = range(0, 2).map(index => ({
|
||||
__typename: 'SearchContext',
|
||||
@ -550,6 +558,8 @@ describe('Search contexts', () => {
|
||||
public: true,
|
||||
autoDefined: false,
|
||||
viewerCanManage: false,
|
||||
viewerHasAsDefault: false,
|
||||
viewerHasStarred: false,
|
||||
description: '',
|
||||
repositories: [],
|
||||
query: '',
|
||||
|
||||
@ -3,7 +3,6 @@ import { createMemoryHistory } from 'history'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -43,7 +42,6 @@ const getDefaultProps = (props: ThemeProps): GlobalNavbarProps => ({
|
||||
batchChangesExecutionEnabled: false,
|
||||
batchChangesWebhookLogsEnabled: false,
|
||||
routes: [],
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
showKeyboardShortcutsHelp: () => undefined,
|
||||
|
||||
@ -5,7 +5,6 @@ import { createLocation, createMemoryHistory } from 'history'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/shared/src/testing'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -43,7 +42,6 @@ const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
|
||||
branding: undefined,
|
||||
routes: [],
|
||||
searchContextsEnabled: true,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
showKeyboardShortcutsHelp: () => undefined,
|
||||
|
||||
@ -6,7 +6,6 @@ import { getDocumentNode } from '@sourcegraph/http-client'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -55,7 +54,6 @@ const defaultProps = (props: ThemeProps): SearchPageProps => ({
|
||||
defaultSearchContextSpec: '',
|
||||
isLightTheme: props.isLightTheme,
|
||||
now: () => parseISO('2020-09-16T23:15:01Z'),
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
})
|
||||
|
||||
@ -6,7 +6,6 @@ import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/teleme
|
||||
import { renderWithBrandedContext } from '@sourcegraph/shared/src/testing'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import {
|
||||
mockFetchAutoDefinedSearchContexts,
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
@ -106,7 +105,6 @@ describe('SearchPage', () => {
|
||||
setSelectedSearchContextSpec: () => {},
|
||||
defaultSearchContextSpec: '',
|
||||
isLightTheme: true,
|
||||
fetchAutoDefinedSearchContexts: mockFetchAutoDefinedSearchContexts(),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
}
|
||||
|
||||
@ -29,6 +29,10 @@ type SearchContextsResolver interface {
|
||||
UpdateSearchContext(ctx context.Context, args UpdateSearchContextArgs) (SearchContextResolver, error)
|
||||
DeleteSearchContext(ctx context.Context, args DeleteSearchContextArgs) (*EmptyResponse, error)
|
||||
|
||||
CreateSearchContextStar(ctx context.Context, args CreateSearchContextStarArgs) (*EmptyResponse, error)
|
||||
DeleteSearchContextStar(ctx context.Context, args DeleteSearchContextStarArgs) (*EmptyResponse, error)
|
||||
SetDefaultSearchContext(ctx context.Context, args SetDefaultSearchContextArgs) (*EmptyResponse, error)
|
||||
|
||||
NodeResolvers() map[string]NodeByIDFunc
|
||||
SearchContextsToResolvers(searchContexts []*types.SearchContext) []SearchContextResolver
|
||||
}
|
||||
@ -43,6 +47,8 @@ type SearchContextResolver interface {
|
||||
UpdatedAt() gqlutil.DateTime
|
||||
Namespace(ctx context.Context) (*NamespaceResolver, error)
|
||||
ViewerCanManage(ctx context.Context) bool
|
||||
ViewerHasAsDefault(ctx context.Context) bool
|
||||
ViewerHasStarred(ctx context.Context) bool
|
||||
Repositories(ctx context.Context) ([]SearchContextRepositoryRevisionsResolver, error)
|
||||
Query() string
|
||||
}
|
||||
@ -93,6 +99,21 @@ type DeleteSearchContextArgs struct {
|
||||
ID graphql.ID
|
||||
}
|
||||
|
||||
type CreateSearchContextStarArgs struct {
|
||||
SearchContextID graphql.ID
|
||||
UserID graphql.ID
|
||||
}
|
||||
|
||||
type DeleteSearchContextStarArgs struct {
|
||||
SearchContextID graphql.ID
|
||||
UserID graphql.ID
|
||||
}
|
||||
|
||||
type SetDefaultSearchContextArgs struct {
|
||||
SearchContextID graphql.ID
|
||||
UserID graphql.ID
|
||||
}
|
||||
|
||||
type SearchContextBySpecArgs struct {
|
||||
Spec string
|
||||
}
|
||||
|
||||
@ -33,10 +33,26 @@ extend type Mutation {
|
||||
"""
|
||||
repositories: [SearchContextRepositoryRevisionsInput!]!
|
||||
): SearchContext!
|
||||
"""
|
||||
Add a star on a search context for the specified user.
|
||||
Only one star can be created per context and user pair.
|
||||
If the star already exists, this is a no-op.
|
||||
"""
|
||||
createSearchContextStar(searchContextId: ID!, userId: ID!): EmptyResponse!
|
||||
"""
|
||||
Delete a star on a search context for the specified user.
|
||||
If the star does not exist, this is a no-op.
|
||||
"""
|
||||
deleteSearchContextStar(searchContextId: ID!, userId: ID!): EmptyResponse!
|
||||
"""
|
||||
Set the default search context for the specified user.
|
||||
"""
|
||||
setDefaultSearchContext(searchContextId: ID!, userId: ID!): EmptyResponse!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
"""
|
||||
DEPRECATED: Auto-defined contexts are now included in the searchContexts query.
|
||||
Auto-defined search contexts available to the current user.
|
||||
"""
|
||||
autoDefinedSearchContexts: [SearchContext!]!
|
||||
@ -66,6 +82,10 @@ extend type Query {
|
||||
namespaces: [ID] = []
|
||||
"""
|
||||
Sort field.
|
||||
Despite the value, the results are always sorted with the global context first,
|
||||
user's default context next, followed by the user's starred contexts,
|
||||
followed by the rest of the contexts.
|
||||
This controls the order of these internal groups.
|
||||
"""
|
||||
orderBy: SearchContextsOrderBy = SEARCH_CONTEXT_SPEC
|
||||
"""
|
||||
@ -140,6 +160,14 @@ type SearchContext implements Node {
|
||||
If current viewer can manage (edit, delete) the search context.
|
||||
"""
|
||||
viewerCanManage: Boolean!
|
||||
"""
|
||||
If the viewer has set this context as default.
|
||||
"""
|
||||
viewerHasAsDefault: Boolean!
|
||||
"""
|
||||
If the viewer has starred this context.
|
||||
"""
|
||||
viewerHasStarred: Boolean!
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gqlutil"
|
||||
@ -195,6 +196,93 @@ func (r *Resolver) DeleteSearchContext(ctx context.Context, args graphqlbackend.
|
||||
return &graphqlbackend.EmptyResponse{}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateSearchContextStar(ctx context.Context, args graphqlbackend.CreateSearchContextStarArgs) (*graphqlbackend.EmptyResponse, error) {
|
||||
// 🚨 SECURITY: Make sure the current user has permission to star the search context.
|
||||
userID, err := graphqlbackend.UnmarshalUserID(args.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchContextSpec, err := unmarshalSearchContextID(args.SearchContextID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchContext, err := searchcontexts.ResolveSearchContextSpec(ctx, r.db, searchContextSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = searchcontexts.CreateSearchContextStarForUser(ctx, r.db, searchContext, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &graphqlbackend.EmptyResponse{}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) DeleteSearchContextStar(ctx context.Context, args graphqlbackend.DeleteSearchContextStarArgs) (*graphqlbackend.EmptyResponse, error) {
|
||||
// 🚨 SECURITY: Make sure the current user has permission to star the search context.
|
||||
userID, err := graphqlbackend.UnmarshalUserID(args.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchContextSpec, err := unmarshalSearchContextID(args.SearchContextID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchContext, err := searchcontexts.ResolveSearchContextSpec(ctx, r.db, searchContextSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = searchcontexts.DeleteSearchContextStarForUser(ctx, r.db, searchContext, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &graphqlbackend.EmptyResponse{}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) SetDefaultSearchContext(ctx context.Context, args graphqlbackend.SetDefaultSearchContextArgs) (*graphqlbackend.EmptyResponse, error) {
|
||||
// 🚨 SECURITY: Make sure the current user has permission to set the search context as default.
|
||||
userID, err := graphqlbackend.UnmarshalUserID(args.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchContextSpec, err := unmarshalSearchContextID(args.SearchContextID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchContext, err := searchcontexts.ResolveSearchContextSpec(ctx, r.db, searchContextSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = searchcontexts.SetDefaultSearchContextForUser(ctx, r.db, searchContext, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &graphqlbackend.EmptyResponse{}, nil
|
||||
}
|
||||
|
||||
func unmarshalSearchContextCursor(cursor *string) (int32, error) {
|
||||
var after int32
|
||||
if cursor == nil {
|
||||
@ -392,6 +480,14 @@ func (r *searchContextResolver) ViewerCanManage(ctx context.Context) bool {
|
||||
return !searchcontexts.IsAutoDefinedSearchContext(r.sc) && hasWriteAccess
|
||||
}
|
||||
|
||||
func (r *searchContextResolver) ViewerHasAsDefault(ctx context.Context) bool {
|
||||
return r.sc.Default
|
||||
}
|
||||
|
||||
func (r *searchContextResolver) ViewerHasStarred(ctx context.Context) bool {
|
||||
return r.sc.Starred
|
||||
}
|
||||
|
||||
func (r *searchContextResolver) Repositories(ctx context.Context) ([]graphqlbackend.SearchContextRepositoryRevisionsResolver, error) {
|
||||
if searchcontexts.IsAutoDefinedSearchContext(r.sc) {
|
||||
return []graphqlbackend.SearchContextRepositoryRevisionsResolver{}, nil
|
||||
|
||||
@ -126,3 +126,79 @@ func TestSearchContexts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextsStarDefaultPermissions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userID := int32(1)
|
||||
graphqlUserID := graphqlbackend.MarshalUserID(userID)
|
||||
username := "alice"
|
||||
ctx := context.Background()
|
||||
ctx = actor.WithActor(ctx, &actor.Actor{UID: userID})
|
||||
|
||||
orig := envvar.SourcegraphDotComMode()
|
||||
envvar.MockSourcegraphDotComMode(true)
|
||||
defer envvar.MockSourcegraphDotComMode(orig) // reset
|
||||
|
||||
users := database.NewMockUserStore()
|
||||
users.GetByIDFunc.SetDefaultReturn(&types.User{Username: username}, nil)
|
||||
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{ID: userID, Username: username}, nil)
|
||||
|
||||
searchContextSpec := "test"
|
||||
graphqlSearchContextID := marshalSearchContextID(searchContextSpec)
|
||||
|
||||
sc := database.NewMockSearchContextsStore()
|
||||
sc.GetSearchContextFunc.SetDefaultReturn(&types.SearchContext{ID: 0, Name: searchContextSpec}, nil)
|
||||
|
||||
db := database.NewMockDB()
|
||||
db.UsersFunc.SetDefaultReturn(users)
|
||||
db.SearchContextsFunc.SetDefaultReturn(sc)
|
||||
|
||||
// User not admin, trying to set things for themselves
|
||||
_, err := (&Resolver{db: db}).SetDefaultSearchContext(ctx, graphqlbackend.SetDefaultSearchContextArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
_, err = (&Resolver{db: db}).CreateSearchContextStar(ctx, graphqlbackend.CreateSearchContextStarArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
_, err = (&Resolver{db: db}).DeleteSearchContextStar(ctx, graphqlbackend.DeleteSearchContextStarArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
|
||||
// User not admin, trying to set things for another user
|
||||
graphqlUserID2 := graphqlbackend.MarshalUserID(int32(2))
|
||||
unauthorizedError := "must be authenticated as the authorized user or site admin"
|
||||
|
||||
_, err = (&Resolver{db: db}).SetDefaultSearchContext(ctx, graphqlbackend.SetDefaultSearchContextArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID2})
|
||||
if err.Error() != unauthorizedError {
|
||||
t.Fatalf("expected error %s, got %s", unauthorizedError, err)
|
||||
}
|
||||
_, err = (&Resolver{db: db}).CreateSearchContextStar(ctx, graphqlbackend.CreateSearchContextStarArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID2})
|
||||
if err.Error() != unauthorizedError {
|
||||
t.Fatalf("expected error %s, got %s", unauthorizedError, err)
|
||||
}
|
||||
_, err = (&Resolver{db: db}).DeleteSearchContextStar(ctx, graphqlbackend.DeleteSearchContextStarArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID2})
|
||||
if err.Error() != unauthorizedError {
|
||||
t.Fatalf("expected error %s, got %s", unauthorizedError, err)
|
||||
}
|
||||
|
||||
// User is admin, trying to set things for another user
|
||||
users.GetByIDFunc.SetDefaultReturn(&types.User{Username: username, SiteAdmin: true}, nil)
|
||||
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{Username: username, SiteAdmin: true}, nil)
|
||||
|
||||
_, err = (&Resolver{db: db}).SetDefaultSearchContext(ctx, graphqlbackend.SetDefaultSearchContextArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID2})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
_, err = (&Resolver{db: db}).CreateSearchContextStar(ctx, graphqlbackend.CreateSearchContextStarArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID2})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
_, err = (&Resolver{db: db}).DeleteSearchContextStar(ctx, graphqlbackend.DeleteSearchContextStarArgs{SearchContextID: graphqlSearchContextID, UserID: graphqlUserID2})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -35566,6 +35566,10 @@ type MockSearchContextsStore struct {
|
||||
// CountSearchContextsFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method CountSearchContexts.
|
||||
CountSearchContextsFunc *SearchContextsStoreCountSearchContextsFunc
|
||||
// CreateSearchContextStarForUserFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// CreateSearchContextStarForUser.
|
||||
CreateSearchContextStarForUserFunc *SearchContextsStoreCreateSearchContextStarForUserFunc
|
||||
// CreateSearchContextWithRepositoryRevisionsFunc is an instance of a
|
||||
// mock function object controlling the behavior of the method
|
||||
// CreateSearchContextWithRepositoryRevisions.
|
||||
@ -35573,6 +35577,10 @@ type MockSearchContextsStore struct {
|
||||
// DeleteSearchContextFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method DeleteSearchContext.
|
||||
DeleteSearchContextFunc *SearchContextsStoreDeleteSearchContextFunc
|
||||
// DeleteSearchContextStarForUserFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// DeleteSearchContextStarForUser.
|
||||
DeleteSearchContextStarForUserFunc *SearchContextsStoreDeleteSearchContextStarForUserFunc
|
||||
// DoneFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Done.
|
||||
DoneFunc *SearchContextsStoreDoneFunc
|
||||
@ -35602,6 +35610,10 @@ type MockSearchContextsStore struct {
|
||||
// function object controlling the behavior of the method
|
||||
// SetSearchContextRepositoryRevisions.
|
||||
SetSearchContextRepositoryRevisionsFunc *SearchContextsStoreSetSearchContextRepositoryRevisionsFunc
|
||||
// SetUserDefaultSearchContextIDFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// SetUserDefaultSearchContextID.
|
||||
SetUserDefaultSearchContextIDFunc *SearchContextsStoreSetUserDefaultSearchContextIDFunc
|
||||
// TransactFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Transact.
|
||||
TransactFunc *SearchContextsStoreTransactFunc
|
||||
@ -35621,6 +35633,11 @@ func NewMockSearchContextsStore() *MockSearchContextsStore {
|
||||
return
|
||||
},
|
||||
},
|
||||
CreateSearchContextStarForUserFunc: &SearchContextsStoreCreateSearchContextStarForUserFunc{
|
||||
defaultHook: func(context.Context, int32, int64) (r0 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
CreateSearchContextWithRepositoryRevisionsFunc: &SearchContextsStoreCreateSearchContextWithRepositoryRevisionsFunc{
|
||||
defaultHook: func(context.Context, *types.SearchContext, []*types.SearchContextRepositoryRevisions) (r0 *types.SearchContext, r1 error) {
|
||||
return
|
||||
@ -35631,6 +35648,11 @@ func NewMockSearchContextsStore() *MockSearchContextsStore {
|
||||
return
|
||||
},
|
||||
},
|
||||
DeleteSearchContextStarForUserFunc: &SearchContextsStoreDeleteSearchContextStarForUserFunc{
|
||||
defaultHook: func(context.Context, int32, int64) (r0 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
DoneFunc: &SearchContextsStoreDoneFunc{
|
||||
defaultHook: func(error) (r0 error) {
|
||||
return
|
||||
@ -35676,6 +35698,11 @@ func NewMockSearchContextsStore() *MockSearchContextsStore {
|
||||
return
|
||||
},
|
||||
},
|
||||
SetUserDefaultSearchContextIDFunc: &SearchContextsStoreSetUserDefaultSearchContextIDFunc{
|
||||
defaultHook: func(context.Context, int32, int64) (r0 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
TransactFunc: &SearchContextsStoreTransactFunc{
|
||||
defaultHook: func(context.Context) (r0 SearchContextsStore, r1 error) {
|
||||
return
|
||||
@ -35699,6 +35726,11 @@ func NewStrictMockSearchContextsStore() *MockSearchContextsStore {
|
||||
panic("unexpected invocation of MockSearchContextsStore.CountSearchContexts")
|
||||
},
|
||||
},
|
||||
CreateSearchContextStarForUserFunc: &SearchContextsStoreCreateSearchContextStarForUserFunc{
|
||||
defaultHook: func(context.Context, int32, int64) error {
|
||||
panic("unexpected invocation of MockSearchContextsStore.CreateSearchContextStarForUser")
|
||||
},
|
||||
},
|
||||
CreateSearchContextWithRepositoryRevisionsFunc: &SearchContextsStoreCreateSearchContextWithRepositoryRevisionsFunc{
|
||||
defaultHook: func(context.Context, *types.SearchContext, []*types.SearchContextRepositoryRevisions) (*types.SearchContext, error) {
|
||||
panic("unexpected invocation of MockSearchContextsStore.CreateSearchContextWithRepositoryRevisions")
|
||||
@ -35709,6 +35741,11 @@ func NewStrictMockSearchContextsStore() *MockSearchContextsStore {
|
||||
panic("unexpected invocation of MockSearchContextsStore.DeleteSearchContext")
|
||||
},
|
||||
},
|
||||
DeleteSearchContextStarForUserFunc: &SearchContextsStoreDeleteSearchContextStarForUserFunc{
|
||||
defaultHook: func(context.Context, int32, int64) error {
|
||||
panic("unexpected invocation of MockSearchContextsStore.DeleteSearchContextStarForUser")
|
||||
},
|
||||
},
|
||||
DoneFunc: &SearchContextsStoreDoneFunc{
|
||||
defaultHook: func(error) error {
|
||||
panic("unexpected invocation of MockSearchContextsStore.Done")
|
||||
@ -35754,6 +35791,11 @@ func NewStrictMockSearchContextsStore() *MockSearchContextsStore {
|
||||
panic("unexpected invocation of MockSearchContextsStore.SetSearchContextRepositoryRevisions")
|
||||
},
|
||||
},
|
||||
SetUserDefaultSearchContextIDFunc: &SearchContextsStoreSetUserDefaultSearchContextIDFunc{
|
||||
defaultHook: func(context.Context, int32, int64) error {
|
||||
panic("unexpected invocation of MockSearchContextsStore.SetUserDefaultSearchContextID")
|
||||
},
|
||||
},
|
||||
TransactFunc: &SearchContextsStoreTransactFunc{
|
||||
defaultHook: func(context.Context) (SearchContextsStore, error) {
|
||||
panic("unexpected invocation of MockSearchContextsStore.Transact")
|
||||
@ -35775,12 +35817,18 @@ func NewMockSearchContextsStoreFrom(i SearchContextsStore) *MockSearchContextsSt
|
||||
CountSearchContextsFunc: &SearchContextsStoreCountSearchContextsFunc{
|
||||
defaultHook: i.CountSearchContexts,
|
||||
},
|
||||
CreateSearchContextStarForUserFunc: &SearchContextsStoreCreateSearchContextStarForUserFunc{
|
||||
defaultHook: i.CreateSearchContextStarForUser,
|
||||
},
|
||||
CreateSearchContextWithRepositoryRevisionsFunc: &SearchContextsStoreCreateSearchContextWithRepositoryRevisionsFunc{
|
||||
defaultHook: i.CreateSearchContextWithRepositoryRevisions,
|
||||
},
|
||||
DeleteSearchContextFunc: &SearchContextsStoreDeleteSearchContextFunc{
|
||||
defaultHook: i.DeleteSearchContext,
|
||||
},
|
||||
DeleteSearchContextStarForUserFunc: &SearchContextsStoreDeleteSearchContextStarForUserFunc{
|
||||
defaultHook: i.DeleteSearchContextStarForUser,
|
||||
},
|
||||
DoneFunc: &SearchContextsStoreDoneFunc{
|
||||
defaultHook: i.Done,
|
||||
},
|
||||
@ -35808,6 +35856,9 @@ func NewMockSearchContextsStoreFrom(i SearchContextsStore) *MockSearchContextsSt
|
||||
SetSearchContextRepositoryRevisionsFunc: &SearchContextsStoreSetSearchContextRepositoryRevisionsFunc{
|
||||
defaultHook: i.SetSearchContextRepositoryRevisions,
|
||||
},
|
||||
SetUserDefaultSearchContextIDFunc: &SearchContextsStoreSetUserDefaultSearchContextIDFunc{
|
||||
defaultHook: i.SetUserDefaultSearchContextID,
|
||||
},
|
||||
TransactFunc: &SearchContextsStoreTransactFunc{
|
||||
defaultHook: i.Transact,
|
||||
},
|
||||
@ -35929,6 +35980,118 @@ func (c SearchContextsStoreCountSearchContextsFuncCall) Results() []interface{}
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// SearchContextsStoreCreateSearchContextStarForUserFunc describes the
|
||||
// behavior when the CreateSearchContextStarForUser method of the parent
|
||||
// MockSearchContextsStore instance is invoked.
|
||||
type SearchContextsStoreCreateSearchContextStarForUserFunc struct {
|
||||
defaultHook func(context.Context, int32, int64) error
|
||||
hooks []func(context.Context, int32, int64) error
|
||||
history []SearchContextsStoreCreateSearchContextStarForUserFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// CreateSearchContextStarForUser delegates to the next hook function in the
|
||||
// queue and stores the parameter and result values of this invocation.
|
||||
func (m *MockSearchContextsStore) CreateSearchContextStarForUser(v0 context.Context, v1 int32, v2 int64) error {
|
||||
r0 := m.CreateSearchContextStarForUserFunc.nextHook()(v0, v1, v2)
|
||||
m.CreateSearchContextStarForUserFunc.appendCall(SearchContextsStoreCreateSearchContextStarForUserFuncCall{v0, v1, v2, r0})
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// CreateSearchContextStarForUser method of the parent
|
||||
// MockSearchContextsStore instance is invoked and the hook queue is empty.
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) SetDefaultHook(hook func(context.Context, int32, int64) error) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// CreateSearchContextStarForUser method of the parent
|
||||
// MockSearchContextsStore instance invokes the hook at the front of the
|
||||
// queue and discards it. After the queue is empty, the default hook
|
||||
// function is invoked for any future action.
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) PushHook(hook func(context.Context, int32, int64) error) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) SetDefaultReturn(r0 error) {
|
||||
f.SetDefaultHook(func(context.Context, int32, int64) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) PushReturn(r0 error) {
|
||||
f.PushHook(func(context.Context, int32, int64) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) nextHook() func(context.Context, int32, int64) error {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) appendCall(r0 SearchContextsStoreCreateSearchContextStarForUserFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// SearchContextsStoreCreateSearchContextStarForUserFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *SearchContextsStoreCreateSearchContextStarForUserFunc) History() []SearchContextsStoreCreateSearchContextStarForUserFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]SearchContextsStoreCreateSearchContextStarForUserFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// SearchContextsStoreCreateSearchContextStarForUserFuncCall is an object
|
||||
// that describes an invocation of method CreateSearchContextStarForUser on
|
||||
// an instance of MockSearchContextsStore.
|
||||
type SearchContextsStoreCreateSearchContextStarForUserFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 int32
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 int64
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c SearchContextsStoreCreateSearchContextStarForUserFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c SearchContextsStoreCreateSearchContextStarForUserFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// SearchContextsStoreCreateSearchContextWithRepositoryRevisionsFunc
|
||||
// describes the behavior when the
|
||||
// CreateSearchContextWithRepositoryRevisions method of the parent
|
||||
@ -36156,6 +36319,118 @@ func (c SearchContextsStoreDeleteSearchContextFuncCall) Results() []interface{}
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// SearchContextsStoreDeleteSearchContextStarForUserFunc describes the
|
||||
// behavior when the DeleteSearchContextStarForUser method of the parent
|
||||
// MockSearchContextsStore instance is invoked.
|
||||
type SearchContextsStoreDeleteSearchContextStarForUserFunc struct {
|
||||
defaultHook func(context.Context, int32, int64) error
|
||||
hooks []func(context.Context, int32, int64) error
|
||||
history []SearchContextsStoreDeleteSearchContextStarForUserFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// DeleteSearchContextStarForUser delegates to the next hook function in the
|
||||
// queue and stores the parameter and result values of this invocation.
|
||||
func (m *MockSearchContextsStore) DeleteSearchContextStarForUser(v0 context.Context, v1 int32, v2 int64) error {
|
||||
r0 := m.DeleteSearchContextStarForUserFunc.nextHook()(v0, v1, v2)
|
||||
m.DeleteSearchContextStarForUserFunc.appendCall(SearchContextsStoreDeleteSearchContextStarForUserFuncCall{v0, v1, v2, r0})
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// DeleteSearchContextStarForUser method of the parent
|
||||
// MockSearchContextsStore instance is invoked and the hook queue is empty.
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) SetDefaultHook(hook func(context.Context, int32, int64) error) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// DeleteSearchContextStarForUser method of the parent
|
||||
// MockSearchContextsStore instance invokes the hook at the front of the
|
||||
// queue and discards it. After the queue is empty, the default hook
|
||||
// function is invoked for any future action.
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) PushHook(hook func(context.Context, int32, int64) error) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) SetDefaultReturn(r0 error) {
|
||||
f.SetDefaultHook(func(context.Context, int32, int64) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) PushReturn(r0 error) {
|
||||
f.PushHook(func(context.Context, int32, int64) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) nextHook() func(context.Context, int32, int64) error {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) appendCall(r0 SearchContextsStoreDeleteSearchContextStarForUserFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// SearchContextsStoreDeleteSearchContextStarForUserFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *SearchContextsStoreDeleteSearchContextStarForUserFunc) History() []SearchContextsStoreDeleteSearchContextStarForUserFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]SearchContextsStoreDeleteSearchContextStarForUserFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// SearchContextsStoreDeleteSearchContextStarForUserFuncCall is an object
|
||||
// that describes an invocation of method DeleteSearchContextStarForUser on
|
||||
// an instance of MockSearchContextsStore.
|
||||
type SearchContextsStoreDeleteSearchContextStarForUserFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 int32
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 int64
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c SearchContextsStoreDeleteSearchContextStarForUserFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c SearchContextsStoreDeleteSearchContextStarForUserFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// SearchContextsStoreDoneFunc describes the behavior when the Done method
|
||||
// of the parent MockSearchContextsStore instance is invoked.
|
||||
type SearchContextsStoreDoneFunc struct {
|
||||
@ -37136,6 +37411,118 @@ func (c SearchContextsStoreSetSearchContextRepositoryRevisionsFuncCall) Results(
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// SearchContextsStoreSetUserDefaultSearchContextIDFunc describes the
|
||||
// behavior when the SetUserDefaultSearchContextID method of the parent
|
||||
// MockSearchContextsStore instance is invoked.
|
||||
type SearchContextsStoreSetUserDefaultSearchContextIDFunc struct {
|
||||
defaultHook func(context.Context, int32, int64) error
|
||||
hooks []func(context.Context, int32, int64) error
|
||||
history []SearchContextsStoreSetUserDefaultSearchContextIDFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// SetUserDefaultSearchContextID delegates to the next hook function in the
|
||||
// queue and stores the parameter and result values of this invocation.
|
||||
func (m *MockSearchContextsStore) SetUserDefaultSearchContextID(v0 context.Context, v1 int32, v2 int64) error {
|
||||
r0 := m.SetUserDefaultSearchContextIDFunc.nextHook()(v0, v1, v2)
|
||||
m.SetUserDefaultSearchContextIDFunc.appendCall(SearchContextsStoreSetUserDefaultSearchContextIDFuncCall{v0, v1, v2, r0})
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// SetUserDefaultSearchContextID method of the parent
|
||||
// MockSearchContextsStore instance is invoked and the hook queue is empty.
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) SetDefaultHook(hook func(context.Context, int32, int64) error) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// SetUserDefaultSearchContextID method of the parent
|
||||
// MockSearchContextsStore instance invokes the hook at the front of the
|
||||
// queue and discards it. After the queue is empty, the default hook
|
||||
// function is invoked for any future action.
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) PushHook(hook func(context.Context, int32, int64) error) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) SetDefaultReturn(r0 error) {
|
||||
f.SetDefaultHook(func(context.Context, int32, int64) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) PushReturn(r0 error) {
|
||||
f.PushHook(func(context.Context, int32, int64) error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) nextHook() func(context.Context, int32, int64) error {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) appendCall(r0 SearchContextsStoreSetUserDefaultSearchContextIDFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// SearchContextsStoreSetUserDefaultSearchContextIDFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *SearchContextsStoreSetUserDefaultSearchContextIDFunc) History() []SearchContextsStoreSetUserDefaultSearchContextIDFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]SearchContextsStoreSetUserDefaultSearchContextIDFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// SearchContextsStoreSetUserDefaultSearchContextIDFuncCall is an object
|
||||
// that describes an invocation of method SetUserDefaultSearchContextID on
|
||||
// an instance of MockSearchContextsStore.
|
||||
type SearchContextsStoreSetUserDefaultSearchContextIDFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 int32
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 int64
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c SearchContextsStoreSetUserDefaultSearchContextIDFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c SearchContextsStoreSetUserDefaultSearchContextIDFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// SearchContextsStoreTransactFunc describes the behavior when the Transact
|
||||
// method of the parent MockSearchContextsStore instance is invoked.
|
||||
type SearchContextsStoreTransactFunc struct {
|
||||
|
||||
@ -17833,6 +17833,67 @@
|
||||
],
|
||||
"Triggers": []
|
||||
},
|
||||
{
|
||||
"Name": "search_context_default",
|
||||
"Comment": "When a user sets a search context as default, a row is inserted into this table. A user can only have one default search context. If the user has not set their default search context, it will fall back to `global`.",
|
||||
"Columns": [
|
||||
{
|
||||
"Name": "search_context_id",
|
||||
"Index": 2,
|
||||
"TypeName": "bigint",
|
||||
"IsNullable": false,
|
||||
"Default": "",
|
||||
"CharacterMaximumLength": 0,
|
||||
"IsIdentity": false,
|
||||
"IdentityGeneration": "",
|
||||
"IsGenerated": "NEVER",
|
||||
"GenerationExpression": "",
|
||||
"Comment": ""
|
||||
},
|
||||
{
|
||||
"Name": "user_id",
|
||||
"Index": 1,
|
||||
"TypeName": "integer",
|
||||
"IsNullable": false,
|
||||
"Default": "",
|
||||
"CharacterMaximumLength": 0,
|
||||
"IsIdentity": false,
|
||||
"IdentityGeneration": "",
|
||||
"IsGenerated": "NEVER",
|
||||
"GenerationExpression": "",
|
||||
"Comment": ""
|
||||
}
|
||||
],
|
||||
"Indexes": [
|
||||
{
|
||||
"Name": "search_context_default_pkey",
|
||||
"IsPrimaryKey": true,
|
||||
"IsUnique": true,
|
||||
"IsExclusion": false,
|
||||
"IsDeferrable": false,
|
||||
"IndexDefinition": "CREATE UNIQUE INDEX search_context_default_pkey ON search_context_default USING btree (user_id)",
|
||||
"ConstraintType": "p",
|
||||
"ConstraintDefinition": "PRIMARY KEY (user_id)"
|
||||
}
|
||||
],
|
||||
"Constraints": [
|
||||
{
|
||||
"Name": "search_context_default_search_context_id_fkey",
|
||||
"ConstraintType": "f",
|
||||
"RefTableName": "search_contexts",
|
||||
"IsDeferrable": true,
|
||||
"ConstraintDefinition": "FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE"
|
||||
},
|
||||
{
|
||||
"Name": "search_context_default_user_id_fkey",
|
||||
"ConstraintType": "f",
|
||||
"RefTableName": "users",
|
||||
"IsDeferrable": true,
|
||||
"ConstraintDefinition": "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE"
|
||||
}
|
||||
],
|
||||
"Triggers": []
|
||||
},
|
||||
{
|
||||
"Name": "search_context_repos",
|
||||
"Comment": "",
|
||||
@ -17907,6 +17968,80 @@
|
||||
],
|
||||
"Triggers": []
|
||||
},
|
||||
{
|
||||
"Name": "search_context_stars",
|
||||
"Comment": "When a user stars a search context, a row is inserted into this table. If the user unstars the search context, the row is deleted. The global context is not in the database, and therefore cannot be starred.",
|
||||
"Columns": [
|
||||
{
|
||||
"Name": "created_at",
|
||||
"Index": 3,
|
||||
"TypeName": "timestamp with time zone",
|
||||
"IsNullable": false,
|
||||
"Default": "now()",
|
||||
"CharacterMaximumLength": 0,
|
||||
"IsIdentity": false,
|
||||
"IdentityGeneration": "",
|
||||
"IsGenerated": "NEVER",
|
||||
"GenerationExpression": "",
|
||||
"Comment": ""
|
||||
},
|
||||
{
|
||||
"Name": "search_context_id",
|
||||
"Index": 1,
|
||||
"TypeName": "bigint",
|
||||
"IsNullable": false,
|
||||
"Default": "",
|
||||
"CharacterMaximumLength": 0,
|
||||
"IsIdentity": false,
|
||||
"IdentityGeneration": "",
|
||||
"IsGenerated": "NEVER",
|
||||
"GenerationExpression": "",
|
||||
"Comment": ""
|
||||
},
|
||||
{
|
||||
"Name": "user_id",
|
||||
"Index": 2,
|
||||
"TypeName": "integer",
|
||||
"IsNullable": false,
|
||||
"Default": "",
|
||||
"CharacterMaximumLength": 0,
|
||||
"IsIdentity": false,
|
||||
"IdentityGeneration": "",
|
||||
"IsGenerated": "NEVER",
|
||||
"GenerationExpression": "",
|
||||
"Comment": ""
|
||||
}
|
||||
],
|
||||
"Indexes": [
|
||||
{
|
||||
"Name": "search_context_stars_pkey",
|
||||
"IsPrimaryKey": true,
|
||||
"IsUnique": true,
|
||||
"IsExclusion": false,
|
||||
"IsDeferrable": false,
|
||||
"IndexDefinition": "CREATE UNIQUE INDEX search_context_stars_pkey ON search_context_stars USING btree (search_context_id, user_id)",
|
||||
"ConstraintType": "p",
|
||||
"ConstraintDefinition": "PRIMARY KEY (search_context_id, user_id)"
|
||||
}
|
||||
],
|
||||
"Constraints": [
|
||||
{
|
||||
"Name": "search_context_stars_search_context_id_fkey",
|
||||
"ConstraintType": "f",
|
||||
"RefTableName": "search_contexts",
|
||||
"IsDeferrable": true,
|
||||
"ConstraintDefinition": "FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE"
|
||||
},
|
||||
{
|
||||
"Name": "search_context_stars_user_id_fkey",
|
||||
"ConstraintType": "f",
|
||||
"RefTableName": "users",
|
||||
"IsDeferrable": true,
|
||||
"ConstraintDefinition": "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE"
|
||||
}
|
||||
],
|
||||
"Triggers": []
|
||||
},
|
||||
{
|
||||
"Name": "search_contexts",
|
||||
"Comment": "",
|
||||
|
||||
@ -2780,6 +2780,22 @@ Foreign-key constraints:
|
||||
|
||||
```
|
||||
|
||||
# Table "public.search_context_default"
|
||||
```
|
||||
Column | Type | Collation | Nullable | Default
|
||||
-------------------+---------+-----------+----------+---------
|
||||
user_id | integer | | not null |
|
||||
search_context_id | bigint | | not null |
|
||||
Indexes:
|
||||
"search_context_default_pkey" PRIMARY KEY, btree (user_id)
|
||||
Foreign-key constraints:
|
||||
"search_context_default_search_context_id_fkey" FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE
|
||||
"search_context_default_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE
|
||||
|
||||
```
|
||||
|
||||
When a user sets a search context as default, a row is inserted into this table. A user can only have one default search context. If the user has not set their default search context, it will fall back to `global`.
|
||||
|
||||
# Table "public.search_context_repos"
|
||||
```
|
||||
Column | Type | Collation | Nullable | Default
|
||||
@ -2795,6 +2811,23 @@ Foreign-key constraints:
|
||||
|
||||
```
|
||||
|
||||
# Table "public.search_context_stars"
|
||||
```
|
||||
Column | Type | Collation | Nullable | Default
|
||||
-------------------+--------------------------+-----------+----------+---------
|
||||
search_context_id | bigint | | not null |
|
||||
user_id | integer | | not null |
|
||||
created_at | timestamp with time zone | | not null | now()
|
||||
Indexes:
|
||||
"search_context_stars_pkey" PRIMARY KEY, btree (search_context_id, user_id)
|
||||
Foreign-key constraints:
|
||||
"search_context_stars_search_context_id_fkey" FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE
|
||||
"search_context_stars_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE
|
||||
|
||||
```
|
||||
|
||||
When a user stars a search context, a row is inserted into this table. If the user unstars the search context, the row is deleted. The global context is not in the database, and therefore cannot be starred.
|
||||
|
||||
# Table "public.search_contexts"
|
||||
```
|
||||
Column | Type | Collation | Nullable | Default
|
||||
@ -2821,7 +2854,9 @@ Foreign-key constraints:
|
||||
"search_contexts_namespace_org_id_fk" FOREIGN KEY (namespace_org_id) REFERENCES orgs(id) ON DELETE CASCADE
|
||||
"search_contexts_namespace_user_id_fk" FOREIGN KEY (namespace_user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
Referenced by:
|
||||
TABLE "search_context_default" CONSTRAINT "search_context_default_search_context_id_fkey" FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE
|
||||
TABLE "search_context_repos" CONSTRAINT "search_context_repos_search_context_id_fk" FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE
|
||||
TABLE "search_context_stars" CONSTRAINT "search_context_stars_search_context_id_fkey" FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE
|
||||
|
||||
```
|
||||
|
||||
@ -3153,6 +3188,8 @@ Referenced by:
|
||||
TABLE "registry_extension_releases" CONSTRAINT "registry_extension_releases_creator_user_id_fkey" FOREIGN KEY (creator_user_id) REFERENCES users(id)
|
||||
TABLE "registry_extensions" CONSTRAINT "registry_extensions_publisher_user_id_fkey" FOREIGN KEY (publisher_user_id) REFERENCES users(id)
|
||||
TABLE "saved_searches" CONSTRAINT "saved_searches_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
TABLE "search_context_default" CONSTRAINT "search_context_default_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE
|
||||
TABLE "search_context_stars" CONSTRAINT "search_context_stars_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE
|
||||
TABLE "search_contexts" CONSTRAINT "search_contexts_namespace_user_id_fk" FOREIGN KEY (namespace_user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
TABLE "settings" CONSTRAINT "settings_author_user_id_fkey" FOREIGN KEY (author_user_id) REFERENCES users(id) ON DELETE RESTRICT
|
||||
TABLE "settings" CONSTRAINT "settings_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT
|
||||
|
||||
@ -40,6 +40,9 @@ type SearchContextsStore interface {
|
||||
SetSearchContextRepositoryRevisions(context.Context, int64, []*types.SearchContextRepositoryRevisions) error
|
||||
Transact(context.Context) (SearchContextsStore, error)
|
||||
UpdateSearchContextWithRepositoryRevisions(context.Context, *types.SearchContext, []*types.SearchContextRepositoryRevisions) (*types.SearchContext, error)
|
||||
SetUserDefaultSearchContextID(ctx context.Context, userID int32, searchContextID int64) error
|
||||
CreateSearchContextStarForUser(ctx context.Context, userID int32, searchContextID int64) error
|
||||
DeleteSearchContextStarForUser(ctx context.Context, userID int32, searchContextID int64) error
|
||||
}
|
||||
|
||||
type searchContextsStore struct {
|
||||
@ -59,61 +62,102 @@ const searchContextsPermissionsConditionFmtStr = `(
|
||||
-- Bypass permission check
|
||||
%s
|
||||
-- Happy path of public search contexts
|
||||
OR sc.public
|
||||
OR public
|
||||
-- Private user contexts are available only to its creator
|
||||
OR (sc.namespace_user_id IS NOT NULL AND sc.namespace_user_id = %d)
|
||||
OR (namespace_user_id IS NOT NULL AND namespace_user_id = %d)
|
||||
-- Private org contexts are available only to its members
|
||||
OR (sc.namespace_org_id IS NOT NULL AND EXISTS (SELECT FROM org_members om WHERE om.org_id = sc.namespace_org_id AND om.user_id = %d))
|
||||
OR (namespace_org_id IS NOT NULL AND EXISTS (SELECT FROM org_members om WHERE om.org_id = namespace_org_id AND om.user_id = %d))
|
||||
-- Private instance-level contexts are available only to site-admins
|
||||
OR (sc.namespace_user_id IS NULL AND sc.namespace_org_id IS NULL AND EXISTS (SELECT FROM users u WHERE u.id = %d AND u.site_admin))
|
||||
OR (namespace_user_id IS NULL AND namespace_org_id IS NULL AND EXISTS (SELECT FROM users u WHERE u.id = %d AND u.site_admin))
|
||||
)`
|
||||
|
||||
func searchContextsPermissionsCondition(ctx context.Context, logger log.Logger, store basestore.ShareableStore) (*sqlf.Query, error) {
|
||||
func searchContextsPermissionsCondition(ctx context.Context) *sqlf.Query {
|
||||
a := actor.FromContext(ctx)
|
||||
authenticatedUserID := int32(0)
|
||||
authenticatedUserID := a.UID
|
||||
bypassPermissionsCheck := a.Internal
|
||||
if !bypassPermissionsCheck && a.IsAuthenticated() {
|
||||
currentUser, err := UsersWith(logger, store).GetByCurrentAuthUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authenticatedUserID = currentUser.ID
|
||||
}
|
||||
q := sqlf.Sprintf(searchContextsPermissionsConditionFmtStr, bypassPermissionsCheck, authenticatedUserID, authenticatedUserID, authenticatedUserID)
|
||||
return q, nil
|
||||
return q
|
||||
}
|
||||
|
||||
const searchContextQueryFmtStr = `
|
||||
SELECT -- The global context is not in the database, it needs to be added here for the sake of pagination.
|
||||
0 as id, -- All other contexts have a non-zero ID.
|
||||
'global' as context_name,
|
||||
'All repositories on Sourcegraph' as description,
|
||||
true as public,
|
||||
true as autodefined,
|
||||
NULL as namespace_user_id,
|
||||
NULL as namespace_org_id,
|
||||
TIMESTAMP WITH TIME ZONE 'epoch' as updated_at, -- Timestamp is not used for global context, but we need to return something.
|
||||
NULL as query,
|
||||
NULL as namespace_name,
|
||||
NULL as namespace_username,
|
||||
NULL as namespace_org_name,
|
||||
NOT EXISTS (SELECT FROM search_context_default scd WHERE scd.user_id = %d) as user_default, -- Global context is the default if there is no default set.
|
||||
false as user_starred -- Global context cannot be starred.
|
||||
UNION ALL
|
||||
SELECT
|
||||
sc.id as id,
|
||||
sc.name as context_name,
|
||||
sc.description as description,
|
||||
sc.public as public,
|
||||
false as autodefined, -- Context in the database are never autodefined.
|
||||
sc.namespace_user_id as namespace_user_id,
|
||||
sc.namespace_org_id as namespace_org_id,
|
||||
sc.updated_at as updated_at,
|
||||
sc.query as query,
|
||||
COALESCE(u.username, o.name) as namespace_name,
|
||||
u.username as namespace_username,
|
||||
o.name as namespace_org_name,
|
||||
scd.search_context_id IS NOT NULL as user_default,
|
||||
scs.search_context_id IS NOT NULL as user_starred
|
||||
FROM search_contexts sc
|
||||
LEFT JOIN users u on sc.namespace_user_id = u.id
|
||||
LEFT JOIN orgs o on sc.namespace_org_id = o.id
|
||||
LEFT JOIN search_context_stars scs
|
||||
ON scs.user_id = %d AND scs.search_context_id = sc.id
|
||||
LEFT JOIN search_context_default scd
|
||||
ON scd.user_id = %d AND scd.search_context_id = sc.id
|
||||
`
|
||||
|
||||
const listSearchContextsFmtStr = `
|
||||
SELECT
|
||||
sc.id,
|
||||
sc.name,
|
||||
sc.description,
|
||||
sc.public,
|
||||
sc.namespace_user_id,
|
||||
sc.namespace_org_id,
|
||||
sc.updated_at,
|
||||
sc.query,
|
||||
u.username,
|
||||
o.name
|
||||
FROM search_contexts sc
|
||||
LEFT JOIN users u on sc.namespace_user_id = u.id
|
||||
LEFT JOIN orgs o on sc.namespace_org_id = o.id
|
||||
id,
|
||||
context_name,
|
||||
description,
|
||||
public,
|
||||
autodefined,
|
||||
namespace_user_id,
|
||||
namespace_org_id,
|
||||
updated_at,
|
||||
query,
|
||||
namespace_username,
|
||||
namespace_org_name,
|
||||
user_default,
|
||||
user_starred
|
||||
FROM (
|
||||
` + searchContextQueryFmtStr + `
|
||||
) AS t
|
||||
WHERE
|
||||
(%s) -- permission conditions
|
||||
AND (%s) -- query conditions
|
||||
ORDER BY %s
|
||||
ORDER BY
|
||||
autodefined DESC, -- Always show global context first
|
||||
user_default DESC,
|
||||
user_starred DESC,
|
||||
%s
|
||||
LIMIT %d
|
||||
OFFSET %d
|
||||
`
|
||||
|
||||
const countSearchContextsFmtStr = `
|
||||
SELECT COUNT(*)
|
||||
FROM search_contexts sc
|
||||
LEFT JOIN users u on sc.namespace_user_id = u.id
|
||||
LEFT JOIN orgs o on sc.namespace_org_id = o.id
|
||||
FROM (
|
||||
` + searchContextQueryFmtStr + `
|
||||
) AS t
|
||||
WHERE
|
||||
(%s) -- permission conditions
|
||||
AND (%s) -- query conditions
|
||||
(%s) -- permission conditions
|
||||
AND (%s) -- query conditions
|
||||
`
|
||||
|
||||
type SearchContextsOrderByOption uint8
|
||||
@ -159,11 +203,11 @@ func getSearchContextOrderByClause(orderBy SearchContextsOrderByOption, descendi
|
||||
}
|
||||
switch orderBy {
|
||||
case SearchContextsOrderBySpec:
|
||||
return sqlf.Sprintf(fmt.Sprintf("COALESCE(u.username, o.name) %s, sc.name %s", orderDirection, orderDirection))
|
||||
return sqlf.Sprintf(fmt.Sprintf("namespace_name %s, context_name %s", orderDirection, orderDirection))
|
||||
case SearchContextsOrderByUpdatedAt:
|
||||
return sqlf.Sprintf("sc.updated_at " + orderDirection)
|
||||
return sqlf.Sprintf("updated_at " + orderDirection)
|
||||
case SearchContextsOrderByID:
|
||||
return sqlf.Sprintf("sc.id " + orderDirection)
|
||||
return sqlf.Sprintf("id " + orderDirection)
|
||||
}
|
||||
panic("invalid SearchContextsOrderByOption option")
|
||||
}
|
||||
@ -174,10 +218,10 @@ func getSearchContextNamespaceQueryConditions(namespaceUserID, namespaceOrgID in
|
||||
return nil, errors.New("options NamespaceUserID and NamespaceOrgID are mutually exclusive")
|
||||
}
|
||||
if namespaceUserID > 0 {
|
||||
conds = append(conds, sqlf.Sprintf("sc.namespace_user_id = %s", namespaceUserID))
|
||||
conds = append(conds, sqlf.Sprintf("namespace_user_id = %s", namespaceUserID))
|
||||
}
|
||||
if namespaceOrgID > 0 {
|
||||
conds = append(conds, sqlf.Sprintf("sc.namespace_org_id = %s", namespaceOrgID))
|
||||
conds = append(conds, sqlf.Sprintf("namespace_org_id = %s", namespaceOrgID))
|
||||
}
|
||||
return conds, nil
|
||||
}
|
||||
@ -193,13 +237,13 @@ func idsToQueries(ids []int32) []*sqlf.Query {
|
||||
func getSearchContextsQueryConditions(opts ListSearchContextsOptions) []*sqlf.Query {
|
||||
namespaceConds := []*sqlf.Query{}
|
||||
if opts.NoNamespace {
|
||||
namespaceConds = append(namespaceConds, sqlf.Sprintf("(sc.namespace_user_id IS NULL AND sc.namespace_org_id IS NULL)"))
|
||||
namespaceConds = append(namespaceConds, sqlf.Sprintf("(namespace_user_id IS NULL AND namespace_org_id IS NULL)"))
|
||||
}
|
||||
if len(opts.NamespaceUserIDs) > 0 {
|
||||
namespaceConds = append(namespaceConds, sqlf.Sprintf("sc.namespace_user_id IN (%s)", sqlf.Join(idsToQueries(opts.NamespaceUserIDs), ",")))
|
||||
namespaceConds = append(namespaceConds, sqlf.Sprintf("namespace_user_id IN (%s)", sqlf.Join(idsToQueries(opts.NamespaceUserIDs), ",")))
|
||||
}
|
||||
if len(opts.NamespaceOrgIDs) > 0 {
|
||||
namespaceConds = append(namespaceConds, sqlf.Sprintf("sc.namespace_org_id IN (%s)", sqlf.Join(idsToQueries(opts.NamespaceOrgIDs), ",")))
|
||||
namespaceConds = append(namespaceConds, sqlf.Sprintf("namespace_org_id IN (%s)", sqlf.Join(idsToQueries(opts.NamespaceOrgIDs), ",")))
|
||||
}
|
||||
|
||||
conds := []*sqlf.Query{}
|
||||
@ -209,11 +253,11 @@ func getSearchContextsQueryConditions(opts ListSearchContextsOptions) []*sqlf.Qu
|
||||
|
||||
if opts.Name != "" {
|
||||
// name column has type citext which automatically performs case-insensitive comparison
|
||||
conds = append(conds, sqlf.Sprintf("sc.name LIKE %s", "%"+opts.Name+"%"))
|
||||
conds = append(conds, sqlf.Sprintf("context_name LIKE %s", "%"+opts.Name+"%"))
|
||||
}
|
||||
|
||||
if opts.NamespaceName != "" {
|
||||
conds = append(conds, sqlf.Sprintf("COALESCE(u.username, o.name, '') ILIKE %s", "%"+opts.NamespaceName+"%"))
|
||||
conds = append(conds, sqlf.Sprintf("COALESCE(namespace_username, namespace_org_name, '') ILIKE %s", "%"+opts.NamespaceName+"%"))
|
||||
}
|
||||
|
||||
if len(conds) == 0 {
|
||||
@ -225,11 +269,11 @@ func getSearchContextsQueryConditions(opts ListSearchContextsOptions) []*sqlf.Qu
|
||||
}
|
||||
|
||||
func (s *searchContextsStore) listSearchContexts(ctx context.Context, cond *sqlf.Query, orderBy *sqlf.Query, limit int32, offset int32) ([]*types.SearchContext, error) {
|
||||
permissionsCond, err := searchContextsPermissionsCondition(ctx, s.logger, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.Query(ctx, sqlf.Sprintf(listSearchContextsFmtStr, permissionsCond, cond, orderBy, limit, offset))
|
||||
permissionsCond := searchContextsPermissionsCondition(ctx)
|
||||
authenticatedUserId := actor.FromContext(ctx).UID
|
||||
|
||||
query := sqlf.Sprintf(listSearchContextsFmtStr, authenticatedUserId, authenticatedUserId, authenticatedUserId, permissionsCond, cond, orderBy, limit, offset)
|
||||
rows, err := s.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -245,12 +289,12 @@ func (s *searchContextsStore) ListSearchContexts(ctx context.Context, pageOpts L
|
||||
|
||||
func (s *searchContextsStore) CountSearchContexts(ctx context.Context, opts ListSearchContextsOptions) (int32, error) {
|
||||
conds := getSearchContextsQueryConditions(opts)
|
||||
permissionsCond, err := searchContextsPermissionsCondition(ctx, s.logger, s)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
permissionsCond := searchContextsPermissionsCondition(ctx)
|
||||
authenticatedUserId := actor.FromContext(ctx).UID
|
||||
|
||||
var count int32
|
||||
err = s.QueryRow(ctx, sqlf.Sprintf(countSearchContextsFmtStr, permissionsCond, sqlf.Join(conds, "\n AND "))).Scan(&count)
|
||||
query := sqlf.Sprintf(countSearchContextsFmtStr, authenticatedUserId, authenticatedUserId, authenticatedUserId, permissionsCond, sqlf.Join(conds, "\n AND "))
|
||||
err := s.QueryRow(ctx, query).Scan(&count)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
@ -266,7 +310,7 @@ type GetSearchContextOptions struct {
|
||||
func (s *searchContextsStore) GetSearchContext(ctx context.Context, opts GetSearchContextOptions) (*types.SearchContext, error) {
|
||||
conds := []*sqlf.Query{}
|
||||
if opts.NamespaceUserID == 0 && opts.NamespaceOrgID == 0 {
|
||||
conds = append(conds, sqlf.Sprintf("sc.namespace_user_id IS NULL"), sqlf.Sprintf("sc.namespace_org_id IS NULL"))
|
||||
conds = append(conds, sqlf.Sprintf("namespace_user_id IS NULL"), sqlf.Sprintf("namespace_org_id IS NULL"))
|
||||
} else {
|
||||
namespaceConds, err := getSearchContextNamespaceQueryConditions(opts.NamespaceUserID, opts.NamespaceOrgID)
|
||||
if err != nil {
|
||||
@ -274,16 +318,17 @@ func (s *searchContextsStore) GetSearchContext(ctx context.Context, opts GetSear
|
||||
}
|
||||
conds = append(conds, namespaceConds...)
|
||||
}
|
||||
conds = append(conds, sqlf.Sprintf("sc.name = %s", opts.Name))
|
||||
conds = append(conds, sqlf.Sprintf("context_name = %s", opts.Name))
|
||||
|
||||
permissionsCond, err := searchContextsPermissionsCondition(ctx, s.logger, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
permissionsCond := searchContextsPermissionsCondition(ctx)
|
||||
authenticatedUserId := actor.FromContext(ctx).UID
|
||||
rows, err := s.Query(
|
||||
ctx,
|
||||
sqlf.Sprintf(
|
||||
listSearchContextsFmtStr,
|
||||
authenticatedUserId,
|
||||
authenticatedUserId,
|
||||
authenticatedUserId,
|
||||
permissionsCond,
|
||||
sqlf.Join(conds, "\n AND "),
|
||||
getSearchContextOrderByClause(SearchContextsOrderByID, false),
|
||||
@ -457,12 +502,15 @@ func scanSearchContexts(rows *sql.Rows) ([]*types.SearchContext, error) {
|
||||
&sc.Name,
|
||||
&sc.Description,
|
||||
&sc.Public,
|
||||
&sc.Autodefined,
|
||||
&dbutil.NullInt32{N: &sc.NamespaceUserID},
|
||||
&dbutil.NullInt32{N: &sc.NamespaceOrgID},
|
||||
&sc.UpdatedAt,
|
||||
&dbutil.NullString{S: &sc.Query},
|
||||
&dbutil.NullString{S: &sc.NamespaceUserName},
|
||||
&dbutil.NullString{S: &sc.NamespaceOrgName},
|
||||
&sc.Default,
|
||||
&sc.Starred,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -603,3 +651,39 @@ func (s *searchContextsStore) GetAllQueries(ctx context.Context) (qs []string, _
|
||||
|
||||
return qs, s.QueryRow(ctx, q).Scan(pq.Array(&qs))
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: The caller must ensure that the actor is the user setting the context as their default.
|
||||
func (s *searchContextsStore) SetUserDefaultSearchContextID(ctx context.Context, userID int32, searchContextID int64) error {
|
||||
if searchContextID == 0 {
|
||||
// If the search context ID is 0, we want to delete the default search context for the user.
|
||||
// This will cause the user to use the global search context as their default.
|
||||
return s.Exec(ctx, sqlf.Sprintf("DELETE FROM search_context_default WHERE user_id = %d", userID))
|
||||
}
|
||||
|
||||
q := sqlf.Sprintf(
|
||||
`INSERT INTO search_context_default (user_id, search_context_id)
|
||||
VALUES (%d, %d)
|
||||
ON CONFLICT (user_id) DO
|
||||
UPDATE SET search_context_id=EXCLUDED.search_context_id`,
|
||||
userID,
|
||||
searchContextID)
|
||||
return s.Exec(ctx, q)
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: The caller must ensure that the actor is the user creating the star for themselves.
|
||||
func (s *searchContextsStore) CreateSearchContextStarForUser(ctx context.Context, userID int32, searchContextID int64) error {
|
||||
q := sqlf.Sprintf(
|
||||
`INSERT INTO search_context_stars (user_id, search_context_id)
|
||||
VALUES (%d, %d)
|
||||
ON CONFLICT DO NOTHING`, userID, searchContextID)
|
||||
return s.Exec(ctx, q)
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: The caller must ensure that the actor is the user deleting the star for themselves.
|
||||
func (s *searchContextsStore) DeleteSearchContextStarForUser(ctx context.Context, userID int32, searchContextID int64) error {
|
||||
q := sqlf.Sprintf(
|
||||
`DELETE FROM search_context_stars
|
||||
WHERE user_id = %d AND search_context_id = %d`,
|
||||
userID, searchContextID)
|
||||
return s.Exec(ctx, q)
|
||||
}
|
||||
|
||||
@ -179,14 +179,14 @@ func TestSearchContexts_List(t *testing.T) {
|
||||
wantInstanceLevelSearchContexts := createdSearchContexts[:1]
|
||||
gotInstanceLevelSearchContexts, err := sc.ListSearchContexts(
|
||||
ctx,
|
||||
ListSearchContextsPageOptions{First: 1},
|
||||
ListSearchContextsPageOptions{First: 2},
|
||||
ListSearchContextsOptions{NoNamespace: true},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(wantInstanceLevelSearchContexts, gotInstanceLevelSearchContexts) {
|
||||
t.Fatalf("wanted %v search contexts, got %v", wantInstanceLevelSearchContexts, gotInstanceLevelSearchContexts)
|
||||
if !reflect.DeepEqual(wantInstanceLevelSearchContexts, gotInstanceLevelSearchContexts[1:]) { // Ignore the first result since it's the global search context
|
||||
t.Fatalf("wanted %#v search contexts, got %#v", wantInstanceLevelSearchContexts, &gotInstanceLevelSearchContexts)
|
||||
}
|
||||
|
||||
wantUserSearchContexts := createdSearchContexts[1:]
|
||||
@ -520,7 +520,7 @@ func TestSearchContexts_Permissions(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.wantSearchContexts, gotSearchContexts) {
|
||||
if !reflect.DeepEqual(tt.wantSearchContexts, gotSearchContexts[1:]) { // Ignore the first result since it's the global search context
|
||||
t.Fatalf("wanted %v search contexts, got %v", tt.wantSearchContexts, gotSearchContexts)
|
||||
}
|
||||
|
||||
@ -665,6 +665,56 @@ func TestSearchContexts_Delete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContexts_Count(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := NewDB(logger, dbtest.NewDB(logger, t))
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
sc := db.SearchContexts()
|
||||
|
||||
// With no contexts added yet, count should be 1 (the global context only)
|
||||
count, err := sc.CountSearchContexts(ctx, ListSearchContextsOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected count to be 1, got %d", count)
|
||||
}
|
||||
|
||||
_, err = createSearchContexts(ctx, sc, []*types.SearchContext{
|
||||
{Name: "ctx", Public: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
// With one context added, count should be 2
|
||||
count, err = sc.CountSearchContexts(ctx, ListSearchContextsOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected count to be 2, got %d", count)
|
||||
}
|
||||
|
||||
// Filtering by name should return 1
|
||||
count, err = sc.CountSearchContexts(ctx, ListSearchContextsOptions{Name: "ctx"})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected count to be 1, got %d", count)
|
||||
}
|
||||
|
||||
count, err = sc.CountSearchContexts(ctx, ListSearchContextsOptions{Name: "glob"})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected count to be 1, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func reverseSearchContextsSlice(s []*types.SearchContext) []*types.SearchContext {
|
||||
copySlice := make([]*types.SearchContext, len(s))
|
||||
copy(copySlice, s)
|
||||
@ -783,18 +833,106 @@ func TestSearchContexts_OrderBy(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotSearchContexts, err := sc.ListSearchContexts(internalCtx, ListSearchContextsPageOptions{First: 6}, ListSearchContextsOptions{OrderBy: tt.orderBy, OrderByDescending: tt.descending})
|
||||
gotSearchContexts, err := sc.ListSearchContexts(internalCtx, ListSearchContextsPageOptions{First: 7}, ListSearchContextsOptions{OrderBy: tt.orderBy, OrderByDescending: tt.descending})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotSearchContextNames := getSearchContextNames(gotSearchContexts)
|
||||
if !reflect.DeepEqual(tt.wantSearchContextNames, gotSearchContextNames) {
|
||||
t.Fatalf("wanted %+v search contexts, got %+v", tt.wantSearchContextNames, gotSearchContextNames)
|
||||
wantSearchContextNames := []string{"global"}
|
||||
wantSearchContextNames = append(wantSearchContextNames, tt.wantSearchContextNames...)
|
||||
if !reflect.DeepEqual(wantSearchContextNames, gotSearchContextNames) {
|
||||
t.Fatalf("wanted %+v search contexts, got %+v", wantSearchContextNames, gotSearchContextNames)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContexts_OrderByWithDefaultAndStarred(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := NewDB(logger, dbtest.NewDB(logger, t))
|
||||
t.Parallel()
|
||||
internalCtx := actor.WithInternalActor(context.Background())
|
||||
u := db.Users()
|
||||
o := db.Orgs()
|
||||
om := db.OrgMembers()
|
||||
sc := db.SearchContexts()
|
||||
|
||||
user1, err := u.Create(internalCtx, NewUser{Username: "u1", Password: "p"})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
err = u.SetIsSiteAdmin(internalCtx, user1.ID, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
displayName := "My Org"
|
||||
org, err := o.Create(internalCtx, "myorg", &displayName)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
_, err = om.Create(internalCtx, org.ID, user1.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
searchContexts, err := createSearchContexts(internalCtx, sc, []*types.SearchContext{
|
||||
{Name: "A-instance-level", Public: true}, // Starred, returned 3rd
|
||||
{Name: "B-instance-level", Public: false}, // Not returned, not public and not owned but this user or their org
|
||||
{Name: "A-user-level", Public: true, NamespaceUserID: user1.ID}, // Default, returned 1st
|
||||
{Name: "B-user-level", Public: false, NamespaceUserID: user1.ID}, // Starred, returned 2nd
|
||||
{Name: "A-org-level", Public: true, NamespaceOrgID: org.ID}, // Returned 4th
|
||||
{Name: "B-org-level", Public: false, NamespaceOrgID: org.ID}, // Returned 5th
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantedSearchContexts := []*types.SearchContext{searchContexts[2], searchContexts[3], searchContexts[0], searchContexts[4], searchContexts[5]}
|
||||
|
||||
_, err = sc.UpdateSearchContextWithRepositoryRevisions(internalCtx, searchContexts[1], nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = sc.UpdateSearchContextWithRepositoryRevisions(internalCtx, searchContexts[3], nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = sc.UpdateSearchContextWithRepositoryRevisions(internalCtx, searchContexts[5], nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set user1 has a default search context of searchContexts[2]
|
||||
err = sc.SetUserDefaultSearchContextID(internalCtx, user1.ID, searchContexts[2].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set user1 as a star for searchContexts[0] and searchContexts[3]
|
||||
err = sc.CreateSearchContextStarForUser(internalCtx, user1.ID, searchContexts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = sc.CreateSearchContextStarForUser(internalCtx, user1.ID, searchContexts[3].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Use a different user to list the search contexts so that we can test that user's starred and default search contexts
|
||||
ctx := actor.WithActor(internalCtx, actor.FromUser(user1.ID))
|
||||
gotSearchContexts, err := sc.ListSearchContexts(ctx, ListSearchContextsPageOptions{First: 7}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotSearchContextNames := getSearchContextNames(gotSearchContexts)
|
||||
wantSearchContextNames := append([]string{"global"}, getSearchContextNames(wantedSearchContexts)...)
|
||||
if !reflect.DeepEqual(wantSearchContextNames, gotSearchContextNames) {
|
||||
t.Fatalf("wanted %+v search contexts, got %+v", wantSearchContextNames, gotSearchContextNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContexts_GetAllRevisionsForRepos(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := NewDB(logger, dbtest.NewDB(logger, t))
|
||||
@ -869,3 +1007,234 @@ func TestSearchContexts_GetAllRevisionsForRepos(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultContext(searchContexts []*types.SearchContext) *types.SearchContext {
|
||||
for _, c := range searchContexts {
|
||||
if c.Default {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSearchContexts_DefaultContexts(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := NewDB(logger, dbtest.NewDB(logger, t))
|
||||
t.Parallel()
|
||||
internalCtx := actor.WithInternalActor(context.Background())
|
||||
u := db.Users()
|
||||
sc := db.SearchContexts()
|
||||
|
||||
user1, err := u.Create(internalCtx, NewUser{Username: "u1", Password: "p"})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
err = u.SetIsSiteAdmin(internalCtx, user1.ID, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
// Use a different user to list the search contexts so that we can test that user's default search contexts
|
||||
userCtx := actor.WithActor(internalCtx, actor.FromUser(user1.ID))
|
||||
|
||||
searchContexts, err := createSearchContexts(userCtx, sc, []*types.SearchContext{
|
||||
{Name: "A-user-level", Public: true, NamespaceUserID: user1.ID},
|
||||
{Name: "B-user-level", Public: false, NamespaceUserID: user1.ID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Global context should be the default
|
||||
gotSearchContexts, err := sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultContext := getDefaultContext(gotSearchContexts)
|
||||
if defaultContext == nil || defaultContext.Name != "global" {
|
||||
t.Fatalf("Expected global context to be the default, got %+v", defaultContext)
|
||||
}
|
||||
|
||||
// Set user1 has a default search context of searchContexts[1]
|
||||
err = sc.SetUserDefaultSearchContextID(userCtx, user1.ID, searchContexts[1].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// B-user-level context should be the default
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultContext = getDefaultContext(gotSearchContexts)
|
||||
if defaultContext == nil || defaultContext.Name != "B-user-level" {
|
||||
t.Fatalf("Expected B-user-level context to be the default, got %+v", defaultContext)
|
||||
}
|
||||
|
||||
// Set user1 has a default search context of searchContexts[0]
|
||||
err = sc.SetUserDefaultSearchContextID(userCtx, user1.ID, searchContexts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A-user-level context should be the default
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultContext = getDefaultContext(gotSearchContexts)
|
||||
if defaultContext == nil || defaultContext.Name != "A-user-level" {
|
||||
t.Fatalf("Expected A-user-level context to be the default, got %+v", defaultContext)
|
||||
}
|
||||
|
||||
// Set user1 default context back to global
|
||||
err = sc.SetUserDefaultSearchContextID(userCtx, user1.ID, gotSearchContexts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Global context should be the default again
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultContext = getDefaultContext(gotSearchContexts)
|
||||
if defaultContext == nil || defaultContext.Name != "global" {
|
||||
t.Fatalf("Expected global context to be the default, got %+v", defaultContext)
|
||||
}
|
||||
}
|
||||
|
||||
func getStarredContexts(searchContexts []*types.SearchContext) []*types.SearchContext {
|
||||
var starredContexts []*types.SearchContext
|
||||
for _, c := range searchContexts {
|
||||
if c.Starred {
|
||||
starredContexts = append(starredContexts, c)
|
||||
}
|
||||
}
|
||||
return starredContexts
|
||||
}
|
||||
|
||||
func TestSearchContexts_StarringContexts(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := NewDB(logger, dbtest.NewDB(logger, t))
|
||||
t.Parallel()
|
||||
internalCtx := actor.WithInternalActor(context.Background())
|
||||
u := db.Users()
|
||||
sc := db.SearchContexts()
|
||||
|
||||
user1, err := u.Create(internalCtx, NewUser{Username: "u1", Password: "p"})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
err = u.SetIsSiteAdmin(internalCtx, user1.ID, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
// Use a different user to list the search contexts so that we can test that user's starred search contexts
|
||||
userCtx := actor.WithActor(internalCtx, actor.FromUser(user1.ID))
|
||||
|
||||
searchContexts, err := createSearchContexts(userCtx, sc, []*types.SearchContext{
|
||||
{Name: "A-user-level", Public: true, NamespaceUserID: user1.ID},
|
||||
{Name: "B-user-level", Public: false, NamespaceUserID: user1.ID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create star for searchContexts[1]
|
||||
err = sc.CreateSearchContextStarForUser(userCtx, user1.ID, searchContexts[1].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// B-user-level context should be starred
|
||||
gotSearchContexts, err := sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starredContexts := getStarredContexts(gotSearchContexts)
|
||||
if len(starredContexts) != 1 || starredContexts[0].Name != "B-user-level" {
|
||||
t.Fatalf("Expected B-user-level context to be starred, got %+v", starredContexts)
|
||||
}
|
||||
|
||||
// Try to star searchContexts[0] again, should be a no-op
|
||||
err = sc.CreateSearchContextStarForUser(userCtx, user1.ID, searchContexts[1].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// B-user-level context should still be starred
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starredContexts = getStarredContexts(gotSearchContexts)
|
||||
if len(starredContexts) != 1 || starredContexts[0].Name != "B-user-level" {
|
||||
t.Fatalf("Expected B-user-level context to be starred, got %+v", starredContexts)
|
||||
}
|
||||
|
||||
// Star searchContexts[0]
|
||||
err = sc.CreateSearchContextStarForUser(userCtx, user1.ID, searchContexts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Both contexts should be starred
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starredContexts = getStarredContexts(gotSearchContexts)
|
||||
if len(starredContexts) != 2 || starredContexts[0].Name != "A-user-level" || starredContexts[1].Name != "B-user-level" {
|
||||
t.Fatalf("Expected both contexts to be starred, got %+v", starredContexts)
|
||||
}
|
||||
|
||||
// Unstar searchContexts[0]
|
||||
err = sc.DeleteSearchContextStarForUser(userCtx, user1.ID, searchContexts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Only B-user-level context should be starred
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starredContexts = getStarredContexts(gotSearchContexts)
|
||||
if len(starredContexts) != 1 || starredContexts[0].Name != "B-user-level" {
|
||||
t.Fatalf("Expected only B-user-level context to be starred, got %+v", starredContexts)
|
||||
}
|
||||
|
||||
// Try to unstar searchContexts[0] again, should be a no-op
|
||||
err = sc.DeleteSearchContextStarForUser(userCtx, user1.ID, searchContexts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Only B-user-level context should be starred
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starredContexts = getStarredContexts(gotSearchContexts)
|
||||
if len(starredContexts) != 1 || starredContexts[0].Name != "B-user-level" {
|
||||
t.Fatalf("Expected only B-user-level context to be starred, got %+v", starredContexts)
|
||||
}
|
||||
|
||||
// Try to star the global context, should fail
|
||||
err = sc.CreateSearchContextStarForUser(userCtx, user1.ID, 0)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
// Only B-user-level context should be starred
|
||||
gotSearchContexts, err = sc.ListSearchContexts(userCtx, ListSearchContextsPageOptions{First: 3}, ListSearchContextsOptions{OrderBy: SearchContextsOrderBySpec, OrderByDescending: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starredContexts = getStarredContexts(gotSearchContexts)
|
||||
if len(starredContexts) != 1 || starredContexts[0].Name != "B-user-level" {
|
||||
t.Fatalf("Expected only B-user-level context to be starred, got %+v", starredContexts)
|
||||
}
|
||||
}
|
||||
|
||||
@ -544,7 +544,7 @@ func GetRepositoryRevisions(ctx context.Context, db database.DB, searchContextID
|
||||
}
|
||||
|
||||
func IsAutoDefinedSearchContext(searchContext *types.SearchContext) bool {
|
||||
return searchContext.ID == 0
|
||||
return searchContext.Autodefined
|
||||
}
|
||||
|
||||
func IsInstanceLevelSearchContext(searchContext *types.SearchContext) bool {
|
||||
@ -561,15 +561,15 @@ func IsGlobalSearchContext(searchContext *types.SearchContext) bool {
|
||||
}
|
||||
|
||||
func GetUserSearchContext(userID int32, name string) *types.SearchContext {
|
||||
return &types.SearchContext{Name: name, Public: true, Description: "All repositories you've added to Sourcegraph", NamespaceUserID: userID}
|
||||
return &types.SearchContext{Name: name, Public: true, Description: "All repositories you've added to Sourcegraph", NamespaceUserID: userID, Autodefined: true}
|
||||
}
|
||||
|
||||
func GetOrganizationSearchContext(orgID int32, name string, displayName string) *types.SearchContext {
|
||||
return &types.SearchContext{Name: name, Public: false, Description: fmt.Sprintf("All repositories %s organization added to Sourcegraph", displayName), NamespaceOrgID: orgID}
|
||||
return &types.SearchContext{Name: name, Public: false, Description: fmt.Sprintf("All repositories %s organization added to Sourcegraph", displayName), NamespaceOrgID: orgID, Autodefined: true}
|
||||
}
|
||||
|
||||
func GetGlobalSearchContext() *types.SearchContext {
|
||||
return &types.SearchContext{Name: GlobalSearchContextName, Public: true, Description: "All repositories on Sourcegraph"}
|
||||
return &types.SearchContext{Name: GlobalSearchContextName, Public: true, Description: "All repositories on Sourcegraph", Autodefined: true}
|
||||
}
|
||||
|
||||
func GetSearchContextSpec(searchContext *types.SearchContext) string {
|
||||
@ -587,3 +587,15 @@ func GetSearchContextSpec(searchContext *types.SearchContext) string {
|
||||
return searchContextSpecPrefix + namespaceName + "/" + searchContext.Name
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSearchContextStarForUser(ctx context.Context, db database.DB, searchContext *types.SearchContext, userID int32) error {
|
||||
return db.SearchContexts().CreateSearchContextStarForUser(ctx, userID, searchContext.ID)
|
||||
}
|
||||
|
||||
func DeleteSearchContextStarForUser(ctx context.Context, db database.DB, searchContext *types.SearchContext, userID int32) error {
|
||||
return db.SearchContexts().DeleteSearchContextStarForUser(ctx, userID, searchContext.ID)
|
||||
}
|
||||
|
||||
func SetDefaultSearchContextForUser(ctx context.Context, db database.DB, searchContext *types.SearchContext, userID int32) error {
|
||||
return db.SearchContexts().SetUserDefaultSearchContextID(ctx, userID, searchContext.ID)
|
||||
}
|
||||
|
||||
@ -196,8 +196,8 @@ func TestConstructingSearchContextSpecs(t *testing.T) {
|
||||
wantSearchContextSpec string
|
||||
}{
|
||||
{name: "global search context", searchContext: GetGlobalSearchContext(), wantSearchContextSpec: "global"},
|
||||
{name: "user auto-defined search context", searchContext: &types.SearchContext{Name: "user", NamespaceUserID: 1}, wantSearchContextSpec: "@user"},
|
||||
{name: "org auto-defined search context", searchContext: &types.SearchContext{Name: "org", NamespaceOrgID: 1}, wantSearchContextSpec: "@org"},
|
||||
{name: "user auto-defined search context", searchContext: &types.SearchContext{Name: "user", NamespaceUserID: 1, Autodefined: true}, wantSearchContextSpec: "@user"},
|
||||
{name: "org auto-defined search context", searchContext: &types.SearchContext{Name: "org", NamespaceOrgID: 1, Autodefined: true}, wantSearchContextSpec: "@org"},
|
||||
{name: "user namespaced search context", searchContext: &types.SearchContext{ID: 1, Name: "context", NamespaceUserID: 1, NamespaceUserName: "user"}, wantSearchContextSpec: "@user/context"},
|
||||
{name: "org namespaced search context", searchContext: &types.SearchContext{ID: 1, Name: "context", NamespaceOrgID: 1, NamespaceOrgName: "org"}, wantSearchContextSpec: "@org/context"},
|
||||
{name: "instance-level search context", searchContext: &types.SearchContext{ID: 1, Name: "instance-level-context"}, wantSearchContextSpec: "instance-level-context"},
|
||||
|
||||
@ -1652,6 +1652,15 @@ type SearchContext struct {
|
||||
// Query is the Sourcegraph query that defines this search context
|
||||
// e.g. repo:^github\.com/org rev:bar archive:no f:sub/dir
|
||||
Query string
|
||||
|
||||
// Whether the search context is auto-defined by Sourcegraph. Auto-defined search contexts are not editable by users.
|
||||
Autodefined bool
|
||||
|
||||
// Whether the search context is the default for the user. If the user hasn't explicitly set a default or is not authenticated, the global search context is used.
|
||||
Default bool
|
||||
|
||||
// Whether the user has starred the context. If the user is not authenticated, this field is always false.
|
||||
Starred bool
|
||||
}
|
||||
|
||||
// SearchContextRepositoryRevisions is a simple wrapper for a repository and its revisions
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS search_context_stars;
|
||||
|
||||
DROP TABLE IF EXISTS search_context_default;
|
||||
@ -0,0 +1,2 @@
|
||||
name: create_search_contexts_stars_defaults
|
||||
parents: [1668603582]
|
||||
@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS search_context_stars (
|
||||
search_context_id bigint REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE,
|
||||
user_id integer REFERENCES users(id) ON DELETE CASCADE DEFERRABLE,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
PRIMARY KEY (search_context_id, user_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE search_context_stars IS 'When a user stars a search context, a row is inserted into this table. If the user unstars the search context, the row is deleted. The global context is not in the database, and therefore cannot be starred.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS search_context_default (
|
||||
user_id integer PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE DEFERRABLE,
|
||||
search_context_id bigint NOT NULL REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE
|
||||
);
|
||||
|
||||
COMMENT ON TABLE search_context_default IS 'When a user sets a search context as default, a row is inserted into this table. A user can only have one default search context. If the user has not set their default search context, it will fall back to `global`.';
|
||||
@ -3370,12 +3370,27 @@ CREATE SEQUENCE saved_searches_id_seq
|
||||
|
||||
ALTER SEQUENCE saved_searches_id_seq OWNED BY saved_searches.id;
|
||||
|
||||
CREATE TABLE search_context_default (
|
||||
user_id integer NOT NULL,
|
||||
search_context_id bigint NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE search_context_default IS 'When a user sets a search context as default, a row is inserted into this table. A user can only have one default search context. If the user has not set their default search context, it will fall back to `global`.';
|
||||
|
||||
CREATE TABLE search_context_repos (
|
||||
search_context_id bigint NOT NULL,
|
||||
repo_id integer NOT NULL,
|
||||
revision text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE search_context_stars (
|
||||
search_context_id bigint NOT NULL,
|
||||
user_id integer NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE search_context_stars IS 'When a user stars a search context, a row is inserted into this table. If the user unstars the search context, the row is deleted. The global context is not in the database, and therefore cannot be starred.';
|
||||
|
||||
CREATE TABLE search_contexts (
|
||||
id bigint NOT NULL,
|
||||
name citext NOT NULL,
|
||||
@ -4204,9 +4219,15 @@ ALTER TABLE ONLY repo
|
||||
ALTER TABLE ONLY saved_searches
|
||||
ADD CONSTRAINT saved_searches_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY search_context_default
|
||||
ADD CONSTRAINT search_context_default_pkey PRIMARY KEY (user_id);
|
||||
|
||||
ALTER TABLE ONLY search_context_repos
|
||||
ADD CONSTRAINT search_context_repos_unique UNIQUE (repo_id, search_context_id, revision);
|
||||
|
||||
ALTER TABLE ONLY search_context_stars
|
||||
ADD CONSTRAINT search_context_stars_pkey PRIMARY KEY (search_context_id, user_id);
|
||||
|
||||
ALTER TABLE ONLY search_contexts
|
||||
ADD CONSTRAINT search_contexts_pkey PRIMARY KEY (id);
|
||||
|
||||
@ -5027,12 +5048,24 @@ ALTER TABLE ONLY saved_searches
|
||||
ALTER TABLE ONLY saved_searches
|
||||
ADD CONSTRAINT saved_searches_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
|
||||
ALTER TABLE ONLY search_context_default
|
||||
ADD CONSTRAINT search_context_default_search_context_id_fkey FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE;
|
||||
|
||||
ALTER TABLE ONLY search_context_default
|
||||
ADD CONSTRAINT search_context_default_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE;
|
||||
|
||||
ALTER TABLE ONLY search_context_repos
|
||||
ADD CONSTRAINT search_context_repos_repo_id_fk FOREIGN KEY (repo_id) REFERENCES repo(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY search_context_repos
|
||||
ADD CONSTRAINT search_context_repos_search_context_id_fk FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY search_context_stars
|
||||
ADD CONSTRAINT search_context_stars_search_context_id_fkey FOREIGN KEY (search_context_id) REFERENCES search_contexts(id) ON DELETE CASCADE DEFERRABLE;
|
||||
|
||||
ALTER TABLE ONLY search_context_stars
|
||||
ADD CONSTRAINT search_context_stars_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE;
|
||||
|
||||
ALTER TABLE ONLY search_contexts
|
||||
ADD CONSTRAINT search_contexts_namespace_org_id_fk FOREIGN KEY (namespace_org_id) REFERENCES orgs(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user