[SG-33302] Upgrade to the latest react version and migrate to the new root API (#36045)

* feat: react v18 and new root api with some errors fixes

* web: repo page integration test flake fix (#37592)

* web: fix `blog-viewer` integration test flake (#38069)

* fix: build-ts

* fix: new failing unit tests

* fix: unit test

* web: fix search integration test

* feat: applied request changes

* fix: new failing test because of global mockReactVisibilitySensor

* fix: unexpected onLayoutChange call on SmartInsightsViewGrid

* fix: make ForwardReferenceComponent support custom children

* fix: more new ts lint issue

* fix: remaining issue BatchSpec

* fix: SmartInsightsViewGrid issue with useLayoutEffect

Co-authored-by: gitstart-sourcegraph <gitstart@users.noreply.github.com>
Co-authored-by: Valery Bugakov <skymk1@gmail.com>
This commit is contained in:
GitStart-SourceGraph 2022-07-15 11:58:08 +08:00 committed by GitHub
parent e599e7b792
commit da154b5a93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
119 changed files with 576 additions and 528 deletions

View File

@ -11,7 +11,9 @@ import { DeprecatedTooltip, WildcardThemeContext } from '@sourcegraph/wildcard'
import brandedStyles from '../global-styles/index.scss'
export interface BrandedProps extends MemoryRouterProps, Pick<MockedStoryProviderProps, 'mocks' | 'useStrictMocking'> {
export interface BrandedProps
extends Omit<MemoryRouterProps, 'children'>,
Pick<MockedStoryProviderProps, 'mocks' | 'useStrictMocking'> {
children: React.FunctionComponent<React.PropsWithChildren<ThemeProps>>
styles?: string
}
@ -20,7 +22,7 @@ export interface BrandedProps extends MemoryRouterProps, Pick<MockedStoryProvide
* Wrapper component for branded Storybook stories that provides light theme and react-router props.
* Takes a render function as children that gets called with the props.
*/
export const BrandedStory: React.FunctionComponent<React.PropsWithChildren<BrandedProps>> = ({
export const BrandedStory: React.FunctionComponent<BrandedProps> = ({
children: Children,
styles = brandedStyles,
mocks,

View File

@ -60,7 +60,7 @@ export interface PanelViewWithComponent extends PanelViewData {
/**
* The React element to render in the panel view.
*/
reactElement?: React.ReactFragment
reactElement?: React.ReactNode
// Should the content of the panel be put inside a wrapper container with padding or not.
noWrapper?: boolean
@ -75,7 +75,7 @@ export interface PanelViewWithComponent extends PanelViewData {
interface TabbedPanelItem {
id: string
label: React.ReactFragment
label: React.ReactNode
/**
* Controls the relative order of panel items. The items are laid out from highest priority (at the beginning)
* to lowest priority (at the end). The default is 0.

View File

@ -3,7 +3,7 @@ import '../../shared/polyfills'
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { AnchorLink, setLinkComponent } from '@sourcegraph/wildcard'
@ -23,4 +23,6 @@ const AfterInstallPage: React.FunctionComponent<React.PropsWithChildren<unknown>
</ThemeWrapper>
)
render(<AfterInstallPage />, document.querySelector('#root'))
const root = createRoot(document.querySelector('#root')!)
root.render(<AfterInstallPage />)

View File

@ -4,7 +4,7 @@ import '../../shared/polyfills'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { trimEnd, uniq } from 'lodash'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { from, noop, Observable, of } from 'rxjs'
import { catchError, distinctUntilChanged, filter, map, mapTo } from 'rxjs/operators'
import { Optional } from 'utility-types'
@ -275,7 +275,9 @@ const Options: React.FunctionComponent<React.PropsWithChildren<unknown>> = () =>
}
const inject = (): void => {
render(<Options />, document.body)
const root = createRoot(document.body)
root.render(<Options />)
}
document.addEventListener('DOMContentLoaded', inject)

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import { trimStart } from 'lodash'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { defer, fromEvent, of } from 'rxjs'
import { distinctUntilChanged, filter, map, startWith } from 'rxjs/operators'
import { Omit } from 'utility-types'
@ -552,8 +552,9 @@ function enhanceSearchPage(sourcegraphURL: string): void {
utm_source: getPlatformName(),
utm_campaign: utmCampaign,
})
const root = createRoot(container)
render(
root.render(
<SourcegraphIconButton
label="Search on Sourcegraph"
title="Search on Sourcegraph to get hover tooltips, go to definition and more"
@ -570,8 +571,7 @@ function enhanceSearchPage(sourcegraphURL: string): void {
searchQuery ? `&q=${searchQuery}` : ''
}`
}}
/>,
container
/>
)
}

View File

@ -3,7 +3,8 @@ import * as React from 'react'
import classNames from 'classnames'
import * as H from 'history'
import { isEqual } from 'lodash'
import { render as reactDOMRender, Renderer } from 'react-dom'
import { Renderer } from 'react-dom'
import { createRoot } from 'react-dom/client'
import {
asyncScheduler,
combineLatest,
@ -1604,8 +1605,14 @@ export function injectCodeIntelligenceToCodeHost(
// Flag to hide the actions in the code view toolbar (hide ActionNavItems) leaving only the "Open on Sourcegraph" button in the toolbar.
const hideActions = codeHost.type === 'gerrit'
const renderWithThemeProvider = (element: React.ReactNode, container: Element | null): void =>
reactDOMRender(<WildcardThemeProvider isBranded={false}>{element}</WildcardThemeProvider>, container)
const renderWithThemeProvider = (element: React.ReactNode, container: Element | null): void => {
if (!container) {
return
}
const root = createRoot(container)
root.render(<WildcardThemeProvider isBranded={false}>{element}</WildcardThemeProvider>)
}
subscriptions.add(
// eslint-disable-next-line rxjs/no-async-subscribe, @typescript-eslint/no-misused-promises

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'
import * as H from 'history'
import { isEqual } from 'lodash'
import { render } from 'react-dom'
import { Renderer } from 'react-dom'
import { ContributableMenu } from '@sourcegraph/client-api'
import { DiffPart } from '@sourcegraph/codeintellify'
@ -51,7 +51,7 @@ interface InjectProps
extends PlatformContextProps<'forceUpdateTooltip' | 'settings' | 'sideloadedExtensionURL' | 'sourcegraphURL'>,
ExtensionsControllerProps {
history: H.History
render: typeof render
render: Renderer
}
interface RenderCommandPaletteProps

View File

@ -1,7 +1,4 @@
import * as React from 'react'
import { cleanup, getByText, render } from '@testing-library/react'
import _VisibilitySensor from 'react-visibility-sensor'
import { of } from 'rxjs'
import { map } from 'rxjs/operators'
@ -10,28 +7,10 @@ import {
HIGHLIGHTED_FILE_LINES_LONG,
HIGHLIGHTED_FILE_LINES_SIMPLE,
} from '@sourcegraph/shared/src/testing/searchTestHelpers'
import '@sourcegraph/shared/dev/mockReactVisibilitySensor'
import { CodeExcerpt } from './CodeExcerpt'
export class MockVisibilitySensor extends React.Component<{ onChange?: (isVisible: boolean) => void }> {
constructor(props: { onChange?: (isVisible: boolean) => void }) {
super(props)
if (props.onChange) {
props.onChange(true)
}
}
public render(): JSX.Element {
return <>{this.props.children}</>
}
}
jest.mock('react-visibility-sensor', (): typeof _VisibilitySensor => ({ children, onChange }) => (
<>
<MockVisibilitySensor onChange={onChange}>{children}</MockVisibilitySensor>
</>
))
describe('CodeExcerpt', () => {
afterAll(cleanup)
@ -66,7 +45,7 @@ describe('CodeExcerpt', () => {
// at least exist.
const { container } = render(<CodeExcerpt {...defaultProps} />)
const dataLines = container.querySelectorAll('[data-line]')
expect(dataLines.length).toMatchInlineSnapshot('3')
expect(dataLines).toHaveLength(3)
})
it('renders the code portion of each row', () => {

View File

@ -1,6 +1,5 @@
import { cleanup } from '@testing-library/react'
import * as H from 'history'
import _VisibilitySensor from 'react-visibility-sensor'
import { of } from 'rxjs'
import sinon from 'sinon'
@ -12,16 +11,10 @@ import {
NOOP_SETTINGS_CASCADE,
HIGHLIGHTED_FILE_LINES,
} from '@sourcegraph/shared/src/testing/searchTestHelpers'
import '@sourcegraph/shared/dev/mockReactVisibilitySensor'
import { MockVisibilitySensor } from './CodeExcerpt.test'
import { FileMatchChildren } from './FileMatchChildren'
jest.mock('react-visibility-sensor', (): typeof _VisibilitySensor => ({ children, onChange }) => (
<>
<MockVisibilitySensor onChange={onChange}>{children}</MockVisibilitySensor>
</>
))
const history = H.createBrowserHistory()
history.replace({ pathname: '/search' })

View File

@ -166,7 +166,7 @@ export const FileMatchChildren: React.FunctionComponent<React.PropsWithChildren<
} = props
const fetchHighlightedFileRangeLines = React.useCallback(
(isFirst, startLine, endLine) => {
(isFirst: boolean, startLine: number, endLine: number) => {
const startTime = Date.now()
return fetchHighlightedFileLineRanges(
{

View File

@ -1,7 +1,6 @@
import { cleanup, getAllByTestId, getByTestId } from '@testing-library/react'
import { createBrowserHistory } from 'history'
import FileIcon from 'mdi-react/FileIcon'
import _VisibilitySensor from 'react-visibility-sensor'
import sinon from 'sinon'
import { MatchGroup } from '@sourcegraph/shared/src/components/ranking/PerFileResultRanking'
@ -13,16 +12,10 @@ import {
NOOP_SETTINGS_CASCADE,
RESULT,
} from '@sourcegraph/shared/src/testing/searchTestHelpers'
import '@sourcegraph/shared/dev/mockReactVisibilitySensor'
import { MockVisibilitySensor } from './CodeExcerpt.test'
import { FileSearchResult, limitGroup } from './FileSearchResult'
jest.mock('react-visibility-sensor', (): typeof _VisibilitySensor => ({ children, onChange }) => (
<>
<MockVisibilitySensor onChange={onChange}>{children}</MockVisibilitySensor>
</>
))
describe('FileSearchResult', () => {
afterAll(cleanup)
const history = createBrowserHistory()

View File

@ -37,7 +37,7 @@ export interface ResultContainerProps {
/**
* The title component.
*/
title: React.ReactFragment
title: React.ReactNode
/**
* CSS class name to apply to the title element.
@ -45,20 +45,18 @@ export interface ResultContainerProps {
titleClassName?: string
/** The content to display next to the title. */
description?: React.ReactFragment
description?: React.ReactNode
/**
* The content of the result displayed underneath the result container's
* header when collapsed.
*/
collapsedChildren?: React.ReactFragment
collapsedChildren?: React.ReactNode
/**
* The content of the result displayed underneath the result container's
* header when expanded.
*/
expandedChildren?: React.ReactFragment
expandedChildren?: React.ReactNode
/**
* The label to display next to the collapse button
*/

View File

@ -32,7 +32,7 @@ export const ModalVideo: React.FunctionComponent<React.PropsWithChildren<ModalVi
}) => {
const [isOpen, setIsOpen] = useState(false)
const toggleDialog = useCallback(
isOpen => {
(isOpen: boolean) => {
setIsOpen(isOpen)
if (onToggle) {
onToggle(isOpen)
@ -88,7 +88,7 @@ export const ModalVideo: React.FunctionComponent<React.PropsWithChildren<ModalVi
<Button
variant="icon"
className="p-1"
data-testId="modal-video-close"
data-testid="modal-video-close"
onClick={() => toggleDialog(false)}
aria-label="Close"
>

View File

@ -189,7 +189,7 @@ export const NoResultsPage: React.FunctionComponent<React.PropsWithChildren<NoRe
const [hiddenSectionIDs, setHiddenSectionIds] = useTemporarySetting('search.hiddenNoResultsSections')
const onClose = useCallback(
sectionID => {
(sectionID: SectionID) => {
telemetryService.log('NoResultsPanel', { panelID: sectionID, action: 'closed' })
setHiddenSectionIds((hiddenSectionIDs = []) =>
!hiddenSectionIDs.includes(sectionID) ? [...hiddenSectionIDs, sectionID] : hiddenSectionIDs

View File

@ -404,7 +404,7 @@ const SearchReferenceEntry = <T extends SearchReferenceInfo>({
const [collapsed, setCollapsed] = useState(true)
const collapseIcon = collapsed ? mdiChevronLeft : mdiChevronDown
const handleOpenChange = useCallback(collapsed => setCollapsed(!collapsed), [])
const handleOpenChange = useCallback((collapsed: boolean) => setCollapsed(!collapsed), [])
let buttonTextPrefix: ReactElement | null = null
if (isFilterInfo(searchReference)) {
@ -620,6 +620,6 @@ const SearchReference = React.memo(
export function getSearchReferenceFactory(
props: Omit<SearchReferenceProps, 'filter'>
): (filter: string) => ReactElement {
): (filter: string) => React.ReactNode {
return (filter: string) => <SearchReference {...props} filter={filter} />
}

View File

@ -41,12 +41,12 @@ export interface SearchSidebarProps
/**
* Not yet implemented in the VS Code extension (blocked on Apollo Client integration).
*/
getRevisions?: (revisionsProps: Omit<RevisionsProps, 'query'>) => (query: string) => JSX.Element
getRevisions?: (revisionsProps: Omit<RevisionsProps, 'query'>) => (query: string) => React.ReactNode
/**
* Content to render inside sidebar, but before other sections.
*/
prefixContent?: JSX.Element
prefixContent?: React.ReactNode
buildSearchURLQueryFromQueryState: (queryParameters: BuildSearchQueryURLParameters) => string

View File

@ -10,28 +10,26 @@ import { FilterLink, FilterLinkProps } from './FilterLink'
import styles from './SearchSidebarSection.module.scss'
export const SearchSidebarSection: React.FunctionComponent<
React.PropsWithChildren<{
sectionId: string
header: string
children?: React.ReactElement | React.ReactElement[] | ((filter: string) => React.ReactElement)
className?: string
showSearch?: boolean // Search only works if children are FilterLink
onToggle?: (id: string, open: boolean) => void
startCollapsed?: boolean
/**
* Shown when the built-in search doesn't find any results.
*/
noResultText?: React.ReactElement | string
/**
* Clear the search input whenever this value changes. This is supposed to
* be used together with function children, which use the search input but
* handle search on their own.
* Defaults to the component's children.
*/
clearSearchOnChange?: {}
}>
> = React.memo(
export const SearchSidebarSection: React.FunctionComponent<{
sectionId: string
header: string
children?: React.ReactNode | React.ReactNode[] | ((filter: string) => React.ReactNode)
className?: string
showSearch?: boolean // Search only works if children are FilterLink
onToggle?: (id: string, open: boolean) => void
startCollapsed?: boolean
/**
* Shown when the built-in search doesn't find any results.
*/
noResultText?: React.ReactElement | string
/**
* Clear the search input whenever this value changes. This is supposed to
* be used together with function children, which use the search input but
* handle search on their own.
* Defaults to the component's children.
*/
clearSearchOnChange?: {}
}> = React.memo(
({
sectionId,
header,
@ -93,7 +91,7 @@ export const SearchSidebarSection: React.FunctionComponent<
const [isOpened, setOpened] = useState(!startCollapsed)
const handleOpenChange = useCallback(
isOpen => {
(isOpen: boolean) => {
if (onToggle) {
onToggle(sectionId, isOpen)
}

View File

@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react-hooks'
import { renderHook } from '@testing-library/react'
import { FilterType } from '@sourcegraph/shared/src/search/query/filters'
import { Filter } from '@sourcegraph/shared/src/search/stream'

View File

@ -1,4 +1,4 @@
import { act, renderHook } from '@testing-library/react-hooks'
import { act, renderHook } from '@testing-library/react'
import { times } from 'lodash'
import { INCREMENTAL_ITEMS_TO_SHOW, DEFAULT_INITIAL_ITEMS_TO_SHOW, useItemsToShow } from './use-items-to-show'

View File

@ -0,0 +1,27 @@
import React from 'react'
import _VisibilitySensor from 'react-visibility-sensor'
type VisibilitySensorPropsType = React.ComponentProps<typeof _VisibilitySensor>
export class MockVisibilitySensor extends React.Component<VisibilitySensorPropsType> {
constructor(props: { onChange?: (isVisible: boolean) => void }) {
super(props)
if (props.onChange) {
props.onChange(true)
}
}
public render(): JSX.Element {
return <>{this.props.children}</>
}
}
jest.mock(
'react-visibility-sensor',
(): typeof _VisibilitySensor => ({ children, onChange }: VisibilitySensorPropsType) => (
<>
<MockVisibilitySensor onChange={onChange}>{children}</MockVisibilitySensor>
</>
)
)

View File

@ -185,7 +185,8 @@ describe('ActionItem', () => {
// to result in the setState call.)
userEvent.click(screen.getByRole('button'))
await new Promise<void>(resolve => setTimeout(resolve))
// we should wait for the button to be enabled again after got errors. Otherwise it will be flaky
await waitFor(() => expect(screen.getByLabelText('d')).toBeEnabled())
expect(asFragment()).toMatchSnapshot()
})
@ -211,7 +212,7 @@ describe('ActionItem', () => {
// to result in the setState call.)
userEvent.click(screen.getByRole('button'))
await new Promise<void>(resolve => setTimeout(resolve))
await waitFor(() => expect(screen.getByLabelText('Error: x')).toBeInTheDocument())
expect(asFragment()).toMatchSnapshot()
})

View File

@ -39,7 +39,7 @@ interface Props extends ActionsProps, TelemetryProps {
}
/** Displays the actions in a container, with a wrapper and/or empty element. */
export const ActionsContainer: React.FunctionComponent<React.PropsWithChildren<Props>> = props => {
export const ActionsContainer: React.FunctionComponent<Props> = props => {
const { scope, extraContext, returnInactiveMenuItems, extensionsController, menu, empty } = props
const contributions = useObservable(

View File

@ -228,8 +228,9 @@ exports[`ActionItem run command with showLoadingSpinnerDuringExecution 2`] = `
<DocumentFragment>
<button
aria-label="d"
class="test-action-item"
class="test-action-item actionItemLoading"
data-tooltip="d"
disabled=""
type="button"
>
<img
@ -237,6 +238,17 @@ exports[`ActionItem run command with showLoadingSpinnerDuringExecution 2`] = `
src="u"
/>
g: t
<div
class="loader"
data-testid="action-item-spinner"
>
<div
aria-label="Loading"
aria-live="polite"
class="mdi-icon loadingSpinner"
role="img"
/>
</div>
</button>
</DocumentFragment>
`;

View File

@ -60,7 +60,7 @@ export class VirtualList<TItem, TExtraItemProps = undefined> extends React.PureC
<Element className={this.props.className} ref={this.props.onRef} aria-label={this.props['aria-label']}>
{this.props.items.slice(0, this.props.itemsToShow).map((item, index) => (
<VisibilitySensor
onChange={isVisible => this.onChangeVisibility(isVisible, index)}
onChange={(isVisible: boolean) => this.onChangeVisibility(isVisible, index)}
key={this.props.itemKey(item)}
containment={this.props.containment}
partialVisibility={true}

View File

@ -1,7 +1,7 @@
import assert from 'assert'
import { render } from '@testing-library/react'
import { createMemoryHistory } from 'history'
import ReactDOM from 'react-dom'
import * as sinon from 'sinon'
import { Link } from '@sourcegraph/wildcard'
@ -15,17 +15,14 @@ describe('createLinkClickHandler', () => {
const history = createMemoryHistory({ initialEntries: [] })
expect(history).toHaveLength(0)
const root = document.createElement('div')
document.body.append(root)
ReactDOM.render(
const { container } = render(
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div onClick={createLinkClickHandler(history)}>
<Link to="https://sourcegraph.test/else/where">Test</Link>
</div>,
root
</div>
)
const anchor = root.querySelector('a')
const anchor = container.querySelector('a')
assert(anchor)
const spy = sinon.spy((_event: MouseEvent) => undefined)
@ -45,17 +42,14 @@ describe('createLinkClickHandler', () => {
const history = createMemoryHistory({ initialEntries: [] })
expect(history).toHaveLength(0)
const root = document.createElement('div')
document.body.append(root)
ReactDOM.render(
const { container } = render(
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div onClick={createLinkClickHandler(history)}>
<Link to="https://github.com/some/where">Test</Link>
</div>,
root
</div>
)
const anchor = root.querySelector('a')
const anchor = container.querySelector('a')
assert(anchor)
const spy = sinon.spy((_event: MouseEvent) => undefined)

View File

@ -2,8 +2,7 @@ import { useEffect } from 'react'
import { gql } from '@apollo/client'
import { createMockClient } from '@apollo/client/testing'
import { render } from '@testing-library/react'
import { renderHook, act as actHook } from '@testing-library/react-hooks'
import { render, renderHook, act as actHook } from '@testing-library/react'
import { TemporarySettingsContext } from './TemporarySettingsProvider'
import { InMemoryMockSettingsBackend, TemporarySettingsStorage } from './TemporarySettingsStorage'

View File

@ -11,13 +11,18 @@ export interface RenderWithBrandedContextResult extends RenderResult {
history: MemoryHistory
}
interface RenderWithBrandedContextOptions {
route?: string
history?: MemoryHistory<unknown>
}
const wildcardTheme: WildcardTheme = {
isBranded: true,
}
export function renderWithBrandedContext(
children: ReactNode,
{ route = '/', history = createMemoryHistory({ initialEntries: [route] }) } = {}
{ route = '/', history = createMemoryHistory({ initialEntries: [route] }) }: RenderWithBrandedContextOptions = {}
): RenderWithBrandedContextResult {
return {
...render(

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { renderHook, act } from '@testing-library/react-hooks'
import { renderHook, act } from '@testing-library/react'
import { last, min, noop } from 'lodash'
import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs'
import { delay } from 'rxjs/operators'

View File

@ -145,7 +145,7 @@ export const SearchHomeView: React.FunctionComponent<React.PropsWithChildren<Sea
)
const fetchStreamSuggestions = useCallback(
(query): Observable<SearchMatch[]> =>
(query: string): Observable<SearchMatch[]> =>
wrapRemoteObservable(extensionCoreAPI.fetchStreamSuggestions(query, instanceURL)),
[extensionCoreAPI, instanceURL]
)

View File

@ -244,7 +244,7 @@ export const SearchResultsView: React.FunctionComponent<React.PropsWithChildren<
)
const fetchStreamSuggestions = useCallback(
(query): Observable<SearchMatch[]> =>
(query: string): Observable<SearchMatch[]> =>
wrapRemoteObservable(extensionCoreAPI.fetchStreamSuggestions(query, instanceURL)),
[extensionCoreAPI, instanceURL]
)

View File

@ -169,7 +169,7 @@ export const FileMatchChildren: React.FunctionComponent<React.PropsWithChildren<
const { openFile, openSymbol } = useOpenSearchResultsContext()
const fetchHighlightedFileRangeLines = React.useCallback(
(isFirst, startLine, endLine) => {
(isFirst: boolean, startLine: number, endLine: number) => {
const startTime = Date.now()
return fetchHighlightedFileLineRanges(
{

View File

@ -5,7 +5,7 @@ import React, { useMemo } from 'react'
import { ShortcutProvider } from '@slimsag/react-shortcuts'
import { VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react'
import * as Comlink from 'comlink'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { MemoryRouter } from 'react-router'
import { CompatRouter } from 'react-router-dom-v5-compat'
@ -117,7 +117,9 @@ const Main: React.FC<React.PropsWithChildren<unknown>> = () => {
)
}
render(
const root = createRoot(document.querySelector('#root')!)
root.render(
<ShortcutProvider>
<WildcardThemeContext.Provider value={{ isBranded: true }}>
{/* Required for shared components that depend on `location`. */}
@ -128,6 +130,5 @@ render(
</MemoryRouter>
<DeprecatedTooltip key={1} className="sourcegraph-tooltip" />
</WildcardThemeContext.Provider>
</ShortcutProvider>,
document.querySelector('#root')
</ShortcutProvider>
)

View File

@ -4,7 +4,7 @@ import React, { useMemo } from 'react'
import { VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react'
import * as Comlink from 'comlink'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
import { AnchorLink, setLinkComponent, useObservable } from '@sourcegraph/wildcard'
@ -51,4 +51,6 @@ const Main: React.FC<React.PropsWithChildren<unknown>> = () => {
)
}
render(<Main />, document.querySelector('#root'))
const root = createRoot(document.querySelector('#root')!)
root.render(<Main />)

View File

@ -5,7 +5,7 @@ import React, { useMemo, useState } from 'react'
import { ShortcutProvider } from '@slimsag/react-shortcuts'
import { VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react'
import * as Comlink from 'comlink'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect'
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
@ -125,12 +125,13 @@ const Main: React.FC<React.PropsWithChildren<unknown>> = () => {
)
}
render(
const root = createRoot(document.querySelector('#root')!)
root.render(
<ShortcutProvider>
<WildcardThemeContext.Provider value={{ isBranded: true }}>
<Main />
<DeprecatedTooltip key={1} className="sourcegraph-tooltip" />
</WildcardThemeContext.Provider>
</ShortcutProvider>,
document.querySelector('#root')
</ShortcutProvider>
)

View File

@ -190,7 +190,10 @@ const history = createBrowserHistory()
/**
* The root component.
*/
export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, SourcegraphWebAppState> {
export class SourcegraphWebApp extends React.Component<
React.PropsWithChildren<SourcegraphWebAppProps>,
SourcegraphWebAppState
> {
private readonly subscriptions = new Subscription()
private readonly userRepositoriesUpdates = new Subject<void>()
private readonly platformContext: PlatformContext = createPlatformContext()
@ -311,7 +314,7 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
this.subscriptions.unsubscribe()
}
public render(): React.ReactFragment | null {
public render(): React.ReactNode {
if (window.pageError && window.pageError.statusCode !== 404) {
const statusCode = window.pageError.statusCode
const statusText = window.pageError.statusText

View File

@ -8,7 +8,7 @@ import styles from './Tooltip.module.scss'
const TOOLTIP_PADDING = createRectangle(0, 0, 10, 10)
export const Tooltip: React.FunctionComponent = props => {
export const Tooltip: React.FunctionComponent<React.PropsWithChildren<{}>> = props => {
const [virtualElement, setVirtualElement] = useState<PopoverPoint | null>(null)
useEffect(() => {

View File

@ -22,7 +22,7 @@ interface Props {
* Optional children that appear below the title bar that can be expanded/collapsed. If present,
* a button that expands or collapses the children will be shown.
*/
children?: React.ReactFragment
children?: React.ReactNode
/**
* Whether the children are expanded and visible by default.
@ -50,7 +50,7 @@ interface Props {
* Collapsible is an element with a title that is always displayed and children that are displayed
* only when expanded.
*/
export const Collapsible: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
export const Collapsible: React.FunctionComponent<Props> = ({
title,
detail,
children,

View File

@ -45,7 +45,7 @@ interface State {
* Components should handle their own errors (and must not rely on this error boundary). This error
* boundary is a last resort in case of an unexpected error.
*/
export class ErrorBoundary extends React.PureComponent<Props, State> {
export class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>, State> {
public state: State = {}
public static getDerivedStateFromError(error: any): Pick<State, 'error'> {

View File

@ -16,7 +16,7 @@ const getBrandName = (): string => {
let titleSet = false
export const PageTitle: React.FunctionComponent<PageTitleProps> = ({ title }) => {
export const PageTitle: React.FunctionComponent<React.PropsWithChildren<PageTitleProps>> = ({ title }) => {
useEffect(() => {
if (titleSet) {
console.error('more than one PageTitle used at the same time')

View File

@ -22,11 +22,11 @@ if (!window.context) {
window.context = {} as SourcegraphContext & Mocha.SuiteFunction
}
export interface WebStoryProps extends MemoryRouterProps, Pick<MockedStoryProviderProps, 'mocks' | 'useStrictMocking'> {
export interface WebStoryProps
extends Omit<MemoryRouterProps, 'children'>,
Pick<MockedStoryProviderProps, 'mocks' | 'useStrictMocking'> {
children: React.FunctionComponent<
React.PropsWithChildren<
ThemeProps & BreadcrumbSetters & BreadcrumbsProps & TelemetryProps & RouteComponentProps<any>
>
ThemeProps & BreadcrumbSetters & BreadcrumbsProps & TelemetryProps & RouteComponentProps<any>
>
}
@ -34,7 +34,7 @@ export interface WebStoryProps extends MemoryRouterProps, Pick<MockedStoryProvid
* Wrapper component for webapp Storybook stories that provides light theme and react-router props.
* Takes a render function as children that gets called with the props.
*/
export const WebStory: React.FunctionComponent<React.PropsWithChildren<WebStoryProps>> = ({
export const WebStory: React.FunctionComponent<WebStoryProps> = ({
children,
mocks,
useStrictMocking,

View File

@ -111,7 +111,7 @@ export const FileDiffConnection: React.FunctionComponent<React.PropsWithChildren
const diffsUpdates = useMemo(() => new ReplaySubject<Connection<FileDiffFields> | ErrorLike | undefined>(1), [])
const nextDiffsUpdate: FileDiffConnectionProps['onUpdate'] = useCallback(
fileDiffsOrError => diffsUpdates.next(fileDiffsOrError),
(fileDiffsOrError: Connection<FileDiffFields> | ErrorLike | undefined) => diffsUpdates.next(fileDiffsOrError),
[diffsUpdates]
)

View File

@ -59,7 +59,7 @@ export const FileDiffNode: React.FunctionComponent<React.PropsWithChildren<FileD
setRenderDeleted(true)
}, [])
let path: React.ReactFragment
let path: React.ReactNode
if (node.newPath && (node.newPath === node.oldPath || !node.oldPath)) {
path = <span title={node.newPath}>{node.newPath}</span>
} else if (node.newPath && node.oldPath && node.newPath !== node.oldPath) {
@ -76,7 +76,7 @@ export const FileDiffNode: React.FunctionComponent<React.PropsWithChildren<FileD
path = <span title={node.oldPath!}>{node.oldPath}</span>
}
let stat: React.ReactFragment
let stat: React.ReactNode
// If one of the files was binary, display file size change instead of DiffStat.
if (node.oldFile?.binary || node.newFile?.binary) {
const sizeChange = (node.newFile?.byteSize ?? 0) - (node.oldFile?.byteSize ?? 0)

View File

@ -59,7 +59,7 @@ export interface AddExternalServiceOptions {
/**
* Instructions that will appear on the add / edit page
*/
instructions?: JSX.Element | string
instructions?: React.ReactNode | string
/**
* The JSON schema of the external service configuration
@ -119,15 +119,15 @@ const editorActionComments = {
// (https://docs.sourcegraph.com/admin/repo/permissions#sudo-access-token).`,
}
const Field = (props: { children: React.ReactChildren | string | string[] }): JSX.Element => (
const Field: React.FunctionComponent<{ children: React.ReactNode | string | string[] }> = props => (
<Code className="hljs-type">{props.children}</Code>
)
const Value = (props: { children: React.ReactChildren | string | string[] }): JSX.Element => (
const Value: React.FunctionComponent<{ children: React.ReactNode | string | string[] }> = props => (
<Code className="hljs-attr">{props.children}</Code>
)
const githubInstructions = (isEnterprise: boolean): JSX.Element => (
const githubInstructions = (isEnterprise: boolean): React.ReactNode => (
<div>
<ol>
{isEnterprise && (

View File

@ -29,7 +29,7 @@ export interface BatchSpecProps extends ThemeProps {
className?: string
}
export const BatchSpec: React.FunctionComponent<React.PropsWithChildren<BatchSpecProps>> = ({
export const BatchSpec: React.FunctionComponent<BatchSpecProps> = ({
originalInput,
isLightTheme,
className,
@ -90,19 +90,12 @@ export const BatchSpecDownloadLink: React.FunctionComponent<
// TODO: Consider merging this component with BatchSpecDownloadLink
export const BatchSpecDownloadButton: React.FunctionComponent<
React.PropsWithChildren<BatchSpecProps & Pick<BatchChangeFields, 'name'>>
BatchSpecProps & Pick<BatchChangeFields, 'name'>
> = React.memo(function BatchSpecDownloadButton(props) {
return (
<Button
className="text-right text-nowrap"
{...props}
variant="secondary"
outline={true}
as={BatchSpecDownloadLink}
asButton={false}
>
<BatchSpecDownloadLink className="text-right text-nowrap" {...props} asButton={false}>
<Icon aria-hidden={true} svgPath={mdiFileDownload} /> Download YAML
</Button>
</BatchSpecDownloadLink>
)
})

View File

@ -35,7 +35,7 @@ export default config
export const Unstarted: Story = () => (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={new WildcardMockLink(UNSTARTED_CONNECTION_MOCKS)}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
@ -46,7 +46,7 @@ export const Unstarted: Story = () => (
}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -55,7 +55,7 @@ export const Unstarted: Story = () => (
export const UnstartedWithCachedConnectionResult: Story = () => (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={new WildcardMockLink(UNSTARTED_WITH_CACHE_CONNECTION_MOCKS)}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
@ -66,7 +66,7 @@ export const UnstartedWithCachedConnectionResult: Story = () => (
}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -113,7 +113,7 @@ export const QueuedInProgress: Story = () => {
return (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={inProgressConnectionMocks}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
@ -124,7 +124,7 @@ export const QueuedInProgress: Story = () => {
}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -172,14 +172,14 @@ export const QueuedInProgressWithCachedConnectionResult: Story = () => {
return (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={inProgressConnectionMocks}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
batchSpec={mockBatchSpec()}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -228,14 +228,14 @@ export const FailedErrored: Story = () => {
return (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={failedConnectionMocks}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
batchSpec={mockBatchSpec()}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -284,14 +284,14 @@ export const FailedErroredWithCachedConnectionResult: Story = () => {
return (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={failedConnectionMocks}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
batchSpec={mockBatchSpec()}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -303,7 +303,7 @@ FailedErroredWithCachedConnectionResult.storyName = 'failed/errored, with cached
export const Succeeded: Story = () => (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={new WildcardMockLink(UNSTARTED_WITH_CACHE_CONNECTION_MOCKS)}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
@ -322,7 +322,7 @@ export const Succeeded: Story = () => (
},
}}
>
<WorkspacesPreview {...props} />
<WorkspacesPreview />
</BatchSpecContextProvider>
</MockedTestProvider>
)}
@ -331,14 +331,14 @@ export const Succeeded: Story = () => (
export const ReadOnly: Story = () => (
<WebStory>
{props => (
{() => (
<MockedTestProvider link={new WildcardMockLink(UNSTARTED_WITH_CACHE_CONNECTION_MOCKS)}>
<BatchSpecContextProvider
batchChange={mockBatchChange()}
batchSpec={mockBatchSpec()}
refetchBatchChange={() => Promise.resolve()}
>
<WorkspacesPreview {...props} isReadOnly={true} />
<WorkspacesPreview isReadOnly={true} />
</BatchSpecContextProvider>
</MockedTestProvider>
)}

View File

@ -22,9 +22,9 @@ export default config
export const Executing: Story = () => (
<WebStory>
{props => (
{() => (
<BatchSpecContextProvider batchChange={mockBatchChange()} batchSpec={EXECUTING_BATCH_SPEC}>
<ActionsMenu {...props} />
<ActionsMenu />
</BatchSpecContextProvider>
)}
</WebStory>
@ -32,9 +32,9 @@ export const Executing: Story = () => (
export const Failed: Story = () => (
<WebStory>
{props => (
{() => (
<BatchSpecContextProvider batchChange={mockBatchChange()} batchSpec={COMPLETED_WITH_ERRORS_BATCH_SPEC}>
<ActionsMenu {...props} />
<ActionsMenu />
</BatchSpecContextProvider>
)}
</WebStory>
@ -42,9 +42,9 @@ export const Failed: Story = () => (
export const Completed: Story = () => (
<WebStory>
{props => (
{() => (
<BatchSpecContextProvider batchChange={mockBatchChange()} batchSpec={COMPLETED_BATCH_SPEC}>
<ActionsMenu {...props} />
<ActionsMenu />
</BatchSpecContextProvider>
)}
</WebStory>
@ -52,9 +52,9 @@ export const Completed: Story = () => (
export const CompletedWithErrors: Story = () => (
<WebStory>
{props => (
{() => (
<BatchSpecContextProvider batchChange={mockBatchChange()} batchSpec={COMPLETED_WITH_ERRORS_BATCH_SPEC}>
<ActionsMenu {...props} />
<ActionsMenu />
</BatchSpecContextProvider>
)}
</WebStory>

View File

@ -33,7 +33,7 @@ import { CancelExecutionModal } from './CancelExecutionModal'
import styles from './ActionsMenu.module.scss'
export const ActionsMenu: React.FunctionComponent<React.PropsWithChildren<{}>> = () => {
export const ActionsMenu: React.FunctionComponent = () => {
const { batchChange, batchSpec, setActionsError } = useBatchSpecContext<BatchSpecExecutionFields>()
return <MemoizedActionsMenu batchChange={batchChange} batchSpec={batchSpec} setActionsError={setActionsError} />

View File

@ -58,7 +58,7 @@ export const OldBatchChangePageContent: React.FunctionComponent<React.PropsWithC
return (
<>
<H2>1. Write a batch spec YAML file</H2>
<H2 data-testid="batch-spec-yaml-file">1. Write a batch spec YAML file</H2>
<Container className="mb-3">
<Text className="mb-0">
The batch spec (

View File

@ -13,8 +13,6 @@ const config: Meta = {
export default config
export const CreatingNewBatchChangeFromSearch: Story = () => (
<WebStory>{props => <SearchTemplatesBanner {...props} />}</WebStory>
)
export const CreatingNewBatchChangeFromSearch: Story = () => <WebStory>{() => <SearchTemplatesBanner />}</WebStory>
CreatingNewBatchChangeFromSearch.storyName = 'Creating new batch change from search'

View File

@ -76,7 +76,11 @@ export const DeleteMonitorModal: React.FunctionComponent<React.PropsWithChildren
)}
</div>
)}
{deleteCompletedOrError && <div>{deleteCompletedOrError === 'loading' && <LoadingSpinner />}</div>}
{/*
* Issue: This JSX tag's 'children' prop expects a single child of type 'ReactNode', but multiple children were provided
* It seems that v18 requires explicit boolean value
*/}
{!!deleteCompletedOrError && <div>{deleteCompletedOrError === 'loading' && <LoadingSpinner />}</div>}
</Modal>
)
}

View File

@ -3,7 +3,7 @@ import React, { FunctionComponent, useCallback, useEffect, useMemo } from 'react
import { useApolloClient } from '@apollo/client'
import { mdiChevronRight } from '@mdi/js'
import classNames from 'classnames'
import { RouteComponentProps, useHistory } from 'react-router'
import { RouteComponentProps, useHistory, useLocation } from 'react-router'
import { Subject } from 'rxjs'
import { GitObjectType } from '@sourcegraph/shared/src/graphql-operations'
@ -81,6 +81,7 @@ export const CodeIntelConfigurationPage: FunctionComponent<
useEffect(() => telemetryService.logViewEvent('CodeIntelConfiguration'), [telemetryService])
const history = useHistory()
const location = useLocation<{ message: string; modal: string }>()
const apolloClient = useApolloClient()
const queryPoliciesCallback = useCallback(
@ -109,9 +110,7 @@ export const CodeIntelConfigurationPage: FunctionComponent<
{authenticatedUser?.siteAdmin && <PolicyListActions history={history} />}
</CodeIntelConfigurationPageHeader>
{history.location.state && (
<FlashMessage state={history.location.state.modal} message={history.location.state.message} />
)}
{location.state && <FlashMessage state={location.state.modal} message={location.state.message} />}
<Container>
<FilteredConnection<CodeIntelligenceConfigurationPolicyFields, {}>
listComponent="div"

View File

@ -3,7 +3,7 @@ import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import { ApolloError } from '@apollo/client'
import { mdiDelete } from '@mdi/js'
import * as H from 'history'
import { RouteComponentProps } from 'react-router'
import { RouteComponentProps, useLocation } from 'react-router'
import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
import { GitObjectType } from '@sourcegraph/shared/src/graphql-operations'
@ -45,6 +45,7 @@ export const CodeIntelConfigurationPolicyPage: FunctionComponent<
lockfileIndexingEnabled = window.context?.codeIntelLockfileIndexingEnabled,
}) => {
useEffect(() => telemetryService.logViewEvent('CodeIntelConfigurationPolicy'), [telemetryService])
const location = useLocation<{ message: string; modal: string }>()
const { policyConfig, loadingPolicyConfig, policyConfigError } = usePolicyConfigurationByID(id)
const [saved, setSaved] = useState<CodeIntelligenceConfigurationPolicyFields>()
@ -129,9 +130,7 @@ export const CodeIntelConfigurationPolicyPage: FunctionComponent<
{savingError && <ErrorAlert prefix="Error saving configuration policy" error={savingError} />}
{deleteError && <ErrorAlert prefix="Error deleting configuration policy" error={deleteError} />}
{history.location.state && (
<FlashMessage state={history.location.state.modal} message={history.location.state.message} />
)}
{location.state && <FlashMessage state={location.state.modal} message={location.state.message} />}
{policy.protected ? (
<Alert variant="info">

View File

@ -2,7 +2,7 @@ import { FunctionComponent, useCallback, useEffect, useMemo } from 'react'
import { useApolloClient } from '@apollo/client'
import classNames from 'classnames'
import { RouteComponentProps } from 'react-router'
import { RouteComponentProps, useLocation } from 'react-router'
import { Subject } from 'rxjs'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
@ -73,7 +73,7 @@ const filters: FilteredConnectionFilter[] = [
},
]
export const CodeIntelIndexesPage: FunctionComponent<React.PropsWithChildren<CodeIntelIndexesPageProps>> = ({
export const CodeIntelIndexesPage: FunctionComponent<CodeIntelIndexesPageProps> = ({
authenticatedUser,
repo,
queryLsifIndexListByRepository = defaultQueryLsifIndexListByRepository,
@ -81,9 +81,9 @@ export const CodeIntelIndexesPage: FunctionComponent<React.PropsWithChildren<Cod
now,
telemetryService,
history,
...props
}) => {
useEffect(() => telemetryService.logViewEvent('CodeIntelIndexes'), [telemetryService])
const location = useLocation<{ message: string; modal: string }>()
const apolloClient = useApolloClient()
const queryIndexes = useCallback(
@ -109,15 +109,13 @@ export const CodeIntelIndexesPage: FunctionComponent<React.PropsWithChildren<Cod
className="mb-3"
/>
{repo && authenticatedUser?.siteAdmin && (
{!!repo && !!authenticatedUser?.siteAdmin && (
<Container className="mb-2">
<EnqueueForm repoId={repo.id} querySubject={querySubject} />
</Container>
)}
{history.location.state && (
<FlashMessage state={history.location.state.modal} message={history.location.state.message} />
)}
{!!location.state && <FlashMessage state={location.state.modal} message={location.state.message} />}
<Container>
<div className="list-group position-relative">
@ -132,7 +130,7 @@ export const CodeIntelIndexesPage: FunctionComponent<React.PropsWithChildren<Cod
nodeComponentProps={{ now }}
queryConnection={queryIndexes}
history={history}
location={props.location}
location={location}
cursorPaging={true}
filters={filters}
emptyElement={<EmptyAutoIndex />}

View File

@ -2,7 +2,7 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're
import { useApolloClient } from '@apollo/client'
import classNames from 'classnames'
import { RouteComponentProps } from 'react-router'
import { RouteComponentProps, useLocation } from 'react-router'
import { of } from 'rxjs'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
@ -102,6 +102,7 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
...props
}) => {
useEffect(() => telemetryService.logViewEvent('CodeIntelUploads'), [telemetryService])
const location = useLocation<{ message: string; modal: string }>()
const apolloClient = useApolloClient()
const queryLsifUploads = useCallback(
@ -124,14 +125,14 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
const [deleteStatus, setDeleteStatus] = useState({ isDeleting: false, message: '', state: '' })
useEffect(() => {
if (history.location.state) {
if (location.state) {
setDeleteStatus({
isDeleting: true,
message: history.location.state.message,
state: history.location.state.modal,
message: location.state.message,
state: location.state.modal,
})
}
}, [history.location.state])
}, [location.state])
return (
<div className="code-intel-uploads">

View File

@ -1,11 +1,13 @@
import '@sourcegraph/shared/src/polyfills'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { EmbeddedWebApp } from './EmbeddedWebApp'
// It's important to have a root component in a separate file to create a react-refresh boundary and avoid page reload.
// https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#edits-always-lead-to-full-reload
window.addEventListener('DOMContentLoaded', () => {
render(<EmbeddedWebApp />, document.querySelector('#root'))
const root = createRoot(document.querySelector('#root')!)
root.render(<EmbeddedWebApp />)
})

View File

@ -38,7 +38,7 @@ export const CodeInsightsCreationActions: FC<CodeInsightsCreationActionsProps> =
)}
<div className="d-flex flex-wrap align-items-center">
{errors && <ErrorAlert className="w-100" error={errors} />}
{!!errors && <ErrorAlert className="w-100" error={errors} />}
<LoaderButton
type="submit"

View File

@ -49,7 +49,7 @@ const LivePreviewBanner: React.FunctionComponent<React.PropsWithChildren<unknown
interface LivePreviewChartProps extends React.ComponentProps<typeof ParentSize> {}
const LivePreviewChart: React.FunctionComponent<React.PropsWithChildren<LivePreviewChartProps>> = props => (
const LivePreviewChart: React.FunctionComponent<LivePreviewChartProps> = props => (
<ParentSize {...props} className={classNames(styles.chartBlock, props.className)} />
)

View File

@ -20,7 +20,7 @@ function getDefaultInputStatus<T>({ meta }: useFieldAPI<T>): InputStatus {
return InputStatus.initial
}
function getDefaultInputError<T>({ meta }: useFieldAPI<T>): Pick<InputProps, 'error'> {
function getDefaultInputError<T>({ meta }: useFieldAPI<T>): InputProps['error'] {
return meta.touched && meta.error
}

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback, useEffect, useState } from 'react'
import React, { memo, useCallback, useLayoutEffect, useState } from 'react'
import { isEqual } from 'lodash'
import { Layout, Layouts } from 'react-grid-layout'
@ -32,7 +32,7 @@ export const SmartInsightsViewGrid: React.FunctionComponent<React.PropsWithChild
const [layouts, setLayouts] = useState<Layouts>({})
const [resizingView, setResizeView] = useState<Layout | null>(null)
useEffect(() => {
useLayoutEffect(() => {
setLayouts(insightLayoutGenerator(insights))
}, [insights])

View File

@ -114,7 +114,12 @@ interface ToggleButtonProps {
onClick: (value: SeriesSortOptionsInput) => void
}
const ToggleButton: React.FunctionComponent<ToggleButtonProps> = ({ selected, value, children, onClick }) => (
const ToggleButton: React.FunctionComponent<React.PropsWithChildren<ToggleButtonProps>> = ({
selected,
value,
children,
onClick,
}) => (
<Button variant="secondary" size="sm" className={getClasses(selected, value)} onClick={() => onClick(value)}>
{children}
</Button>

View File

@ -1,4 +1,4 @@
import { renderHook, act } from '@testing-library/react-hooks'
import { renderHook, act } from '@testing-library/react'
import { Observable, ObservableInput, of } from 'rxjs'
import { delay, map, switchMap, tap } from 'rxjs/operators'
import sinon from 'sinon'

View File

@ -1,6 +1,6 @@
import React from 'react'
import { renderHook } from '@testing-library/react-hooks'
import { renderHook } from '@testing-library/react'
import { CodeInsightsBackend, CodeInsightsBackendContext, CodeInsightsGqlBackend } from '../core'

View File

@ -44,9 +44,7 @@ export interface InsightsDashboardCreationContentProps {
/**
* Renders creation UI form content (fields, submit and cancel buttons).
*/
export const InsightsDashboardCreationContent: React.FunctionComponent<
React.PropsWithChildren<InsightsDashboardCreationContentProps>
> = props => {
export const InsightsDashboardCreationContent: React.FunctionComponent<InsightsDashboardCreationContentProps> = props => {
const { initialValues, owners, onSubmit, children } = props
const { UIFeatures } = useContext(CodeInsightsBackendContext)

View File

@ -173,10 +173,8 @@ const triggerDashboardMenuItem = async (
const dashboardMenuItem = screen.getByTestId(testId)
// We're simulating keyboard navigation here to circumvent a bug in ReachUI
// does not respond to programmatic click events on menu items
dashboardMenuItem.focus()
user.keyboard(' ')
user.click(dashboardMenuItem)
}
beforeEach(() => {

View File

@ -41,7 +41,7 @@ interface ComputeLivePreviewProps {
}[]
}
export const ComputeLivePreview: React.FunctionComponent<React.PropsWithChildren<ComputeLivePreviewProps>> = props => {
export const ComputeLivePreview: React.FunctionComponent<ComputeLivePreviewProps> = props => {
// For the purposes of building out this component before the backend is ready
// we are using the standard "line series" type data.
// TODO after backend is merged, remove update the series value to use that structure

View File

@ -42,9 +42,7 @@ interface LineChartLivePreviewProps {
series: LivePreviewSeries[]
}
export const LineChartLivePreview: React.FunctionComponent<
React.PropsWithChildren<LineChartLivePreviewProps>
> = props => {
export const LineChartLivePreview: React.FunctionComponent<LineChartLivePreviewProps> = props => {
const { disabled, repositories, stepValue, step, series, isAllReposMode, className } = props
const { getInsightPreviewContent: getLivePreviewContent } = useContext(CodeInsightsBackendContext)
const seriesToggleState = useSeriesToggle()

View File

@ -35,9 +35,7 @@ export interface LangStatsInsightLivePreviewProps {
* Displays live preview chart for creation UI with the latest insights settings
* from creation UI form.
*/
export const LangStatsInsightLivePreview: React.FunctionComponent<
React.PropsWithChildren<LangStatsInsightLivePreviewProps>
> = props => {
export const LangStatsInsightLivePreview: React.FunctionComponent<LangStatsInsightLivePreviewProps> = props => {
const { repository = '', threshold, disabled = false, className } = props
const { getLangStatsInsightContent } = useContext(CodeInsightsBackendContext)

View File

@ -7,12 +7,14 @@ import '@sourcegraph/shared/src/polyfills'
import '../monitoring/initMonitoring'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { EnterpriseWebApp } from './EnterpriseWebApp'
// It's important to have a root component in a separate file to create a react-refresh boundary and avoid page reload.
// https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#edits-always-lead-to-full-reload
window.addEventListener('DOMContentLoaded', () => {
render(<EnterpriseWebApp />, document.querySelector('#root'))
const root = createRoot(document.querySelector('#root')!)
root.render(<EnterpriseWebApp />)
})

View File

@ -1,5 +1,7 @@
import { FunctionComponent, useCallback, useState } from 'react'
import { RouteComponentProps } from 'react-router'
import { Form } from '@sourcegraph/branded/src/components/Form'
import { gql, useLazyQuery, useMutation } from '@sourcegraph/http-client'
import { IFeatureFlagOverride } from '@sourcegraph/shared/src/schema'
@ -41,6 +43,9 @@ export const CREATE_FEATURE_FLAG_OVERRIDE = gql`
}
}
`
export interface EarlyAccessOrgsCodeFormProps extends RouteComponentProps<{}> {}
/**
* Form that sets a feature flag override for org-code flag, based on organization name.
* This enables 2 screens - organization code host connections and organization repositories.
@ -51,7 +56,7 @@ export const CREATE_FEATURE_FLAG_OVERRIDE = gql`
* This implementation is a quick hack for making our lives easier while in early access
* stage. IMO it's not worth a lot of effort as it is throwaway work once we are in GA.
*/
export const EarlyAccessOrgsCodeForm: FunctionComponent<any> = () => {
export const EarlyAccessOrgsCodeForm: FunctionComponent<EarlyAccessOrgsCodeFormProps> = () => {
const [name, setName] = useState<string>('')
const [updateFeatureFlag, { data, loading: flagLoading, error: flagError }] = useMutation<

View File

@ -8,16 +8,16 @@ import styles from './ProductCertificate.module.scss'
interface Props {
/** The title of the certificate. */
title: React.ReactFragment
title: React.ReactNode
/** The subtitle of the certificate. */
subtitle?: React.ReactFragment | null
subtitle?: React.ReactNode
/** The detail text of the certificate. */
detail?: React.ReactFragment | null
detail?: React.ReactNode
/** Rendered after the certificate body (usually consists of a Wildcard <CardFooter />). */
footer?: React.ReactFragment | null
footer?: React.ReactNode
className?: string
}

View File

@ -180,7 +180,7 @@ export const SearchContextForm: React.FunctionComponent<React.PropsWithChildren<
const [hasRepositoriesConfigChanged, setHasRepositoriesConfigChanged] = useState(false)
const [repositoriesConfig, setRepositoriesConfig] = useState('')
const onRepositoriesConfigChange = useCallback(
(config, isInitialValue) => {
(config: string, isInitialValue?: boolean) => {
setRepositoriesConfig(config)
if (!isInitialValue && config !== repositoriesConfig) {
setHasRepositoriesConfigChanged(true)

View File

@ -80,7 +80,7 @@ interface Props extends ThemeProps {
primaryButtonTextNoPaymentRequired?: string
/** A fragment to render below the form's primary button. */
afterPrimaryButton?: React.ReactFragment
afterPrimaryButton?: React.ReactNode
history: H.History
}

View File

@ -149,12 +149,16 @@ export class ExternalAccountNode extends React.PureComponent<ExternalAccountNode
</small>
</div>
<div className="text-nowrap">
{this.props.node.accountData && (
{/*
* Issue: This JSX tag's 'children' prop expects a single child of type 'ReactNode', but multiple children were provided
* It seems that v18 requires explicit boolean value
*/}
{!!this.props.node.accountData && (
<Button onClick={this.toggleShowData} variant="secondary">
{this.state.showData ? 'Hide' : 'Show'} data
</Button>
)}{' '}
{this.props.node.refreshURL && (
{!!this.props.node.refreshURL && (
<Button href={this.props.node.refreshURL} variant="secondary" as="a">
Refresh
</Button>

View File

@ -24,7 +24,7 @@ interface ContributionGroup {
title: string
error?: ErrorLike
columnHeaders: string[]
rows: (React.ReactFragment | null)[][]
rows: React.ReactNode[][]
}
const ContributionsTable: React.FunctionComponent<

View File

@ -49,7 +49,7 @@ const FeatureFlagsLocalOverrideAgent = React.memo(() => {
})
const MINUTE = 60000
export const FeatureFlagsProvider: React.FunctionComponent<FeatureFlagsProviderProps> = ({
export const FeatureFlagsProvider: React.FunctionComponent<React.PropsWithChildren<FeatureFlagsProviderProps>> = ({
isLocalOverrideEnabled = true,
children,
}) => {
@ -77,11 +77,9 @@ interface MockedFeatureFlagsProviderProps {
* <ComponentUsingFeatureFlag />
* </MockedFeatureFlagsProvider>)
*/
export const MockedFeatureFlagsProvider: React.FunctionComponent<MockedFeatureFlagsProviderProps> = ({
overrides,
refetchInterval,
children,
}) => {
export const MockedFeatureFlagsProvider: React.FunctionComponent<
React.PropsWithChildren<MockedFeatureFlagsProviderProps>
> = ({ overrides, refetchInterval, children }) => {
const mockRequestGraphQL = useMemo(
() => (
query: string,

View File

@ -1,4 +1,6 @@
import { renderHook } from '@testing-library/react-hooks'
import React from 'react'
import { renderHook, waitFor } from '@testing-library/react'
import { FeatureFlagName } from './featureFlags'
import { MockedFeatureFlagsProvider } from './FeatureFlagsProvider'
@ -9,25 +11,49 @@ describe('useFeatureFlag', () => {
const DISABLED_FLAG = 'disabled-flag' as FeatureFlagName
const ERROR_FLAG = 'error-flag' as FeatureFlagName
const NON_EXISTING_FLAG = 'non-existing-flag' as FeatureFlagName
const setup = (initialFlagName: FeatureFlagName, defaultValue = false, refetchInterval?: number) =>
renderHook(({ flagName }) => useFeatureFlag(flagName, defaultValue), {
wrapper: function Wrapper({ children, overrides }) {
return (
<MockedFeatureFlagsProvider
overrides={overrides as Partial<Record<FeatureFlagName, boolean>>}
refetchInterval={refetchInterval}
>
{children}
</MockedFeatureFlagsProvider>
)
},
const Wrapper: React.JSXElementConstructor<{
children: React.ReactElement
nextOverrides?: Partial<Record<FeatureFlagName, boolean | Error>>
refetchInterval?: number
}> = ({ nextOverrides, children, refetchInterval }) => {
// New `renderHook` doesn't pass any props into Wrapper component like the old one
// so couldn't find a way to reproduce `state.setRender(...)` with custom `overrides`
// we have to use this state together with `nextOverrides` as an alternative.
const [overrides, setOverrides] = React.useState({
[ENABLED_FLAG]: true,
[DISABLED_FLAG]: false,
[ERROR_FLAG]: new Error('Some error'),
})
React.useEffect(() => {
setTimeout(() => {
setOverrides(current => nextOverrides ?? current)
}, refetchInterval)
}, [nextOverrides, refetchInterval])
return (
<MockedFeatureFlagsProvider overrides={overrides} refetchInterval={refetchInterval}>
{children}
</MockedFeatureFlagsProvider>
)
}
const setup = (
initialFlagName: FeatureFlagName,
defaultValue = false,
refetchInterval?: number,
nextOverrides?: Partial<Record<FeatureFlagName, boolean | Error>>
) =>
renderHook<
ReturnType<typeof useFeatureFlag>,
{
flagName: FeatureFlagName
}
>(({ flagName }) => useFeatureFlag(flagName, defaultValue), {
wrapper: props => <Wrapper refetchInterval={refetchInterval} nextOverrides={nextOverrides} {...props} />,
initialProps: {
flagName: initialFlagName,
overrides: {
[ENABLED_FLAG]: true,
[DISABLED_FLAG]: false,
[ERROR_FLAG]: new Error('Some error'),
},
},
})
@ -37,20 +63,19 @@ describe('useFeatureFlag', () => {
expect(state.result.current).toStrictEqual([false, 'initial', undefined])
// Loaded state
await state.waitForNextUpdate()
expect(state.result.current).toStrictEqual([false, 'loaded', undefined])
await waitFor(() => expect(state.result.current).toStrictEqual([false, 'loaded', undefined]))
})
it('returns [defaultValue=true] correctly', async () => {
const state = setup(NON_EXISTING_FLAG, true)
await state.waitForNextUpdate()
expect(state.result.current).toEqual(expect.arrayContaining([true, 'loaded']))
await waitFor(() => expect(state.result.current).toEqual(expect.arrayContaining([true, 'loaded'])))
})
it('returns [defaultValue=false] correctly', async () => {
const state = setup(NON_EXISTING_FLAG, false)
await state.waitForNextUpdate()
expect(state.result.current).toEqual(expect.arrayContaining([false, 'loaded']))
await waitFor(() => expect(state.result.current).toEqual(expect.arrayContaining([false, 'loaded'])))
})
it('returns [true] value correctly', async () => {
@ -59,51 +84,44 @@ describe('useFeatureFlag', () => {
expect(state.result.current).toStrictEqual([false, 'initial', undefined])
// Loaded state
await state.waitForNextUpdate()
expect(state.result.current).toStrictEqual([true, 'loaded', undefined])
expect(state.result.all.length).toBe(2)
await waitFor(() => expect(state.result.current).toStrictEqual([true, 'loaded', undefined]))
})
it('updates on value change', async () => {
const state = setup(ENABLED_FLAG, false, 100)
const state = setup(ENABLED_FLAG, false, 100, { [ENABLED_FLAG]: false })
// Initial state
expect(state.result.current).toStrictEqual([false, 'initial', undefined])
// Loaded state
await state.waitForNextUpdate()
expect(state.result.current).toStrictEqual([true, 'loaded', undefined])
await waitFor(() => expect(state.result.current).toStrictEqual([true, 'loaded', undefined]))
// Rerender and wait for new state
state.rerender({ overrides: { [ENABLED_FLAG]: false }, flagName: ENABLED_FLAG })
await state.waitForNextUpdate()
expect(state.result.current).toStrictEqual([false, 'loaded', undefined])
state.rerender({ flagName: ENABLED_FLAG })
await waitFor(() => expect(state.result.current).toStrictEqual([false, 'loaded', undefined]))
})
it('updates when feature flag prop changes', async () => {
const state = setup(ENABLED_FLAG)
const state = setup(ENABLED_FLAG, false, undefined, {})
// Initial state
expect(state.result.all[0]).toStrictEqual([false, 'initial', undefined])
expect(state.result.current).toStrictEqual([false, 'initial', undefined])
// Loaded state
await state.waitForNextUpdate()
expect(state.result.current).toStrictEqual([true, 'loaded', undefined])
await waitFor(() => expect(state.result.current).toStrictEqual([true, 'loaded', undefined]))
// Rerender and wait for new state
state.rerender({ overrides: {}, flagName: DISABLED_FLAG })
await state.waitForNextUpdate()
expect(state.result.current).toStrictEqual([false, 'loaded', undefined])
state.rerender({ flagName: DISABLED_FLAG })
await waitFor(() => expect(state.result.current).toStrictEqual([false, 'loaded', undefined]))
})
it('returns "error" when no context parent', () => {
const state = renderHook(() => useFeatureFlag(ENABLED_FLAG))
// Initial state
expect(state.result.all[0]).toStrictEqual([false, 'initial', undefined])
// Loaded state
expect(state.result.current).toEqual(expect.arrayContaining([false, 'error']))
})
it('returns "error" when unhandled error', async () => {
const state = setup(ERROR_FLAG)
await state.waitForNextUpdate()
expect(state.result.current).toEqual(expect.arrayContaining([false, 'error']))
await waitFor(() => expect(state.result.current).toEqual(expect.arrayContaining([false, 'error'])))
})
})

View File

@ -583,6 +583,7 @@ describe('Batches', () => {
// TODO: SSBC has to go through accessibility audits before this can pass.
it.skip('is styled correctly', async () => {
await driver.page.goto(driver.sourcegraphBaseUrl + '/batch-changes/create')
await driver.page.waitForSelector('[data-testid="batch-spec-yaml-file"]')
await percySnapshotWithVariants(driver.page, 'Create batch change')
await accessibilityAudit(driver.page)
})

View File

@ -72,9 +72,9 @@ describe('Blob viewer', () => {
describe('general layout for viewing a file', () => {
it('populates editor content and FILES tab', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/${fileName}`)
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
const blobContent = await driver.page.evaluate(
() => document.querySelector<HTMLElement>('.test-repo-blob')?.textContent
() => document.querySelector<HTMLElement>('[data-testid="repo-blob"]')?.textContent
)
// editor shows the return string content from Blob request
@ -144,13 +144,13 @@ describe('Blob viewer', () => {
it('should redirect from line number hash to query parameter', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/${fileName}#2`)
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.assertWindowLocation(`/${repositoryName}/-/blob/${fileName}?L2`)
})
it('should redirect from line range hash to query parameter', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/${fileName}#1-3`)
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.assertWindowLocation(`/${repositoryName}/-/blob/${fileName}?L1-3`)
})
})
@ -271,7 +271,7 @@ describe('Blob viewer', () => {
await driver.page.goto(
`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/this_is_a_long_file_path/apps/rest-showcase/src/main/java/org/demo/rest/example/OrdersController.java`
)
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.page.waitForSelector('.test-breadcrumb')
// Uncomment this snapshot once https://github.com/sourcegraph/sourcegraph/issues/15126 is resolved
// await percySnapshot(driver.page, this.test!.fullTitle())
@ -279,7 +279,7 @@ describe('Blob viewer', () => {
it.skip('shows a hover overlay from a hover provider when a token is hovered', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/${fileName}`)
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
// TODO
})
@ -513,7 +513,7 @@ describe('Blob viewer', () => {
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
})
}
const timeout = 5000
const timeout = 10000
await driver.page.goto(`${driver.sourcegraphBaseUrl}/github.com/sourcegraph/test/-/blob/test.ts`)
// Wait for some line decoration attachment portal
@ -785,6 +785,7 @@ describe('Blob viewer', () => {
const timeout = 5000
await driver.page.goto(`${driver.sourcegraphBaseUrl}/github.com/sourcegraph/test/-/blob/test.ts`)
await driver.page.waitForSelector('[data-testid="repo-blob"]')
// File 1 (test.ts). Only line two contains 'word'
try {
@ -1072,7 +1073,7 @@ describe('Blob viewer', () => {
await driver.page.waitForFunction(
() =>
document
.querySelector('.test-repo-blob [data-line="1"]')
.querySelector('[data-testid="repo-blob"] [data-line="1"]')
?.nextElementSibling?.textContent?.includes('file path: test spaces.ts'),
{ timeout: 5000 }
)

View File

@ -87,6 +87,8 @@ describe('GlobalNavbar', () => {
test('is highlighted on search page', async () => {
await driver.page.goto(driver.sourcegraphBaseUrl + '/search?q=test&patternType=regexp')
await driver.page.waitForSelector('[data-test-id="/search"]')
await driver.page.waitForSelector('[data-test-active="true"]')
const active = await driver.page.evaluate(() =>
document.querySelector('[data-test-id="/search"]')?.getAttribute('data-test-active')
@ -97,6 +99,8 @@ describe('GlobalNavbar', () => {
test('is highlighted on repo page', async () => {
await driver.page.goto(driver.sourcegraphBaseUrl + '/github.com/sourcegraph/sourcegraph')
await driver.page.waitForSelector('[data-test-id="/search"]')
await driver.page.waitForSelector('[data-test-active="true"]')
const active = await driver.page.evaluate(() =>
document.querySelector('[data-test-id="/search"]')?.getAttribute('data-test-active')
@ -107,6 +111,8 @@ describe('GlobalNavbar', () => {
test('is highlighted on repo file page', async () => {
await driver.page.goto(driver.sourcegraphBaseUrl + '/github.com/sourcegraph/sourcegraph/-/blob/README.md')
await driver.page.waitForSelector('[data-test-id="/search"]')
await driver.page.waitForSelector('[data-test-active="true"]')
const active = await driver.page.evaluate(() =>
document.querySelector('[data-test-id="/search"]')?.getAttribute('data-test-active')
@ -117,6 +123,8 @@ describe('GlobalNavbar', () => {
test('is not highlighted on notebook page', async () => {
await driver.page.goto(driver.sourcegraphBaseUrl + '/notebooks/id')
await driver.page.waitForSelector('[data-test-id="/search"]')
await driver.page.waitForSelector('[data-test-active="false"]')
const active = await driver.page.evaluate(() =>
document.querySelector('[data-test-id="/search"]')?.getAttribute('data-test-active')

View File

@ -61,9 +61,9 @@ describe('User profile page', () => {
UpdateUser: () => ({ updateUser: { ...USER, displayName: 'Test2' } }),
})
await driver.page.goto(driver.sourcegraphBaseUrl + '/users/test/settings/profile')
await driver.page.waitForSelector('[data-testid="user-profile-form-fields"]')
await percySnapshotWithVariants(driver.page, 'User Profile Settings Page')
await accessibilityAudit(driver.page)
await driver.page.waitForSelector('[data-testid="user-profile-form-fields"]')
await driver.replaceText({
selector: '[data-testid="test-UserProfileFormFields__displayName"]',
newText: 'Test2',

View File

@ -82,6 +82,7 @@ describe('Repository', () => {
const shortRepositoryName = 'sourcegraph/jsonrpc2'
const repositoryName = `github.com/${shortRepositoryName}`
const repositorySourcegraphUrl = `/${repositoryName}`
const commitUrl = `${repositorySourcegraphUrl}/-/commit/15c2290dcb37731cc4ee5a2a1c1e5a25b4c28f81?visible=1`
const clickedFileName = 'async.go'
const clickedCommit = ''
const fileEntries = ['jsonrpc2.go', clickedFileName]
@ -136,10 +137,8 @@ describe('Repository', () => {
'/github.com/sourcegraph/jsonrpc2/-/commit/9e615b1c32cc519130575e8d10d0d0fee8a5eb6c',
},
],
url:
'/github.com/sourcegraph/jsonrpc2/-/commit/15c2290dcb37731cc4ee5a2a1c1e5a25b4c28f81',
canonicalURL:
'/github.com/sourcegraph/jsonrpc2/-/commit/15c2290dcb37731cc4ee5a2a1c1e5a25b4c28f81',
url: commitUrl,
canonicalURL: commitUrl,
externalURLs: [
{
url:
@ -312,9 +311,8 @@ describe('Repository', () => {
'/github.com/sourcegraph/jsonrpc2/-/commit/9e615b1c32cc519130575e8d10d0d0fee8a5eb6c',
},
],
url: '/github.com/sourcegraph/jsonrpc2/-/commit/15c2290dcb37731cc4ee5a2a1c1e5a25b4c28f81',
canonicalURL:
'/github.com/sourcegraph/jsonrpc2/-/commit/15c2290dcb37731cc4ee5a2a1c1e5a25b4c28f81',
url: commitUrl,
canonicalURL: commitUrl,
externalURLs: [
{
url:
@ -414,7 +412,7 @@ describe('Repository', () => {
})
}, 'Blob')
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.assertWindowLocation(`/${repositoryName}/-/blob/${clickedFileName}`)
// Assert breadcrumb order
@ -436,7 +434,10 @@ describe('Repository', () => {
selector: '[data-testid="git-commit-node-oid"]',
action: 'click',
})
await driver.page.waitForSelector('[data-testid="repository-commit-page"]')
await driver.page.waitForSelector('[data-testid="git-commit-node-message-subject"]')
await driver.assertWindowLocation(commitUrl)
await assertSelectorHasText(
'[data-testid="git-commit-node-message-subject"]',
'update LSIF indexing CI workflow'
@ -494,7 +495,7 @@ describe('Repository', () => {
// page.click() fails for some reason with Error: Node is either not visible or not an HTMLElement
await driver.page.$eval('.test-tree-file-link', linkElement => (linkElement as HTMLElement).click())
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.page.waitForSelector('.test-breadcrumb')
const breadcrumbTexts = await driver.page.evaluate(() =>
@ -522,7 +523,9 @@ describe('Repository', () => {
"https://github.com/ggilmore/q-test/blob/master/Geoffrey's%20random%20queries.32r242442bf/%25%20token.4288249258.sql"
)
const blobContent = await driver.page.evaluate(() => document.querySelector('.test-repo-blob')?.textContent)
const blobContent = await driver.page.evaluate(
() => document.querySelector('[data-testid="repo-blob"]')?.textContent
)
assert.strictEqual(blobContent, `content for: ${filePath}\nsecond line\nthird line`)
})
@ -549,7 +552,7 @@ describe('Repository', () => {
// page.click() fails for some reason with Error: Node is either not visible or not an HTMLElement
await driver.page.$eval('.test-tree-file-link', linkElement => (linkElement as HTMLElement).click())
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.page.waitForSelector('.test-breadcrumb')
const breadcrumbTexts = await driver.page.evaluate(() =>
@ -576,7 +579,7 @@ describe('Repository', () => {
// page.click() fails for some reason with Error: Node is either not visible or not an HTMLElement
await driver.page.$eval('.test-tree-file-link', linkElement => (linkElement as HTMLElement).click())
await driver.page.waitForSelector('.test-repo-blob')
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.page.waitForSelector('.test-breadcrumb')
const breadcrumbTexts = await driver.page.evaluate(() =>
@ -1047,7 +1050,7 @@ describe('Repository', () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repoName}`)
try {
await driver.page.waitForSelector('.test-file-decoration-container', { timeout: 5000 })
await driver.page.waitForSelector('.test-file-decoration-container', { timeout: 10000 })
} catch {
throw new Error('Expected to see file decorations')
}

View File

@ -235,6 +235,8 @@ describe('Search', () => {
test('Is set from the URL query parameter when loading a search-related page', async () => {
await driver.page.goto(driver.sourcegraphBaseUrl + '/search?q=foo')
const editor = await createEditorAPI(driver, queryInputSelector)
await editor.waitForIt()
await driver.page.waitForSelector('[data-testid="results-info-bar"]')
expect(await editor.getValue()).toStrictEqual('foo')
// Field value is cleared when navigating to a non search-related page
await driver.page.waitForSelector('a[href="/extensions"]')
@ -243,6 +245,8 @@ describe('Search', () => {
expect(await editor.getValue()).toStrictEqual(undefined)
// Field value is restored when the back button is pressed
await driver.page.goBack()
await editor.waitForIt()
await driver.page.waitForSelector('[data-testid="results-info-bar"]')
expect(await editor.getValue()).toStrictEqual('foo')
})

View File

@ -7,12 +7,14 @@ import '@sourcegraph/shared/src/polyfills'
import './monitoring/initMonitoring'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { OpenSourceWebApp } from './OpenSourceWebApp'
// It's important to have a root component in a separate file to create a react-refresh boundary and avoid page reload.
// https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#edits-always-lead-to-full-reload
window.addEventListener('DOMContentLoaded', () => {
render(<OpenSourceWebApp />, document.querySelector('#root'))
const root = createRoot(document.querySelector('#root')!)
root.render(<OpenSourceWebApp />)
})

View File

@ -77,7 +77,7 @@ interface Props
CodeInsightsProps,
BatchChangesProps {
history: H.History
location: H.Location<{ query: string }>
location: H.Location
authenticatedUser: AuthenticatedUser | null
authRequired: boolean
isSourcegraphDotCom: boolean

View File

@ -73,11 +73,9 @@ const commonProps = (): UserNavItemProps => ({
position: Position.bottomStart,
})
const OpenByDefaultWrapper: React.FunctionComponent<
React.PropsWithChildren<{
children: React.FunctionComponent<React.PropsWithChildren<{ menuButtonRef: React.Ref<HTMLButtonElement> }>>
}>
> = ({ children }) => {
const OpenByDefaultWrapper: React.FunctionComponent<{
children: React.FunctionComponent<React.PropsWithChildren<{ menuButtonRef: React.Ref<HTMLButtonElement> }>>
}> = ({ children }) => {
const menuButtonReference = useRef<HTMLButtonElement>(null)
useEffect(() => {

View File

@ -113,6 +113,7 @@ export interface BlobProps
wrapCode: boolean
/** The current text document to be rendered and provided to extensions */
blobInfo: BlobInfo
'data-testid'?: string
// Experimental reference panel
disableStatusBar: boolean
@ -196,7 +197,15 @@ const STATUS_BAR_VERTICAL_GAP_VAR = '--blob-status-bar-vertical-gap'
* in this state, hovers can lead to errors like `DocumentNotFoundError`.
*/
export const Blob: React.FunctionComponent<React.PropsWithChildren<BlobProps>> = props => {
const { location, isLightTheme, extensionsController, blobInfo, platformContext, settingsCascade } = props
const {
location,
isLightTheme,
extensionsController,
blobInfo,
platformContext,
settingsCascade,
'data-testid': dataTestId,
} = props
const settingsChanges = useMemo(() => new BehaviorSubject<Settings | null>(null), [])
useEffect(() => {
@ -783,7 +792,12 @@ export const Blob: React.FunctionComponent<React.PropsWithChildren<BlobProps>> =
return (
<>
<div className={classNames(props.className, styles.blob)} ref={nextBlobElement} tabIndex={-1}>
<div
data-testid={dataTestId}
className={classNames(props.className, styles.blob)}
ref={nextBlobElement}
tabIndex={-1}
>
<Code
className={classNames('test-blob', styles.blobCode, props.wrapCode && styles.blobCodeWrapped)}
ref={nextCodeViewElement}

View File

@ -365,7 +365,8 @@ export const BlobPage: React.FunctionComponent<React.PropsWithChildren<Props>> =
{/* Render the (unhighlighted) blob also in the case highlighting timed out */}
{renderMode === 'code' && (
<Blob
className={classNames('test-repo-blob', styles.blob, styles.border)}
data-testid="repo-blob"
className={classNames(styles.blob, styles.border)}
blobInfo={blobInfoOrError}
wrapCode={wrapCode}
platformContext={props.platformContext}

View File

@ -88,8 +88,9 @@ const queryCommit = memoizeObservable(
}
if (!data.node.commit) {
// Filter out any revision not found errors, they usually come in multiples when searching for a commit, we want to replace all of them with 1 "Commit not found" error
// TODO: Figuring why should we use `errors` here since it is `undefined` in this place
const errorsWithoutRevisionError = errors?.filter(
error => !error.message.includes('revision not found')
(error: { message: string | string[] }) => !error.message.includes('revision not found')
)
const revisionErrorsFiltered =
@ -258,7 +259,11 @@ export class RepositoryCommitPage extends React.Component<Props, State> {
public render(): JSX.Element | null {
return (
<div className={classNames('p-3', styles.repositoryCommitPage)} ref={this.nextRepositoryCommitPageElement}>
<div
data-testid="repository-commit-page"
className={classNames('p-3', styles.repositoryCommitPage)}
ref={this.nextRepositoryCommitPageElement}
>
<PageTitle
title={
this.state.commitOrError && !isErrorLike(this.state.commitOrError)

View File

@ -51,7 +51,7 @@ export interface GitCommitNodeProps {
preferAbsoluteTimestamps?: boolean
/** Fragment to show at the end to the right of the SHA. */
afterElement?: React.ReactFragment
afterElement?: React.ReactNode
/** Determine the git diff visualization UI */
diffMode?: DiffMode
@ -99,7 +99,7 @@ export const GitCommitNode: React.FunctionComponent<React.PropsWithChildren<GitC
DeprecatedTooltipController.forceUpdate()
}, [flashCopiedToClipboardMessage])
const copyToClipboard = useCallback((oid): void => {
const copyToClipboard = useCallback((oid: string): void => {
eventLogger.log('CommitSHACopiedToClipboard')
copy(oid)
setFlashCopiedToClipboardMessage(true)

View File

@ -66,9 +66,9 @@ class UpdateMirrorRepositoryActionContainer extends React.PureComponent<UpdateMi
}
public render(): JSX.Element | null {
let title: React.ReactFragment
let description: React.ReactFragment
let buttonLabel: React.ReactFragment
let title: React.ReactNode
let description: React.ReactNode
let buttonLabel: React.ReactNode
let buttonDisabled = false
let info: React.ReactNode
if (this.props.repo.mirrorInfo.cloneInProgress) {

View File

@ -11,10 +11,10 @@ import styles from './ActionContainer.module.scss'
export const BaseActionContainer: React.FunctionComponent<
React.PropsWithChildren<{
title: React.ReactFragment
description: React.ReactFragment
action: React.ReactFragment
details?: React.ReactFragment
title: React.ReactNode
description: React.ReactNode
action: React.ReactNode
details?: React.ReactNode
className?: string
}>
> = ({ title, description, action, details, className }) => (
@ -31,10 +31,10 @@ export const BaseActionContainer: React.FunctionComponent<
)
interface Props {
title: React.ReactFragment
description: React.ReactFragment
title: React.ReactNode
description: React.ReactNode
buttonClassName?: string
buttonLabel: React.ReactFragment
buttonLabel: React.ReactNode
buttonSubtitle?: string
buttonDisabled?: boolean
info?: React.ReactNode

View File

@ -3,7 +3,7 @@ import * as React from 'react'
import { mdiMessageTextOutline, mdiCog, mdiDelete, mdiPlus } from '@mdi/js'
import { VisuallyHidden } from '@reach/visually-hidden'
import classNames from 'classnames'
import { RouteComponentProps } from 'react-router'
import { RouteComponentProps, useLocation } from 'react-router'
import { Subject, Subscription } from 'rxjs'
import { catchError, map, mapTo, startWith, switchMap } from 'rxjs/operators'
import { useCallbackRef } from 'use-callback-ref'
@ -23,7 +23,7 @@ import { eventLogger } from '../tracking/eventLogger'
import styles from './SavedSearchListPage.module.scss'
interface NodeProps extends RouteComponentProps<{}, {}, { description?: string }>, SearchPatternTypeProps {
interface NodeProps extends RouteComponentProps, SearchPatternTypeProps {
savedSearch: GQL.ISavedSearch
onDelete: () => void
linkRef: React.MutableRefObject<HTMLAnchorElement | null> | null
@ -126,7 +126,7 @@ interface State {
savedSearchesOrError?: GQL.ISavedSearch[] | ErrorLike
}
interface Props extends RouteComponentProps<{}, {}, { description?: string }>, NamespaceProps {}
interface Props extends RouteComponentProps, NamespaceProps {}
export class SavedSearchListPage extends React.Component<Props, State> {
public subscriptions = new Subscription()
@ -188,6 +188,7 @@ const SavedSearchListPageContent: React.FunctionComponent<React.PropsWithChildre
savedSearchesOrError,
...props
}) => {
const location = useLocation<{ description?: string }>()
const searchPatternType = useNavbarQueryState(state => state.searchPatternType)
const callbackReference = useCallbackRef<HTMLAnchorElement>(null, ref => ref?.focus())
@ -210,7 +211,7 @@ const SavedSearchListPageContent: React.FunctionComponent<React.PropsWithChildre
{namespaceSavedSearches.map(search => (
<SavedSearchNode
key={search.id}
linkRef={props.location.state?.description === search.description ? callbackReference : null}
linkRef={location.state?.description === search.description ? callbackReference : null}
{...props}
patternType={searchPatternType}
savedSearch={search}

View File

@ -48,7 +48,7 @@ export function submitSearch({
},
{ source }
)
history.push(path, { ...history.location.state, query })
history.push(path, { ...(typeof history.location.state === 'object' ? history.location.state : null), query })
if (activation) {
activation.update({ DidSearch: true })
}

View File

@ -47,7 +47,7 @@ export interface SiteAdminAreaRouteContext
isSourcegraphDotCom: boolean
/** This property is only used by {@link SiteAdminOverviewPage}. */
overviewComponents: readonly React.ComponentType<React.PropsWithChildren<unknown>>[]
overviewComponents: readonly React.ComponentType<React.PropsWithChildren<{}>>[]
}
export interface SiteAdminAreaRoute extends RouteDescriptor<SiteAdminAreaRouteContext> {}

View File

@ -451,7 +451,7 @@ const FeatureFlagOverrideItem: FunctionComponent<
const nsValue = orgID > 0 ? orgID : userID
const onError = useCallback(
error => {
(error: Error) => {
setError(error)
},
[setError]

View File

@ -4,7 +4,7 @@ import { mdiChartLineVariant } from '@mdi/js'
import { Badge, H1, Icon } from '@sourcegraph/wildcard'
export const AnalyticsPageTitle: React.FunctionComponent = ({ children }) => (
export const AnalyticsPageTitle: React.FunctionComponent<React.PropsWithChildren<{}>> = ({ children }) => (
<div className="d-flex flex-column justify-content-between align-items-start">
<Badge variant="merged">Experimental</Badge>

View File

@ -1,4 +1,4 @@
import { act } from '@testing-library/react-hooks'
import { act } from '@testing-library/react'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'

View File

@ -1,6 +1,6 @@
// causes false positive on act()
/* eslint-disable @typescript-eslint/no-floating-promises */
import { act, renderHook } from '@testing-library/react-hooks'
import { renderHook, act } from '@testing-library/react'
import { useThemeState } from './stores'
import { ThemePreference } from './stores/themeState'

View File

@ -23,7 +23,7 @@ interface TourContentProps {
className?: string
}
const Header: React.FunctionComponent<{ onClose: () => void; title?: string }> = ({
const Header: React.FunctionComponent<React.PropsWithChildren<{ onClose: () => void; title?: string }>> = ({
children,
onClose,
title = 'Quick start',
@ -36,7 +36,7 @@ const Header: React.FunctionComponent<{ onClose: () => void; title?: string }> =
</div>
)
const Footer: React.FunctionComponent<{ completedCount: number; totalCount: number }> = ({
const Footer: React.FunctionComponent<React.PropsWithChildren<{ completedCount: number; totalCount: number }>> = ({
completedCount,
totalCount,
}) => (
@ -50,7 +50,7 @@ const Footer: React.FunctionComponent<{ completedCount: number; totalCount: numb
</Text>
)
const CompletedItem: React.FunctionComponent = ({ children }) => (
const CompletedItem: React.FunctionComponent<React.PropsWithChildren<{}>> = ({ children }) => (
<li className="d-flex align-items-start">
<Icon
size="sm"
@ -62,7 +62,7 @@ const CompletedItem: React.FunctionComponent = ({ children }) => (
</li>
)
export const TourContent: React.FunctionComponent<TourContentProps> = ({
export const TourContent: React.FunctionComponent<React.PropsWithChildren<TourContentProps>> = ({
onClose,
tasks,
variant,

View File

@ -1,5 +1,5 @@
import { cleanup } from '@testing-library/react'
import { renderHook, WrapperComponent, act } from '@testing-library/react-hooks'
import { renderHook, cleanup, act } from '@testing-library/react'
import { WrapperComponent } from '@testing-library/react-hooks'
import { TemporarySettings } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings'
import { MockTemporarySettings } from '@sourcegraph/shared/src/settings/temporary/testUtils'
@ -20,7 +20,7 @@ const getFieldsAsObject = (value: object): object =>
const TourId = 'MockTour'
const setup = (settings: TemporarySettings['onboarding.quickStartTour'] = {}) => {
const wrapper: WrapperComponent<{}> = ({ children }) => (
const wrapper: WrapperComponent<React.PropsWithChildren<{}>> = ({ children }) => (
<MockTemporarySettings settings={{ 'onboarding.quickStartTour': settings }}>{children}</MockTemporarySettings>
)
return renderHook(() => useTour(TourId), { wrapper })

Some files were not shown because too many files have changed in this diff Show More