From da154b5a93d7505b0fa994753851c27af385a347 Mon Sep 17 00:00:00 2001 From: GitStart-SourceGraph <89894075+gitstart-sourcegraph@users.noreply.github.com> Date: Fri, 15 Jul 2022 11:58:08 +0800 Subject: [PATCH] [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 Co-authored-by: Valery Bugakov --- .../branded/src/components/BrandedStory.tsx | 6 +- .../components/panel/TabbedPanelContent.tsx | 4 +- .../scripts/afterInstallPage.main.tsx | 6 +- .../scripts/optionsPage.main.tsx | 6 +- .../src/shared/code-hosts/github/codeHost.tsx | 8 +- .../src/shared/code-hosts/shared/codeHost.tsx | 13 +- .../shared/code-hosts/shared/extensions.tsx | 4 +- .../src/components/CodeExcerpt.test.tsx | 25 +-- .../src/components/FileMatchChildren.test.tsx | 9 +- .../src/components/FileMatchChildren.tsx | 2 +- .../src/components/FileSearchResult.test.tsx | 9 +- .../src/components/ResultContainer.tsx | 10 +- .../src/documentation/ModalVideo.tsx | 4 +- .../search-ui/src/results/NoResultsPage.tsx | 2 +- .../src/results/sidebar/SearchReference.tsx | 4 +- .../src/results/sidebar/SearchSidebar.tsx | 4 +- .../results/sidebar/SearchSidebarSection.tsx | 44 ++--- .../src/results/sidebar/helpers.test.ts | 2 +- .../src/results/use-items-to-show.test.ts | 2 +- .../shared/dev/mockReactVisibilitySensor.tsx | 27 +++ client/shared/src/actions/ActionItem.test.tsx | 5 +- .../shared/src/actions/ActionsContainer.tsx | 2 +- .../__snapshots__/ActionItem.test.tsx.snap | 14 +- client/shared/src/components/VirtualList.tsx | 2 +- .../utils/linkClickHandler.test.tsx | 20 +- .../temporary/useTemporarySetting.test.tsx | 3 +- .../testing/render-with-branded-context.tsx | 7 +- .../src/util/useInputValidation.test.ts | 2 +- .../webview/search-panel/SearchHomeView.tsx | 2 +- .../search-panel/SearchResultsView.tsx | 2 +- .../search-panel/alias/FileMatchChildren.tsx | 2 +- .../vscode/src/webview/search-panel/index.tsx | 9 +- .../src/webview/sidebars/help/index.tsx | 6 +- .../src/webview/sidebars/search/index.tsx | 9 +- client/web/src/SourcegraphWebApp.tsx | 7 +- .../core/components/tooltip/Tooltip.tsx | 2 +- client/web/src/components/Collapsible.tsx | 4 +- client/web/src/components/ErrorBoundary.tsx | 2 +- client/web/src/components/PageTitle.tsx | 2 +- client/web/src/components/WebStory.tsx | 10 +- .../components/diff/FileDiffConnection.tsx | 2 +- .../web/src/components/diff/FileDiffNode.tsx | 4 +- .../externalServices/externalServices.tsx | 8 +- .../web/src/enterprise/batches/BatchSpec.tsx | 15 +- .../WorkspacesPreview.story.tsx | 32 ++-- .../batch-spec/execute/ActionsMenu.story.tsx | 16 +- .../batch-spec/execute/ActionsMenu.tsx | 2 +- .../create/OldCreateBatchChangeContent.tsx | 2 +- .../create/SearchTemplatesBanner.story.tsx | 4 +- .../components/DeleteMonitorModal.tsx | 6 +- .../pages/CodeIntelConfigurationPage.tsx | 7 +- .../CodeIntelConfigurationPolicyPage.tsx | 7 +- .../indexes/pages/CodeIntelIndexesPage.tsx | 14 +- .../uploads/pages/CodeIntelUploadsPage.tsx | 11 +- client/web/src/enterprise/embed/main.tsx | 6 +- .../CodeInsightsCreationActions.tsx | 2 +- .../live-preview/LivePreviewCard.tsx | 2 +- .../components/form/getDefaultInputProps.ts | 2 +- .../SmartInsightsViewGrid.tsx | 4 +- .../SortFilterSeriesPanel.tsx | 7 +- .../use-prallel-requests.test.ts | 2 +- .../insights/hooks/use-ui-features.test.tsx | 2 +- .../InsightsDashboardCreationContent.tsx | 4 +- .../DashboardsContentPage.test.tsx | 4 +- .../insights/creation/ComputeLivePreview.tsx | 2 +- .../creation/LineChartLivePreview.tsx | 4 +- .../LangStatsInsightLivePreview.tsx | 4 +- client/web/src/enterprise/main.tsx | 6 +- .../organizations/EarlyAccessOrgsCodeForm.tsx | 7 +- .../ProductCertificate.tsx | 8 +- .../searchContexts/SearchContextForm.tsx | 2 +- .../ProductSubscriptionForm.tsx | 2 +- .../user/settings/ExternalAccountNode.tsx | 8 +- .../RegistryExtensionContributionsPage.tsx | 2 +- .../src/featureFlags/FeatureFlagsProvider.tsx | 10 +- .../src/featureFlags/useFeatureFlag.test.tsx | 108 ++++++----- client/web/src/integration/batches.test.ts | 1 + .../web/src/integration/blob-viewer.test.ts | 17 +- client/web/src/integration/nav.test.ts | 8 + client/web/src/integration/profile.test.ts | 2 +- client/web/src/integration/repository.test.ts | 29 +-- client/web/src/integration/search.test.ts | 4 + client/web/src/main.tsx | 6 +- client/web/src/nav/GlobalNavbar.tsx | 2 +- client/web/src/nav/UserNavItem.story.tsx | 8 +- client/web/src/repo/blob/Blob.tsx | 18 +- client/web/src/repo/blob/BlobPage.tsx | 3 +- .../src/repo/commit/RepositoryCommitPage.tsx | 9 +- client/web/src/repo/commits/GitCommitNode.tsx | 4 +- .../repo/settings/RepoSettingsMirrorPage.tsx | 6 +- .../settings/components/ActionContainer.tsx | 14 +- .../src/savedSearches/SavedSearchListPage.tsx | 9 +- client/web/src/search/helpers.tsx | 2 +- client/web/src/site-admin/SiteAdminArea.tsx | 2 +- .../SiteAdminFeatureFlagConfigurationPage.tsx | 2 +- .../components/AnalyticsPageTitle.tsx | 2 +- client/web/src/stores/notepad.test.ts | 2 +- client/web/src/theme.test.ts | 2 +- .../src/tour/components/Tour/TourContent.tsx | 8 +- .../src/tour/components/Tour/useTour.test.tsx | 6 +- client/web/src/tracking/withActivation.tsx | 2 +- client/web/src/types/graphiql/index.d.ts | 2 +- .../src/types/react-scroll-manager/index.d.ts | 20 ++ .../codeHosts/AddCodeHostConnectionModal.tsx | 2 +- .../UpdateCodeHostConnectionModal.tsx | 2 +- .../user/settings/codeHosts/modalHints.tsx | 4 +- .../settings/profile/EditUserProfileForm.tsx | 2 +- .../src/components/Collapse/Collapse.tsx | 6 +- .../FeedbackPrompt/FeedbackPrompt.story.tsx | 2 +- .../FeedbackPrompt/FeedbackPrompt.tsx | 2 +- .../FeedbackPrompt.test.tsx.snap | 20 +- .../src/components/Tabs/Tabs.story.tsx | 7 +- .../src/components/Tooltip/Tooltip.test.tsx | 11 +- .../wildcard/src/hooks/useObservable.test.ts | 13 +- client/wildcard/src/hooks/useObservable.ts | 4 +- .../src/hooks/useTimeoutManager.test.ts | 2 +- client/wildcard/src/types.ts | 6 +- package.json | 20 +- yarn.lock | 180 ++++++------------ 119 files changed, 576 insertions(+), 528 deletions(-) create mode 100644 client/shared/dev/mockReactVisibilitySensor.tsx create mode 100644 client/web/src/types/react-scroll-manager/index.d.ts diff --git a/client/branded/src/components/BrandedStory.tsx b/client/branded/src/components/BrandedStory.tsx index 30092241462..1e87cffe982 100644 --- a/client/branded/src/components/BrandedStory.tsx +++ b/client/branded/src/components/BrandedStory.tsx @@ -11,7 +11,9 @@ import { DeprecatedTooltip, WildcardThemeContext } from '@sourcegraph/wildcard' import brandedStyles from '../global-styles/index.scss' -export interface BrandedProps extends MemoryRouterProps, Pick { +export interface BrandedProps + extends Omit, + Pick { children: React.FunctionComponent> styles?: string } @@ -20,7 +22,7 @@ export interface BrandedProps extends MemoryRouterProps, Pick> = ({ +export const BrandedStory: React.FunctionComponent = ({ children: Children, styles = brandedStyles, mocks, diff --git a/client/branded/src/components/panel/TabbedPanelContent.tsx b/client/branded/src/components/panel/TabbedPanelContent.tsx index 259eb396d0a..37e88482ac8 100644 --- a/client/branded/src/components/panel/TabbedPanelContent.tsx +++ b/client/branded/src/components/panel/TabbedPanelContent.tsx @@ -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. diff --git a/client/browser/src/browser-extension/scripts/afterInstallPage.main.tsx b/client/browser/src/browser-extension/scripts/afterInstallPage.main.tsx index 9775bcfefcf..8edb2002c0b 100644 --- a/client/browser/src/browser-extension/scripts/afterInstallPage.main.tsx +++ b/client/browser/src/browser-extension/scripts/afterInstallPage.main.tsx @@ -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 ) -render(, document.querySelector('#root')) +const root = createRoot(document.querySelector('#root')!) + +root.render() diff --git a/client/browser/src/browser-extension/scripts/optionsPage.main.tsx b/client/browser/src/browser-extension/scripts/optionsPage.main.tsx index 4a6dd80a473..0732d49159b 100644 --- a/client/browser/src/browser-extension/scripts/optionsPage.main.tsx +++ b/client/browser/src/browser-extension/scripts/optionsPage.main.tsx @@ -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> = () => } const inject = (): void => { - render(, document.body) + const root = createRoot(document.body) + + root.render() } document.addEventListener('DOMContentLoaded', inject) diff --git a/client/browser/src/shared/code-hosts/github/codeHost.tsx b/client/browser/src/shared/code-hosts/github/codeHost.tsx index 7cd2ccf975f..c9091c2deb3 100644 --- a/client/browser/src/shared/code-hosts/github/codeHost.tsx +++ b/client/browser/src/shared/code-hosts/github/codeHost.tsx @@ -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( , - container + /> ) } diff --git a/client/browser/src/shared/code-hosts/shared/codeHost.tsx b/client/browser/src/shared/code-hosts/shared/codeHost.tsx index b8aabb1cf16..a1198495572 100644 --- a/client/browser/src/shared/code-hosts/shared/codeHost.tsx +++ b/client/browser/src/shared/code-hosts/shared/codeHost.tsx @@ -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({element}, container) + const renderWithThemeProvider = (element: React.ReactNode, container: Element | null): void => { + if (!container) { + return + } + + const root = createRoot(container) + root.render({element}) + } subscriptions.add( // eslint-disable-next-line rxjs/no-async-subscribe, @typescript-eslint/no-misused-promises diff --git a/client/browser/src/shared/code-hosts/shared/extensions.tsx b/client/browser/src/shared/code-hosts/shared/extensions.tsx index 8414232e8ae..c9c713ab4f5 100644 --- a/client/browser/src/shared/code-hosts/shared/extensions.tsx +++ b/client/browser/src/shared/code-hosts/shared/extensions.tsx @@ -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 diff --git a/client/search-ui/src/components/CodeExcerpt.test.tsx b/client/search-ui/src/components/CodeExcerpt.test.tsx index c16d81485ef..8d8fe9ddccb 100644 --- a/client/search-ui/src/components/CodeExcerpt.test.tsx +++ b/client/search-ui/src/components/CodeExcerpt.test.tsx @@ -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 }) => ( - <> - {children} - -)) - describe('CodeExcerpt', () => { afterAll(cleanup) @@ -66,7 +45,7 @@ describe('CodeExcerpt', () => { // at least exist. const { container } = render() const dataLines = container.querySelectorAll('[data-line]') - expect(dataLines.length).toMatchInlineSnapshot('3') + expect(dataLines).toHaveLength(3) }) it('renders the code portion of each row', () => { diff --git a/client/search-ui/src/components/FileMatchChildren.test.tsx b/client/search-ui/src/components/FileMatchChildren.test.tsx index 0457699bb2e..f67f195e87a 100644 --- a/client/search-ui/src/components/FileMatchChildren.test.tsx +++ b/client/search-ui/src/components/FileMatchChildren.test.tsx @@ -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 }) => ( - <> - {children} - -)) - const history = H.createBrowserHistory() history.replace({ pathname: '/search' }) diff --git a/client/search-ui/src/components/FileMatchChildren.tsx b/client/search-ui/src/components/FileMatchChildren.tsx index 5bf63591242..d9b40f429f2 100644 --- a/client/search-ui/src/components/FileMatchChildren.tsx +++ b/client/search-ui/src/components/FileMatchChildren.tsx @@ -166,7 +166,7 @@ export const FileMatchChildren: React.FunctionComponent { + (isFirst: boolean, startLine: number, endLine: number) => { const startTime = Date.now() return fetchHighlightedFileLineRanges( { diff --git a/client/search-ui/src/components/FileSearchResult.test.tsx b/client/search-ui/src/components/FileSearchResult.test.tsx index 4991664176d..563bdaf111b 100644 --- a/client/search-ui/src/components/FileSearchResult.test.tsx +++ b/client/search-ui/src/components/FileSearchResult.test.tsx @@ -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 }) => ( - <> - {children} - -)) - describe('FileSearchResult', () => { afterAll(cleanup) const history = createBrowserHistory() diff --git a/client/search-ui/src/components/ResultContainer.tsx b/client/search-ui/src/components/ResultContainer.tsx index 852d7fcba99..abdef5ff301 100644 --- a/client/search-ui/src/components/ResultContainer.tsx +++ b/client/search-ui/src/components/ResultContainer.tsx @@ -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 */ diff --git a/client/search-ui/src/documentation/ModalVideo.tsx b/client/search-ui/src/documentation/ModalVideo.tsx index 41dd8b5246f..c8cbe369729 100644 --- a/client/search-ui/src/documentation/ModalVideo.tsx +++ b/client/search-ui/src/documentation/ModalVideo.tsx @@ -32,7 +32,7 @@ export const ModalVideo: React.FunctionComponent { 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 toggleDialog(false)} aria-label="Close" > diff --git a/client/search-ui/src/results/NoResultsPage.tsx b/client/search-ui/src/results/NoResultsPage.tsx index ac7e906c10f..a761cfa63ce 100644 --- a/client/search-ui/src/results/NoResultsPage.tsx +++ b/client/search-ui/src/results/NoResultsPage.tsx @@ -189,7 +189,7 @@ export const NoResultsPage: React.FunctionComponent { + (sectionID: SectionID) => { telemetryService.log('NoResultsPanel', { panelID: sectionID, action: 'closed' }) setHiddenSectionIds((hiddenSectionIDs = []) => !hiddenSectionIDs.includes(sectionID) ? [...hiddenSectionIDs, sectionID] : hiddenSectionIDs diff --git a/client/search-ui/src/results/sidebar/SearchReference.tsx b/client/search-ui/src/results/sidebar/SearchReference.tsx index 89d4fc84f69..bc21f392969 100644 --- a/client/search-ui/src/results/sidebar/SearchReference.tsx +++ b/client/search-ui/src/results/sidebar/SearchReference.tsx @@ -404,7 +404,7 @@ const SearchReferenceEntry = ({ 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 -): (filter: string) => ReactElement { +): (filter: string) => React.ReactNode { return (filter: string) => } diff --git a/client/search-ui/src/results/sidebar/SearchSidebar.tsx b/client/search-ui/src/results/sidebar/SearchSidebar.tsx index a37ed39fbff..cf773357585 100644 --- a/client/search-ui/src/results/sidebar/SearchSidebar.tsx +++ b/client/search-ui/src/results/sidebar/SearchSidebar.tsx @@ -41,12 +41,12 @@ export interface SearchSidebarProps /** * Not yet implemented in the VS Code extension (blocked on Apollo Client integration). */ - getRevisions?: (revisionsProps: Omit) => (query: string) => JSX.Element + getRevisions?: (revisionsProps: Omit) => (query: string) => React.ReactNode /** * Content to render inside sidebar, but before other sections. */ - prefixContent?: JSX.Element + prefixContent?: React.ReactNode buildSearchURLQueryFromQueryState: (queryParameters: BuildSearchQueryURLParameters) => string diff --git a/client/search-ui/src/results/sidebar/SearchSidebarSection.tsx b/client/search-ui/src/results/sidebar/SearchSidebarSection.tsx index b4627e37bec..3b8d5f1916e 100644 --- a/client/search-ui/src/results/sidebar/SearchSidebarSection.tsx +++ b/client/search-ui/src/results/sidebar/SearchSidebarSection.tsx @@ -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) } diff --git a/client/search-ui/src/results/sidebar/helpers.test.ts b/client/search-ui/src/results/sidebar/helpers.test.ts index 376dae41588..0dd05b33e3c 100644 --- a/client/search-ui/src/results/sidebar/helpers.test.ts +++ b/client/search-ui/src/results/sidebar/helpers.test.ts @@ -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' diff --git a/client/search-ui/src/results/use-items-to-show.test.ts b/client/search-ui/src/results/use-items-to-show.test.ts index f1318735ab7..efbf00b577c 100644 --- a/client/search-ui/src/results/use-items-to-show.test.ts +++ b/client/search-ui/src/results/use-items-to-show.test.ts @@ -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' diff --git a/client/shared/dev/mockReactVisibilitySensor.tsx b/client/shared/dev/mockReactVisibilitySensor.tsx new file mode 100644 index 00000000000..6853f004238 --- /dev/null +++ b/client/shared/dev/mockReactVisibilitySensor.tsx @@ -0,0 +1,27 @@ +import React from 'react' + +import _VisibilitySensor from 'react-visibility-sensor' + +type VisibilitySensorPropsType = React.ComponentProps + +export class MockVisibilitySensor extends React.Component { + 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) => ( + <> + {children} + + ) +) diff --git a/client/shared/src/actions/ActionItem.test.tsx b/client/shared/src/actions/ActionItem.test.tsx index ba9b9373d42..76264bb8fbe 100644 --- a/client/shared/src/actions/ActionItem.test.tsx +++ b/client/shared/src/actions/ActionItem.test.tsx @@ -185,7 +185,8 @@ describe('ActionItem', () => { // to result in the setState call.) userEvent.click(screen.getByRole('button')) - await new Promise(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(resolve => setTimeout(resolve)) + await waitFor(() => expect(screen.getByLabelText('Error: x')).toBeInTheDocument()) expect(asFragment()).toMatchSnapshot() }) diff --git a/client/shared/src/actions/ActionsContainer.tsx b/client/shared/src/actions/ActionsContainer.tsx index 4b7f4a1c464..5afb2849ab8 100644 --- a/client/shared/src/actions/ActionsContainer.tsx +++ b/client/shared/src/actions/ActionsContainer.tsx @@ -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> = props => { +export const ActionsContainer: React.FunctionComponent = props => { const { scope, extraContext, returnInactiveMenuItems, extensionsController, menu, empty } = props const contributions = useObservable( diff --git a/client/shared/src/actions/__snapshots__/ActionItem.test.tsx.snap b/client/shared/src/actions/__snapshots__/ActionItem.test.tsx.snap index d78f0eec12c..7bff64b9214 100644 --- a/client/shared/src/actions/__snapshots__/ActionItem.test.tsx.snap +++ b/client/shared/src/actions/__snapshots__/ActionItem.test.tsx.snap @@ -228,8 +228,9 @@ exports[`ActionItem run command with showLoadingSpinnerDuringExecution 2`] = ` `; diff --git a/client/shared/src/components/VirtualList.tsx b/client/shared/src/components/VirtualList.tsx index 5ec59c57f99..b72dd041d34 100644 --- a/client/shared/src/components/VirtualList.tsx +++ b/client/shared/src/components/VirtualList.tsx @@ -60,7 +60,7 @@ export class VirtualList extends React.PureC {this.props.items.slice(0, this.props.itemsToShow).map((item, index) => ( this.onChangeVisibility(isVisible, index)} + onChange={(isVisible: boolean) => this.onChangeVisibility(isVisible, index)} key={this.props.itemKey(item)} containment={this.props.containment} partialVisibility={true} diff --git a/client/shared/src/components/utils/linkClickHandler.test.tsx b/client/shared/src/components/utils/linkClickHandler.test.tsx index 0cd9fa719a0..e8e7f3e94d6 100644 --- a/client/shared/src/components/utils/linkClickHandler.test.tsx +++ b/client/shared/src/components/utils/linkClickHandler.test.tsx @@ -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
Test -
, - root + ) - 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
Test -
, - root + ) - const anchor = root.querySelector('a') + const anchor = container.querySelector('a') assert(anchor) const spy = sinon.spy((_event: MouseEvent) => undefined) diff --git a/client/shared/src/settings/temporary/useTemporarySetting.test.tsx b/client/shared/src/settings/temporary/useTemporarySetting.test.tsx index 41a065a998f..f6768a47601 100644 --- a/client/shared/src/settings/temporary/useTemporarySetting.test.tsx +++ b/client/shared/src/settings/temporary/useTemporarySetting.test.tsx @@ -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' diff --git a/client/shared/src/testing/render-with-branded-context.tsx b/client/shared/src/testing/render-with-branded-context.tsx index 26a48a94d53..ec974e8293e 100644 --- a/client/shared/src/testing/render-with-branded-context.tsx +++ b/client/shared/src/testing/render-with-branded-context.tsx @@ -11,13 +11,18 @@ export interface RenderWithBrandedContextResult extends RenderResult { history: MemoryHistory } +interface RenderWithBrandedContextOptions { + route?: string + history?: MemoryHistory +} + const wildcardTheme: WildcardTheme = { isBranded: true, } export function renderWithBrandedContext( children: ReactNode, - { route = '/', history = createMemoryHistory({ initialEntries: [route] }) } = {} + { route = '/', history = createMemoryHistory({ initialEntries: [route] }) }: RenderWithBrandedContextOptions = {} ): RenderWithBrandedContextResult { return { ...render( diff --git a/client/shared/src/util/useInputValidation.test.ts b/client/shared/src/util/useInputValidation.test.ts index 2fba6901ab5..2574776d321 100644 --- a/client/shared/src/util/useInputValidation.test.ts +++ b/client/shared/src/util/useInputValidation.test.ts @@ -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' diff --git a/client/vscode/src/webview/search-panel/SearchHomeView.tsx b/client/vscode/src/webview/search-panel/SearchHomeView.tsx index d38a6d9a1b3..db6e7139c33 100644 --- a/client/vscode/src/webview/search-panel/SearchHomeView.tsx +++ b/client/vscode/src/webview/search-panel/SearchHomeView.tsx @@ -145,7 +145,7 @@ export const SearchHomeView: React.FunctionComponent => + (query: string): Observable => wrapRemoteObservable(extensionCoreAPI.fetchStreamSuggestions(query, instanceURL)), [extensionCoreAPI, instanceURL] ) diff --git a/client/vscode/src/webview/search-panel/SearchResultsView.tsx b/client/vscode/src/webview/search-panel/SearchResultsView.tsx index cd3cec6eef1..594fc57a511 100644 --- a/client/vscode/src/webview/search-panel/SearchResultsView.tsx +++ b/client/vscode/src/webview/search-panel/SearchResultsView.tsx @@ -244,7 +244,7 @@ export const SearchResultsView: React.FunctionComponent => + (query: string): Observable => wrapRemoteObservable(extensionCoreAPI.fetchStreamSuggestions(query, instanceURL)), [extensionCoreAPI, instanceURL] ) diff --git a/client/vscode/src/webview/search-panel/alias/FileMatchChildren.tsx b/client/vscode/src/webview/search-panel/alias/FileMatchChildren.tsx index 98a781367bb..1ed8fbe12c8 100644 --- a/client/vscode/src/webview/search-panel/alias/FileMatchChildren.tsx +++ b/client/vscode/src/webview/search-panel/alias/FileMatchChildren.tsx @@ -169,7 +169,7 @@ export const FileMatchChildren: React.FunctionComponent { + (isFirst: boolean, startLine: number, endLine: number) => { const startTime = Date.now() return fetchHighlightedFileLineRanges( { diff --git a/client/vscode/src/webview/search-panel/index.tsx b/client/vscode/src/webview/search-panel/index.tsx index 433df02d4e0..bc8c3d95d17 100644 --- a/client/vscode/src/webview/search-panel/index.tsx +++ b/client/vscode/src/webview/search-panel/index.tsx @@ -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> = () => { ) } -render( +const root = createRoot(document.querySelector('#root')!) + +root.render( {/* Required for shared components that depend on `location`. */} @@ -128,6 +130,5 @@ render( - , - document.querySelector('#root') + ) diff --git a/client/vscode/src/webview/sidebars/help/index.tsx b/client/vscode/src/webview/sidebars/help/index.tsx index 6b4134a5241..47a7be068b1 100644 --- a/client/vscode/src/webview/sidebars/help/index.tsx +++ b/client/vscode/src/webview/sidebars/help/index.tsx @@ -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> = () => { ) } -render(
, document.querySelector('#root')) +const root = createRoot(document.querySelector('#root')!) + +root.render(
) diff --git a/client/vscode/src/webview/sidebars/search/index.tsx b/client/vscode/src/webview/sidebars/search/index.tsx index d49559ac2df..2821a64cb36 100644 --- a/client/vscode/src/webview/sidebars/search/index.tsx +++ b/client/vscode/src/webview/sidebars/search/index.tsx @@ -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> = () => { ) } -render( +const root = createRoot(document.querySelector('#root')!) + +root.render(
- , - document.querySelector('#root') + ) diff --git a/client/web/src/SourcegraphWebApp.tsx b/client/web/src/SourcegraphWebApp.tsx index 8bcbe6f4493..928531df47d 100644 --- a/client/web/src/SourcegraphWebApp.tsx +++ b/client/web/src/SourcegraphWebApp.tsx @@ -190,7 +190,10 @@ const history = createBrowserHistory() /** * The root component. */ -export class SourcegraphWebApp extends React.Component { +export class SourcegraphWebApp extends React.Component< + React.PropsWithChildren, + SourcegraphWebAppState +> { private readonly subscriptions = new Subscription() private readonly userRepositoriesUpdates = new Subject() private readonly platformContext: PlatformContext = createPlatformContext() @@ -311,7 +314,7 @@ export class SourcegraphWebApp extends React.Component { +export const Tooltip: React.FunctionComponent> = props => { const [virtualElement, setVirtualElement] = useState(null) useEffect(() => { diff --git a/client/web/src/components/Collapsible.tsx b/client/web/src/components/Collapsible.tsx index a0d534e8698..be50ac02b53 100644 --- a/client/web/src/components/Collapsible.tsx +++ b/client/web/src/components/Collapsible.tsx @@ -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> = ({ +export const Collapsible: React.FunctionComponent = ({ title, detail, children, diff --git a/client/web/src/components/ErrorBoundary.tsx b/client/web/src/components/ErrorBoundary.tsx index 234f9c3ebe5..162145d80db 100644 --- a/client/web/src/components/ErrorBoundary.tsx +++ b/client/web/src/components/ErrorBoundary.tsx @@ -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 { +export class ErrorBoundary extends React.PureComponent, State> { public state: State = {} public static getDerivedStateFromError(error: any): Pick { diff --git a/client/web/src/components/PageTitle.tsx b/client/web/src/components/PageTitle.tsx index a3d8a9739dd..cfb44011052 100644 --- a/client/web/src/components/PageTitle.tsx +++ b/client/web/src/components/PageTitle.tsx @@ -16,7 +16,7 @@ const getBrandName = (): string => { let titleSet = false -export const PageTitle: React.FunctionComponent = ({ title }) => { +export const PageTitle: React.FunctionComponent> = ({ title }) => { useEffect(() => { if (titleSet) { console.error('more than one PageTitle used at the same time') diff --git a/client/web/src/components/WebStory.tsx b/client/web/src/components/WebStory.tsx index 3a7cd6fd282..2e69c03f37e 100644 --- a/client/web/src/components/WebStory.tsx +++ b/client/web/src/components/WebStory.tsx @@ -22,11 +22,11 @@ if (!window.context) { window.context = {} as SourcegraphContext & Mocha.SuiteFunction } -export interface WebStoryProps extends MemoryRouterProps, Pick { +export interface WebStoryProps + extends Omit, + Pick { children: React.FunctionComponent< - React.PropsWithChildren< - ThemeProps & BreadcrumbSetters & BreadcrumbsProps & TelemetryProps & RouteComponentProps - > + ThemeProps & BreadcrumbSetters & BreadcrumbsProps & TelemetryProps & RouteComponentProps > } @@ -34,7 +34,7 @@ export interface WebStoryProps extends MemoryRouterProps, Pick> = ({ +export const WebStory: React.FunctionComponent = ({ children, mocks, useStrictMocking, diff --git a/client/web/src/components/diff/FileDiffConnection.tsx b/client/web/src/components/diff/FileDiffConnection.tsx index 3d61a413b54..3680f35db92 100644 --- a/client/web/src/components/diff/FileDiffConnection.tsx +++ b/client/web/src/components/diff/FileDiffConnection.tsx @@ -111,7 +111,7 @@ export const FileDiffConnection: React.FunctionComponent new ReplaySubject | ErrorLike | undefined>(1), []) const nextDiffsUpdate: FileDiffConnectionProps['onUpdate'] = useCallback( - fileDiffsOrError => diffsUpdates.next(fileDiffsOrError), + (fileDiffsOrError: Connection | ErrorLike | undefined) => diffsUpdates.next(fileDiffsOrError), [diffsUpdates] ) diff --git a/client/web/src/components/diff/FileDiffNode.tsx b/client/web/src/components/diff/FileDiffNode.tsx index 7d2e0e924c7..43eb945451c 100644 --- a/client/web/src/components/diff/FileDiffNode.tsx +++ b/client/web/src/components/diff/FileDiffNode.tsx @@ -59,7 +59,7 @@ export const FileDiffNode: React.FunctionComponent{node.newPath} } else if (node.newPath && node.oldPath && node.newPath !== node.oldPath) { @@ -76,7 +76,7 @@ export const FileDiffNode: React.FunctionComponent{node.oldPath} } - 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) diff --git a/client/web/src/components/externalServices/externalServices.tsx b/client/web/src/components/externalServices/externalServices.tsx index 230af20e3b4..1700ac1b0ef 100644 --- a/client/web/src/components/externalServices/externalServices.tsx +++ b/client/web/src/components/externalServices/externalServices.tsx @@ -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 => ( {props.children} ) -const Value = (props: { children: React.ReactChildren | string | string[] }): JSX.Element => ( +const Value: React.FunctionComponent<{ children: React.ReactNode | string | string[] }> = props => ( {props.children} ) -const githubInstructions = (isEnterprise: boolean): JSX.Element => ( +const githubInstructions = (isEnterprise: boolean): React.ReactNode => (
    {isEnterprise && ( diff --git a/client/web/src/enterprise/batches/BatchSpec.tsx b/client/web/src/enterprise/batches/BatchSpec.tsx index 7551bb80589..3e28bcf15bc 100644 --- a/client/web/src/enterprise/batches/BatchSpec.tsx +++ b/client/web/src/enterprise/batches/BatchSpec.tsx @@ -29,7 +29,7 @@ export interface BatchSpecProps extends ThemeProps { className?: string } -export const BatchSpec: React.FunctionComponent> = ({ +export const BatchSpec: React.FunctionComponent = ({ 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 > = React.memo(function BatchSpecDownloadButton(props) { return ( - + ) }) diff --git a/client/web/src/enterprise/batches/batch-spec/edit/workspaces-preview/WorkspacesPreview.story.tsx b/client/web/src/enterprise/batches/batch-spec/edit/workspaces-preview/WorkspacesPreview.story.tsx index d65eea6ab98..d27169c0baa 100644 --- a/client/web/src/enterprise/batches/batch-spec/edit/workspaces-preview/WorkspacesPreview.story.tsx +++ b/client/web/src/enterprise/batches/batch-spec/edit/workspaces-preview/WorkspacesPreview.story.tsx @@ -35,7 +35,7 @@ export default config export const Unstarted: Story = () => ( - {props => ( + {() => ( ( } refetchBatchChange={() => Promise.resolve()} > - + )} @@ -55,7 +55,7 @@ export const Unstarted: Story = () => ( export const UnstartedWithCachedConnectionResult: Story = () => ( - {props => ( + {() => ( ( } refetchBatchChange={() => Promise.resolve()} > - + )} @@ -113,7 +113,7 @@ export const QueuedInProgress: Story = () => { return ( - {props => ( + {() => ( { } refetchBatchChange={() => Promise.resolve()} > - + )} @@ -172,14 +172,14 @@ export const QueuedInProgressWithCachedConnectionResult: Story = () => { return ( - {props => ( + {() => ( Promise.resolve()} > - + )} @@ -228,14 +228,14 @@ export const FailedErrored: Story = () => { return ( - {props => ( + {() => ( Promise.resolve()} > - + )} @@ -284,14 +284,14 @@ export const FailedErroredWithCachedConnectionResult: Story = () => { return ( - {props => ( + {() => ( Promise.resolve()} > - + )} @@ -303,7 +303,7 @@ FailedErroredWithCachedConnectionResult.storyName = 'failed/errored, with cached export const Succeeded: Story = () => ( - {props => ( + {() => ( ( }, }} > - + )} @@ -331,14 +331,14 @@ export const Succeeded: Story = () => ( export const ReadOnly: Story = () => ( - {props => ( + {() => ( Promise.resolve()} > - + )} diff --git a/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.story.tsx b/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.story.tsx index 2dbccece2c8..d2df5786e24 100644 --- a/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.story.tsx +++ b/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.story.tsx @@ -22,9 +22,9 @@ export default config export const Executing: Story = () => ( - {props => ( + {() => ( - + )} @@ -32,9 +32,9 @@ export const Executing: Story = () => ( export const Failed: Story = () => ( - {props => ( + {() => ( - + )} @@ -42,9 +42,9 @@ export const Failed: Story = () => ( export const Completed: Story = () => ( - {props => ( + {() => ( - + )} @@ -52,9 +52,9 @@ export const Completed: Story = () => ( export const CompletedWithErrors: Story = () => ( - {props => ( + {() => ( - + )} diff --git a/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.tsx b/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.tsx index 0f36c4e0f37..8dbd720677f 100644 --- a/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.tsx +++ b/client/web/src/enterprise/batches/batch-spec/execute/ActionsMenu.tsx @@ -33,7 +33,7 @@ import { CancelExecutionModal } from './CancelExecutionModal' import styles from './ActionsMenu.module.scss' -export const ActionsMenu: React.FunctionComponent> = () => { +export const ActionsMenu: React.FunctionComponent = () => { const { batchChange, batchSpec, setActionsError } = useBatchSpecContext() return diff --git a/client/web/src/enterprise/batches/create/OldCreateBatchChangeContent.tsx b/client/web/src/enterprise/batches/create/OldCreateBatchChangeContent.tsx index 49c32401f7f..49e65ec58b4 100644 --- a/client/web/src/enterprise/batches/create/OldCreateBatchChangeContent.tsx +++ b/client/web/src/enterprise/batches/create/OldCreateBatchChangeContent.tsx @@ -58,7 +58,7 @@ export const OldBatchChangePageContent: React.FunctionComponent -

    1. Write a batch spec YAML file

    +

    1. Write a batch spec YAML file

    The batch spec ( diff --git a/client/web/src/enterprise/batches/create/SearchTemplatesBanner.story.tsx b/client/web/src/enterprise/batches/create/SearchTemplatesBanner.story.tsx index fedce466b53..987d7677bb8 100644 --- a/client/web/src/enterprise/batches/create/SearchTemplatesBanner.story.tsx +++ b/client/web/src/enterprise/batches/create/SearchTemplatesBanner.story.tsx @@ -13,8 +13,6 @@ const config: Meta = { export default config -export const CreatingNewBatchChangeFromSearch: Story = () => ( - {props => } -) +export const CreatingNewBatchChangeFromSearch: Story = () => {() => } CreatingNewBatchChangeFromSearch.storyName = 'Creating new batch change from search' diff --git a/client/web/src/enterprise/code-monitoring/components/DeleteMonitorModal.tsx b/client/web/src/enterprise/code-monitoring/components/DeleteMonitorModal.tsx index 67d922c056a..f061ac02a96 100644 --- a/client/web/src/enterprise/code-monitoring/components/DeleteMonitorModal.tsx +++ b/client/web/src/enterprise/code-monitoring/components/DeleteMonitorModal.tsx @@ -76,7 +76,11 @@ export const DeleteMonitorModal: React.FunctionComponent )} - {deleteCompletedOrError &&
    {deleteCompletedOrError === 'loading' && }
    } + {/* + * 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 &&
    {deleteCompletedOrError === 'loading' && }
    } ) } diff --git a/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx b/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx index 6b56ba15334..a7a38a86451 100644 --- a/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx +++ b/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx @@ -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 && } - {history.location.state && ( - - )} + {location.state && } listComponent="div" diff --git a/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPolicyPage.tsx b/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPolicyPage.tsx index 7d582274836..edb0e527bb1 100644 --- a/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPolicyPage.tsx +++ b/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPolicyPage.tsx @@ -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() @@ -129,9 +130,7 @@ export const CodeIntelConfigurationPolicyPage: FunctionComponent< {savingError && } {deleteError && } - {history.location.state && ( - - )} + {location.state && } {policy.protected ? ( diff --git a/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelIndexesPage.tsx b/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelIndexesPage.tsx index 2a409d87074..7b356116b03 100644 --- a/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelIndexesPage.tsx +++ b/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelIndexesPage.tsx @@ -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> = ({ +export const CodeIntelIndexesPage: FunctionComponent = ({ authenticatedUser, repo, queryLsifIndexListByRepository = defaultQueryLsifIndexListByRepository, @@ -81,9 +81,9 @@ export const CodeIntelIndexesPage: FunctionComponent { 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 - {repo && authenticatedUser?.siteAdmin && ( + {!!repo && !!authenticatedUser?.siteAdmin && ( )} - {history.location.state && ( - - )} + {!!location.state && }
    @@ -132,7 +130,7 @@ export const CodeIntelIndexesPage: FunctionComponent} diff --git a/client/web/src/enterprise/codeintel/uploads/pages/CodeIntelUploadsPage.tsx b/client/web/src/enterprise/codeintel/uploads/pages/CodeIntelUploadsPage.tsx index ab679493584..0711fd46dec 100644 --- a/client/web/src/enterprise/codeintel/uploads/pages/CodeIntelUploadsPage.tsx +++ b/client/web/src/enterprise/codeintel/uploads/pages/CodeIntelUploadsPage.tsx @@ -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 { 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 { - 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 (
    diff --git a/client/web/src/enterprise/embed/main.tsx b/client/web/src/enterprise/embed/main.tsx index e497eb89f8b..e4818b9433a 100644 --- a/client/web/src/enterprise/embed/main.tsx +++ b/client/web/src/enterprise/embed/main.tsx @@ -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(, document.querySelector('#root')) + const root = createRoot(document.querySelector('#root')!) + + root.render() }) diff --git a/client/web/src/enterprise/insights/components/creation-ui/creation-actions/CodeInsightsCreationActions.tsx b/client/web/src/enterprise/insights/components/creation-ui/creation-actions/CodeInsightsCreationActions.tsx index b8501924ebc..acd4f08da5c 100644 --- a/client/web/src/enterprise/insights/components/creation-ui/creation-actions/CodeInsightsCreationActions.tsx +++ b/client/web/src/enterprise/insights/components/creation-ui/creation-actions/CodeInsightsCreationActions.tsx @@ -38,7 +38,7 @@ export const CodeInsightsCreationActions: FC = )}
    - {errors && } + {!!errors && } {} -const LivePreviewChart: React.FunctionComponent> = props => ( +const LivePreviewChart: React.FunctionComponent = props => ( ) diff --git a/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts b/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts index be786e7e958..bc386013e09 100644 --- a/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts +++ b/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts @@ -20,7 +20,7 @@ function getDefaultInputStatus({ meta }: useFieldAPI): InputStatus { return InputStatus.initial } -function getDefaultInputError({ meta }: useFieldAPI): Pick { +function getDefaultInputError({ meta }: useFieldAPI): InputProps['error'] { return meta.touched && meta.error } diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.tsx index 6efc884a69e..3c6a1781f48 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.tsx @@ -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({}) const [resizingView, setResizeView] = useState(null) - useEffect(() => { + useLayoutEffect(() => { setLayouts(insightLayoutGenerator(insights)) }, [insights]) diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/sort-filter-series-panel/SortFilterSeriesPanel.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/sort-filter-series-panel/SortFilterSeriesPanel.tsx index 3e8a0b65f09..29c776fd608 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/sort-filter-series-panel/SortFilterSeriesPanel.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/sort-filter-series-panel/SortFilterSeriesPanel.tsx @@ -114,7 +114,12 @@ interface ToggleButtonProps { onClick: (value: SeriesSortOptionsInput) => void } -const ToggleButton: React.FunctionComponent = ({ selected, value, children, onClick }) => ( +const ToggleButton: React.FunctionComponent> = ({ + selected, + value, + children, + onClick, +}) => ( diff --git a/client/web/src/enterprise/insights/hooks/use-parallel-requests/use-prallel-requests.test.ts b/client/web/src/enterprise/insights/hooks/use-parallel-requests/use-prallel-requests.test.ts index 8063a934c56..063a21d3b2b 100644 --- a/client/web/src/enterprise/insights/hooks/use-parallel-requests/use-prallel-requests.test.ts +++ b/client/web/src/enterprise/insights/hooks/use-parallel-requests/use-prallel-requests.test.ts @@ -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' diff --git a/client/web/src/enterprise/insights/hooks/use-ui-features.test.tsx b/client/web/src/enterprise/insights/hooks/use-ui-features.test.tsx index 109e8c1541a..0dd2138b421 100644 --- a/client/web/src/enterprise/insights/hooks/use-ui-features.test.tsx +++ b/client/web/src/enterprise/insights/hooks/use-ui-features.test.tsx @@ -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' diff --git a/client/web/src/enterprise/insights/pages/dashboards/creation/components/InsightsDashboardCreationContent.tsx b/client/web/src/enterprise/insights/pages/dashboards/creation/components/InsightsDashboardCreationContent.tsx index f668827b8d0..b955441a018 100644 --- a/client/web/src/enterprise/insights/pages/dashboards/creation/components/InsightsDashboardCreationContent.tsx +++ b/client/web/src/enterprise/insights/pages/dashboards/creation/components/InsightsDashboardCreationContent.tsx @@ -44,9 +44,7 @@ export interface InsightsDashboardCreationContentProps { /** * Renders creation UI form content (fields, submit and cancel buttons). */ -export const InsightsDashboardCreationContent: React.FunctionComponent< - React.PropsWithChildren -> = props => { +export const InsightsDashboardCreationContent: React.FunctionComponent = props => { const { initialValues, owners, onSubmit, children } = props const { UIFeatures } = useContext(CodeInsightsBackendContext) diff --git a/client/web/src/enterprise/insights/pages/dashboards/dashboard-page/DashboardsContentPage.test.tsx b/client/web/src/enterprise/insights/pages/dashboards/dashboard-page/DashboardsContentPage.test.tsx index 314122a3558..e642ff183cd 100644 --- a/client/web/src/enterprise/insights/pages/dashboards/dashboard-page/DashboardsContentPage.test.tsx +++ b/client/web/src/enterprise/insights/pages/dashboards/dashboard-page/DashboardsContentPage.test.tsx @@ -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(() => { diff --git a/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx b/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx index 032af900a1e..270c0d7aefb 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx @@ -41,7 +41,7 @@ interface ComputeLivePreviewProps { }[] } -export const ComputeLivePreview: React.FunctionComponent> = props => { +export const ComputeLivePreview: React.FunctionComponent = 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 diff --git a/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx b/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx index 9183e5cdb2e..d476f21a4ee 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx @@ -42,9 +42,7 @@ interface LineChartLivePreviewProps { series: LivePreviewSeries[] } -export const LineChartLivePreview: React.FunctionComponent< - React.PropsWithChildren -> = props => { +export const LineChartLivePreview: React.FunctionComponent = props => { const { disabled, repositories, stepValue, step, series, isAllReposMode, className } = props const { getInsightPreviewContent: getLivePreviewContent } = useContext(CodeInsightsBackendContext) const seriesToggleState = useSeriesToggle() diff --git a/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/components/live-preview-chart/LangStatsInsightLivePreview.tsx b/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/components/live-preview-chart/LangStatsInsightLivePreview.tsx index bd62c4c0791..e2348bf22d5 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/components/live-preview-chart/LangStatsInsightLivePreview.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/components/live-preview-chart/LangStatsInsightLivePreview.tsx @@ -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 -> = props => { +export const LangStatsInsightLivePreview: React.FunctionComponent = props => { const { repository = '', threshold, disabled = false, className } = props const { getLangStatsInsightContent } = useContext(CodeInsightsBackendContext) diff --git a/client/web/src/enterprise/main.tsx b/client/web/src/enterprise/main.tsx index ea6da0f098b..0fc0056920b 100644 --- a/client/web/src/enterprise/main.tsx +++ b/client/web/src/enterprise/main.tsx @@ -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(, document.querySelector('#root')) + const root = createRoot(document.querySelector('#root')!) + + root.render() }) diff --git a/client/web/src/enterprise/organizations/EarlyAccessOrgsCodeForm.tsx b/client/web/src/enterprise/organizations/EarlyAccessOrgsCodeForm.tsx index 155e0b6754c..457f0bb3efc 100644 --- a/client/web/src/enterprise/organizations/EarlyAccessOrgsCodeForm.tsx +++ b/client/web/src/enterprise/organizations/EarlyAccessOrgsCodeForm.tsx @@ -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 = () => { +export const EarlyAccessOrgsCodeForm: FunctionComponent = () => { const [name, setName] = useState('') const [updateFeatureFlag, { data, loading: flagLoading, error: flagError }] = useMutation< diff --git a/client/web/src/enterprise/productSubscription/ProductCertificate.tsx b/client/web/src/enterprise/productSubscription/ProductCertificate.tsx index 50b73d6efad..f25da3c06e7 100644 --- a/client/web/src/enterprise/productSubscription/ProductCertificate.tsx +++ b/client/web/src/enterprise/productSubscription/ProductCertificate.tsx @@ -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 ). */ - footer?: React.ReactFragment | null + footer?: React.ReactNode className?: string } diff --git a/client/web/src/enterprise/searchContexts/SearchContextForm.tsx b/client/web/src/enterprise/searchContexts/SearchContextForm.tsx index 58c786cadb8..5dbc3e8a509 100644 --- a/client/web/src/enterprise/searchContexts/SearchContextForm.tsx +++ b/client/web/src/enterprise/searchContexts/SearchContextForm.tsx @@ -180,7 +180,7 @@ export const SearchContextForm: React.FunctionComponent { + (config: string, isInitialValue?: boolean) => { setRepositoriesConfig(config) if (!isInitialValue && config !== repositoriesConfig) { setHasRepositoriesConfigChanged(true) diff --git a/client/web/src/enterprise/user/productSubscriptions/ProductSubscriptionForm.tsx b/client/web/src/enterprise/user/productSubscriptions/ProductSubscriptionForm.tsx index cc0377eeb75..7ff183d7bf7 100644 --- a/client/web/src/enterprise/user/productSubscriptions/ProductSubscriptionForm.tsx +++ b/client/web/src/enterprise/user/productSubscriptions/ProductSubscriptionForm.tsx @@ -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 } diff --git a/client/web/src/enterprise/user/settings/ExternalAccountNode.tsx b/client/web/src/enterprise/user/settings/ExternalAccountNode.tsx index 437a7019c7b..925a450ae69 100644 --- a/client/web/src/enterprise/user/settings/ExternalAccountNode.tsx +++ b/client/web/src/enterprise/user/settings/ExternalAccountNode.tsx @@ -149,12 +149,16 @@ export class ExternalAccountNode extends React.PureComponent
    - {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 && ( )}{' '} - {this.props.node.refreshURL && ( + {!!this.props.node.refreshURL && ( diff --git a/client/web/src/extensions/extension/RegistryExtensionContributionsPage.tsx b/client/web/src/extensions/extension/RegistryExtensionContributionsPage.tsx index c1973a369e6..7f9404a9c53 100644 --- a/client/web/src/extensions/extension/RegistryExtensionContributionsPage.tsx +++ b/client/web/src/extensions/extension/RegistryExtensionContributionsPage.tsx @@ -24,7 +24,7 @@ interface ContributionGroup { title: string error?: ErrorLike columnHeaders: string[] - rows: (React.ReactFragment | null)[][] + rows: React.ReactNode[][] } const ContributionsTable: React.FunctionComponent< diff --git a/client/web/src/featureFlags/FeatureFlagsProvider.tsx b/client/web/src/featureFlags/FeatureFlagsProvider.tsx index ee7878a1bb8..d1362a9682d 100644 --- a/client/web/src/featureFlags/FeatureFlagsProvider.tsx +++ b/client/web/src/featureFlags/FeatureFlagsProvider.tsx @@ -49,7 +49,7 @@ const FeatureFlagsLocalOverrideAgent = React.memo(() => { }) const MINUTE = 60000 -export const FeatureFlagsProvider: React.FunctionComponent = ({ +export const FeatureFlagsProvider: React.FunctionComponent> = ({ isLocalOverrideEnabled = true, children, }) => { @@ -77,11 +77,9 @@ interface MockedFeatureFlagsProviderProps { * * ) */ -export const MockedFeatureFlagsProvider: React.FunctionComponent = ({ - overrides, - refetchInterval, - children, -}) => { +export const MockedFeatureFlagsProvider: React.FunctionComponent< + React.PropsWithChildren +> = ({ overrides, refetchInterval, children }) => { const mockRequestGraphQL = useMemo( () => ( query: string, diff --git a/client/web/src/featureFlags/useFeatureFlag.test.tsx b/client/web/src/featureFlags/useFeatureFlag.test.tsx index b8462d6187f..69c5e88a49d 100644 --- a/client/web/src/featureFlags/useFeatureFlag.test.tsx +++ b/client/web/src/featureFlags/useFeatureFlag.test.tsx @@ -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 ( - >} - refetchInterval={refetchInterval} - > - {children} - - ) - }, + + const Wrapper: React.JSXElementConstructor<{ + children: React.ReactElement + nextOverrides?: Partial> + 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 ( + + {children} + + ) + } + + const setup = ( + initialFlagName: FeatureFlagName, + defaultValue = false, + refetchInterval?: number, + nextOverrides?: Partial> + ) => + renderHook< + ReturnType, + { + flagName: FeatureFlagName + } + >(({ flagName }) => useFeatureFlag(flagName, defaultValue), { + wrapper: 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']))) }) }) diff --git a/client/web/src/integration/batches.test.ts b/client/web/src/integration/batches.test.ts index 6c3d211e9fe..d08310277a0 100644 --- a/client/web/src/integration/batches.test.ts +++ b/client/web/src/integration/batches.test.ts @@ -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) }) diff --git a/client/web/src/integration/blob-viewer.test.ts b/client/web/src/integration/blob-viewer.test.ts index 8a2df2eb1b4..8d98c8a6043 100644 --- a/client/web/src/integration/blob-viewer.test.ts +++ b/client/web/src/integration/blob-viewer.test.ts @@ -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('.test-repo-blob')?.textContent + () => document.querySelector('[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 } ) diff --git a/client/web/src/integration/nav.test.ts b/client/web/src/integration/nav.test.ts index 4989353b236..d6455a92bd7 100644 --- a/client/web/src/integration/nav.test.ts +++ b/client/web/src/integration/nav.test.ts @@ -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') diff --git a/client/web/src/integration/profile.test.ts b/client/web/src/integration/profile.test.ts index 7ff6e393483..a253ff6da7e 100644 --- a/client/web/src/integration/profile.test.ts +++ b/client/web/src/integration/profile.test.ts @@ -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', diff --git a/client/web/src/integration/repository.test.ts b/client/web/src/integration/repository.test.ts index 6b923feacf1..4c4424340b7 100644 --- a/client/web/src/integration/repository.test.ts +++ b/client/web/src/integration/repository.test.ts @@ -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') } diff --git a/client/web/src/integration/search.test.ts b/client/web/src/integration/search.test.ts index 0b7021d1ab1..313adef7a07 100644 --- a/client/web/src/integration/search.test.ts +++ b/client/web/src/integration/search.test.ts @@ -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') }) diff --git a/client/web/src/main.tsx b/client/web/src/main.tsx index 398a72f7a89..77f452c7a95 100644 --- a/client/web/src/main.tsx +++ b/client/web/src/main.tsx @@ -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(, document.querySelector('#root')) + const root = createRoot(document.querySelector('#root')!) + + root.render() }) diff --git a/client/web/src/nav/GlobalNavbar.tsx b/client/web/src/nav/GlobalNavbar.tsx index df2840d8ab8..2b088148c02 100644 --- a/client/web/src/nav/GlobalNavbar.tsx +++ b/client/web/src/nav/GlobalNavbar.tsx @@ -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 diff --git a/client/web/src/nav/UserNavItem.story.tsx b/client/web/src/nav/UserNavItem.story.tsx index 859fa0018f9..adad9657398 100644 --- a/client/web/src/nav/UserNavItem.story.tsx +++ b/client/web/src/nav/UserNavItem.story.tsx @@ -73,11 +73,9 @@ const commonProps = (): UserNavItemProps => ({ position: Position.bottomStart, }) -const OpenByDefaultWrapper: React.FunctionComponent< - React.PropsWithChildren<{ - children: React.FunctionComponent }>> - }> -> = ({ children }) => { +const OpenByDefaultWrapper: React.FunctionComponent<{ + children: React.FunctionComponent }>> +}> = ({ children }) => { const menuButtonReference = useRef(null) useEffect(() => { diff --git a/client/web/src/repo/blob/Blob.tsx b/client/web/src/repo/blob/Blob.tsx index 91f7fb20057..ffbadabe1f4 100644 --- a/client/web/src/repo/blob/Blob.tsx +++ b/client/web/src/repo/blob/Blob.tsx @@ -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> = 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(null), []) useEffect(() => { @@ -783,7 +792,12 @@ export const Blob: React.FunctionComponent> = return ( <> -
    +
    > = {/* Render the (unhighlighted) blob also in the case highlighting timed out */} {renderMode === 'code' && ( !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 { public render(): JSX.Element | null { return ( -
    +
    { + const copyToClipboard = useCallback((oid: string): void => { eventLogger.log('CommitSHACopiedToClipboard') copy(oid) setFlashCopiedToClipboardMessage(true) diff --git a/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx b/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx index 0aea5c9b11c..ecaaa39db3b 100644 --- a/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx +++ b/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx @@ -66,9 +66,9 @@ class UpdateMirrorRepositoryActionContainer extends React.PureComponent > = ({ 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 diff --git a/client/web/src/savedSearches/SavedSearchListPage.tsx b/client/web/src/savedSearches/SavedSearchListPage.tsx index 8290029993c..e60236f2f1d 100644 --- a/client/web/src/savedSearches/SavedSearchListPage.tsx +++ b/client/web/src/savedSearches/SavedSearchListPage.tsx @@ -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 | 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 { public subscriptions = new Subscription() @@ -188,6 +188,7 @@ const SavedSearchListPageContent: React.FunctionComponent { + const location = useLocation<{ description?: string }>() const searchPatternType = useNavbarQueryState(state => state.searchPatternType) const callbackReference = useCallbackRef(null, ref => ref?.focus()) @@ -210,7 +211,7 @@ const SavedSearchListPageContent: React.FunctionComponent ( >[] + overviewComponents: readonly React.ComponentType>[] } export interface SiteAdminAreaRoute extends RouteDescriptor {} diff --git a/client/web/src/site-admin/SiteAdminFeatureFlagConfigurationPage.tsx b/client/web/src/site-admin/SiteAdminFeatureFlagConfigurationPage.tsx index fb4dde5657e..85b8e65c532 100644 --- a/client/web/src/site-admin/SiteAdminFeatureFlagConfigurationPage.tsx +++ b/client/web/src/site-admin/SiteAdminFeatureFlagConfigurationPage.tsx @@ -451,7 +451,7 @@ const FeatureFlagOverrideItem: FunctionComponent< const nsValue = orgID > 0 ? orgID : userID const onError = useCallback( - error => { + (error: Error) => { setError(error) }, [setError] diff --git a/client/web/src/site-admin/analytics/components/AnalyticsPageTitle.tsx b/client/web/src/site-admin/analytics/components/AnalyticsPageTitle.tsx index a75fe91f304..769ad0ff87f 100644 --- a/client/web/src/site-admin/analytics/components/AnalyticsPageTitle.tsx +++ b/client/web/src/site-admin/analytics/components/AnalyticsPageTitle.tsx @@ -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> = ({ children }) => (
    Experimental diff --git a/client/web/src/stores/notepad.test.ts b/client/web/src/stores/notepad.test.ts index 26f70c4e921..bf4145f8455 100644 --- a/client/web/src/stores/notepad.test.ts +++ b/client/web/src/stores/notepad.test.ts @@ -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' diff --git a/client/web/src/theme.test.ts b/client/web/src/theme.test.ts index fd8c68f9180..dbfe274f1ba 100644 --- a/client/web/src/theme.test.ts +++ b/client/web/src/theme.test.ts @@ -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' diff --git a/client/web/src/tour/components/Tour/TourContent.tsx b/client/web/src/tour/components/Tour/TourContent.tsx index 791b79434ac..4d16e586106 100644 --- a/client/web/src/tour/components/Tour/TourContent.tsx +++ b/client/web/src/tour/components/Tour/TourContent.tsx @@ -23,7 +23,7 @@ interface TourContentProps { className?: string } -const Header: React.FunctionComponent<{ onClose: () => void; title?: string }> = ({ +const Header: React.FunctionComponent void; title?: string }>> = ({ children, onClose, title = 'Quick start', @@ -36,7 +36,7 @@ const Header: React.FunctionComponent<{ onClose: () => void; title?: string }> =
    ) -const Footer: React.FunctionComponent<{ completedCount: number; totalCount: number }> = ({ +const Footer: React.FunctionComponent> = ({ completedCount, totalCount, }) => ( @@ -50,7 +50,7 @@ const Footer: React.FunctionComponent<{ completedCount: number; totalCount: numb ) -const CompletedItem: React.FunctionComponent = ({ children }) => ( +const CompletedItem: React.FunctionComponent> = ({ children }) => (
  1. (
  2. ) -export const TourContent: React.FunctionComponent = ({ +export const TourContent: React.FunctionComponent> = ({ onClose, tasks, variant, diff --git a/client/web/src/tour/components/Tour/useTour.test.tsx b/client/web/src/tour/components/Tour/useTour.test.tsx index b9c8c73d9a6..9fb8c4bd13b 100644 --- a/client/web/src/tour/components/Tour/useTour.test.tsx +++ b/client/web/src/tour/components/Tour/useTour.test.tsx @@ -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> = ({ children }) => ( {children} ) return renderHook(() => useTour(TourId), { wrapper }) diff --git a/client/web/src/tracking/withActivation.tsx b/client/web/src/tracking/withActivation.tsx index af49313d1fc..428ab6e28da 100644 --- a/client/web/src/tracking/withActivation.tsx +++ b/client/web/src/tracking/withActivation.tsx @@ -276,7 +276,7 @@ export const withActivation =

    ( private updateCompletionStatus = (update: Partial): void => this.updates.next(update) - public render(): React.ReactFragment | null { + public render(): React.ReactNode { const steps = this.steps() const activationProps: ActivationProps = { activation: steps && { diff --git a/client/web/src/types/graphiql/index.d.ts b/client/web/src/types/graphiql/index.d.ts index cafbd7811c0..78e013108ce 100644 --- a/client/web/src/types/graphiql/index.d.ts +++ b/client/web/src/types/graphiql/index.d.ts @@ -87,7 +87,7 @@ declare module 'graphiql' { editorTheme?: string } - export default class GraphiQL extends React.Component { + export default class GraphiQL extends React.Component> { public static Logo: React.ComponentClass public static Toolbar: React.ComponentClass public static Button: React.ComponentClass diff --git a/client/web/src/types/react-scroll-manager/index.d.ts b/client/web/src/types/react-scroll-manager/index.d.ts new file mode 100644 index 00000000000..07f301e3f01 --- /dev/null +++ b/client/web/src/types/react-scroll-manager/index.d.ts @@ -0,0 +1,20 @@ +/* eslint-disable react/prefer-stateless-function */ +import React from 'react' + +import { History } from 'history' + +export interface ScrollManagerProps { + history: History + sessionKey?: string + timeout?: number +} + +export class ScrollManager extends React.Component> {} + +export class WindowScroller extends React.Component {} + +export interface ElementScrollerProps { + scrollKey: string +} + +export class ElementScroller extends React.Component> {} diff --git a/client/web/src/user/settings/codeHosts/AddCodeHostConnectionModal.tsx b/client/web/src/user/settings/codeHosts/AddCodeHostConnectionModal.tsx index ca9c73c93ca..9e89930435e 100644 --- a/client/web/src/user/settings/codeHosts/AddCodeHostConnectionModal.tsx +++ b/client/web/src/user/settings/codeHosts/AddCodeHostConnectionModal.tsx @@ -36,7 +36,7 @@ export const AddCodeHostConnectionModal: React.FunctionComponent< onDidCancel: () => void onDidError: (error: ErrorLike) => void - hintFragment?: React.ReactFragment + hintFragment?: React.ReactNode }> > = ({ ownerID, serviceName, serviceKind, hintFragment, onDidAdd, onDidCancel, onDidError }) => { const [token, setToken] = useState('') diff --git a/client/web/src/user/settings/codeHosts/UpdateCodeHostConnectionModal.tsx b/client/web/src/user/settings/codeHosts/UpdateCodeHostConnectionModal.tsx index ce0bcf7eec5..8d072f4643c 100644 --- a/client/web/src/user/settings/codeHosts/UpdateCodeHostConnectionModal.tsx +++ b/client/web/src/user/settings/codeHosts/UpdateCodeHostConnectionModal.tsx @@ -33,7 +33,7 @@ export const UpdateCodeHostConnectionModal: React.FunctionComponent< onDidCancel: () => void onDidError: (error: ErrorLike) => void - hintFragment?: React.ReactFragment + hintFragment?: React.ReactNode }> > = ({ serviceID, serviceConfig, serviceName, hintFragment, onDidUpdate, onDidCancel, onDidError }) => { const [token, setToken] = useState('') diff --git a/client/web/src/user/settings/codeHosts/modalHints.tsx b/client/web/src/user/settings/codeHosts/modalHints.tsx index e675edc06fd..5d365ed169d 100644 --- a/client/web/src/user/settings/codeHosts/modalHints.tsx +++ b/client/web/src/user/settings/codeHosts/modalHints.tsx @@ -16,7 +16,7 @@ const MachineUserRecommendation = ( ) -export const scopes: Partial> = { +export const scopes: Partial> = { [ExternalServiceKind.GITHUB]: ( Use an access token with repo,{' '} @@ -35,7 +35,7 @@ export const scopes: Partial> = ), } -export const getMachineUserFragment = (serviceName: string): React.ReactFragment => ( +export const getMachineUserFragment = (serviceName: string): React.ReactNode => (

    diff --git a/client/web/src/user/settings/profile/EditUserProfileForm.tsx b/client/web/src/user/settings/profile/EditUserProfileForm.tsx index 5861f78524b..c32e1c2d6ed 100644 --- a/client/web/src/user/settings/profile/EditUserProfileForm.tsx +++ b/client/web/src/user/settings/profile/EditUserProfileForm.tsx @@ -27,7 +27,7 @@ export const UPDATE_USER = gql` interface Props { user: Pick initialValue: UserProfileFormFieldsValue - after?: React.ReactFragment + after?: React.ReactNode } /** diff --git a/client/wildcard/src/components/Collapse/Collapse.tsx b/client/wildcard/src/components/Collapse/Collapse.tsx index ce56d2a41f5..92ea2e811dd 100644 --- a/client/wildcard/src/components/Collapse/Collapse.tsx +++ b/client/wildcard/src/components/Collapse/Collapse.tsx @@ -29,9 +29,7 @@ const DEFAULT_CONTEXT_VALUE: CollapseContextData = { const CollapseContext = createContext(DEFAULT_CONTEXT_VALUE) -export const Collapse: React.FunctionComponent> = React.memo(function Collapse( - props -) { +export const Collapse: React.FunctionComponent = React.memo(function Collapse(props) { const { children, isOpen, openByDefault, onOpenChange = noop } = props const [isInternalOpen, setInternalOpen] = useState(Boolean(openByDefault)) const isControlled = isOpen !== undefined @@ -40,7 +38,7 @@ export const Collapse: React.FunctionComponent : children const setOpen = useCallback( - opened => { + (opened: boolean) => { if (!isControlled) { setInternalOpen(opened) return diff --git a/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.story.tsx b/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.story.tsx index b5f4826a7dd..cb88eb49d95 100644 --- a/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.story.tsx +++ b/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.story.tsx @@ -48,7 +48,7 @@ const handleErrorSubmit = () => isHappinessFeedback: false, }) -export const FeedbackPromptWithSuccessResponse = () => ( +export const FeedbackPromptWithSuccessResponse: Story = () => ( <>

    This is a feedbackPrompt with success response

    diff --git a/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.tsx b/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.tsx index 0444cb85ede..0d0477d5ba1 100644 --- a/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.tsx +++ b/client/wildcard/src/components/Feedback/FeedbackPrompt/FeedbackPrompt.tsx @@ -193,7 +193,7 @@ interface FeedbackPromptProps extends FeedbackPromptContentProps { children: React.FunctionComponent> | ReactNode } -export const FeedbackPrompt: React.FunctionComponent> = ({ +export const FeedbackPrompt: React.FunctionComponent = ({ openByDefault = false, onSubmit, children, diff --git a/client/wildcard/src/components/Feedback/FeedbackPrompt/__snapshots__/FeedbackPrompt.test.tsx.snap b/client/wildcard/src/components/Feedback/FeedbackPrompt/__snapshots__/FeedbackPrompt.test.tsx.snap index e3b47c9bac0..775e5156ae7 100644 --- a/client/wildcard/src/components/Feedback/FeedbackPrompt/__snapshots__/FeedbackPrompt.test.tsx.snap +++ b/client/wildcard/src/components/Feedback/FeedbackPrompt/__snapshots__/FeedbackPrompt.test.tsx.snap @@ -287,22 +287,20 @@ exports[`FeedbackPrompt should render submit error correctly 1`] = `