mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
code-search: improve actions discoverability and UX on file page (#58122)
This commit is contained in:
parent
f6a5abe6e0
commit
87d481544d
@ -1211,9 +1211,11 @@ ts_project(
|
||||
"src/repo/DirectImportRepoAlert.tsx",
|
||||
"src/repo/FilePathBreadcrumbs.tsx",
|
||||
"src/repo/GitReference.tsx",
|
||||
"src/repo/RepoActionInfo.tsx",
|
||||
"src/repo/RepoContainer.tsx",
|
||||
"src/repo/RepoContainerError.tsx",
|
||||
"src/repo/RepoHeader.tsx",
|
||||
"src/repo/RepoHeaderContextMenu.tsx",
|
||||
"src/repo/RepoHeaderContributionPortal.tsx",
|
||||
"src/repo/RepoLinkPicker.tsx",
|
||||
"src/repo/RepoMetadataPage/AddMetadataForm.tsx",
|
||||
@ -1249,8 +1251,8 @@ ts_project(
|
||||
"src/repo/RevisionsPopover/components/ConnectionPopoverNodeLink/index.ts",
|
||||
"src/repo/RevisionsPopover/components/index.ts",
|
||||
"src/repo/RevisionsPopover/index.ts",
|
||||
"src/repo/actions/CopyPermalinkAction.tsx",
|
||||
"src/repo/actions/GoToCodeHostAction.tsx",
|
||||
"src/repo/actions/GoToPermalinkAction.tsx",
|
||||
"src/repo/actions/ToggleBlameAction.tsx",
|
||||
"src/repo/backend.ts",
|
||||
"src/repo/blame/useBlameHunks.ts",
|
||||
@ -1259,9 +1261,9 @@ ts_project(
|
||||
"src/repo/blob/BlobLoadingSpinner.tsx",
|
||||
"src/repo/blob/BlobPage.tsx",
|
||||
"src/repo/blob/CodeMirrorBlob.tsx",
|
||||
"src/repo/blob/GoToRawAction.tsx",
|
||||
"src/repo/blob/RenderedFile.tsx",
|
||||
"src/repo/blob/RenderedNotebookMarkdown.tsx",
|
||||
"src/repo/blob/actions/GoToRawAction.tsx",
|
||||
"src/repo/blob/actions/ToggleHistoryPanel.tsx",
|
||||
"src/repo/blob/actions/ToggleLineWrap.tsx",
|
||||
"src/repo/blob/actions/ToggleRenderedFileMode.tsx",
|
||||
|
||||
@ -1,28 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
import { mdiArrowRightThin, mdiBrain } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import { mdiBrain } from '@mdi/js'
|
||||
|
||||
import {
|
||||
Position,
|
||||
Icon,
|
||||
Link,
|
||||
LoadingSpinner,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuHeader,
|
||||
MenuList,
|
||||
Tooltip,
|
||||
RadioButton,
|
||||
useSessionStorage,
|
||||
Code,
|
||||
Text,
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import { useVisibleIndexes } from '../hooks/useVisibleIndexes'
|
||||
|
||||
import styles from './BrainDot.module.scss'
|
||||
|
||||
export interface BrainDotProps {
|
||||
repoName: string
|
||||
commit: string
|
||||
@ -30,29 +22,14 @@ export interface BrainDotProps {
|
||||
}
|
||||
|
||||
export const BrainDot: React.FunctionComponent<BrainDotProps> = ({ repoName, commit, path }) => (
|
||||
<Menu>
|
||||
<Tooltip content="View code intelligence summary">
|
||||
<MenuButton className={classNames('text-decoration-none', styles.braindot)} aria-label="Code graph">
|
||||
<Icon aria-hidden={true} svgPath={mdiBrain} />
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList position={Position.bottomEnd} className={styles.dropdownMenu}>
|
||||
<MenuHeader>
|
||||
Click to view code intelligence summary
|
||||
<span className="float-right">
|
||||
<Tooltip content="View code intelligence summary">
|
||||
<Link to={`/${repoName}/-/code-graph/dashboard`}>
|
||||
<Icon aria-hidden={true} svgPath={mdiArrowRightThin} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</MenuHeader>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
<BrainDotContent repoName={repoName} commit={commit} path={path} />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuHeader className="d-flex">
|
||||
<Icon aria-hidden={true} svgPath={mdiBrain} fill="text-muted" />
|
||||
<Text className="mb-0 ml-2">Code intelligence preview</Text>
|
||||
</MenuHeader>
|
||||
<BrainDotContent repoName={repoName} commit={commit} path={path} />
|
||||
</>
|
||||
)
|
||||
|
||||
const BrainDotContent: React.FunctionComponent<BrainDotProps> = ({ repoName, commit, path }) => {
|
||||
@ -76,16 +53,13 @@ const BrainDotContent: React.FunctionComponent<BrainDotProps> = ({ repoName, com
|
||||
{visibleIndexesLoading && <LoadingSpinner className="mx-2" />}
|
||||
{visibleIndexes && visibleIndexes.length > 0 && (
|
||||
<MenuHeader>
|
||||
<Tooltip content="Not intended for regular use">
|
||||
<span>Display debug information for uploaded index.</span>
|
||||
</Tooltip>
|
||||
{[
|
||||
<RadioButton
|
||||
id="none"
|
||||
key="none"
|
||||
name="none"
|
||||
label="None"
|
||||
wrapperClassName="py-1"
|
||||
wrapperClassName="py-1 px-2"
|
||||
checked={visibleIndexID === undefined}
|
||||
onChange={() => {
|
||||
delete indexIDsForSnapshotData[repoName]
|
||||
@ -93,29 +67,28 @@ const BrainDotContent: React.FunctionComponent<BrainDotProps> = ({ repoName, com
|
||||
}}
|
||||
/>,
|
||||
...visibleIndexes.map(index => (
|
||||
<Tooltip content={`Uploaded at ${index.uploadedAt}`} key={index.id}>
|
||||
<RadioButton
|
||||
id={index.id}
|
||||
name={index.id}
|
||||
checked={visibleIndexID === index.id}
|
||||
wrapperClassName="py-1"
|
||||
label={
|
||||
<>
|
||||
Index at <Code>{index.inputCommit.slice(0, 7)}</Code>
|
||||
</>
|
||||
}
|
||||
onChange={() => {
|
||||
indexIDsForSnapshotData[repoName] = index.id
|
||||
setIndexIDForSnapshotData(indexIDsForSnapshotData)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<RadioButton
|
||||
key={index.id}
|
||||
id={index.id}
|
||||
name={index.id}
|
||||
checked={visibleIndexID === index.id}
|
||||
wrapperClassName="py-1 px-2"
|
||||
label={
|
||||
<>
|
||||
Index at <Code>{index.inputCommit.slice(0, 7)}</Code>
|
||||
</>
|
||||
}
|
||||
onChange={() => {
|
||||
indexIDsForSnapshotData[repoName] = index.id
|
||||
setIndexIDForSnapshotData(indexIDsForSnapshotData)
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
]}
|
||||
</MenuHeader>
|
||||
)}
|
||||
{(visibleIndexes?.length ?? 0) === 0 && !visibleIndexesLoading && (
|
||||
<MenuHeader>No precise indexes to display debug information for.</MenuHeader>
|
||||
<small className="px-2">No precise indexes to display debug information for.</small>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ApolloError } from '@apollo/client'
|
||||
|
||||
import { useQuery } from '@sourcegraph/http-client'
|
||||
import { TetherAPI } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { VisibleIndexesResult, VisibleIndexesVariables } from '../../../../graphql-operations'
|
||||
import { visibleIndexesQuery } from '../backend'
|
||||
@ -20,6 +21,12 @@ export const useVisibleIndexes = (variables: VisibleIndexesVariables): UseVisibl
|
||||
const { data, error, loading } = useQuery<VisibleIndexesResult, VisibleIndexesVariables>(visibleIndexesQuery, {
|
||||
variables,
|
||||
fetchPolicy: 'cache-first',
|
||||
onCompleted() {
|
||||
TetherAPI.update()
|
||||
},
|
||||
onError() {
|
||||
TetherAPI.update()
|
||||
},
|
||||
})
|
||||
|
||||
const indexes = data?.repository?.commit?.blob?.lsif?.visibleIndexes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import { RepoHeaderActionAnchor, RepoHeaderActionMenuLink } from '../repo/components/RepoHeaderActions'
|
||||
import { RepoActionInfo } from '../repo/RepoActionInfo'
|
||||
import { eventLogger } from '../tracking/eventLogger'
|
||||
|
||||
import { getEditorSettingsErrorMessage } from './build-url'
|
||||
@ -189,6 +190,7 @@ interface EditorItemProps {
|
||||
source?: 'repoHeader' | 'actionItemsBar'
|
||||
actionType?: 'nav' | 'dropdown'
|
||||
}
|
||||
|
||||
function EditorItem(props: EditorItemProps): JSX.Element {
|
||||
if (props.source === 'actionItemsBar') {
|
||||
return (
|
||||
@ -210,7 +212,7 @@ function EditorItem(props: EditorItemProps): JSX.Element {
|
||||
return (
|
||||
<Tooltip content={props.tooltip}>
|
||||
<RepoHeaderActionAnchor onSelect={props.onClick} className={styles.item}>
|
||||
{props.icon}
|
||||
<RepoActionInfo icon={props.icon} displayName="Editor" />
|
||||
</RepoHeaderActionAnchor>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
13
client/web/src/repo/RepoActionInfo.module.scss
Normal file
13
client/web/src/repo/RepoActionInfo.module.scss
Normal file
@ -0,0 +1,13 @@
|
||||
@import 'wildcard/src/global-styles/breakpoints';
|
||||
|
||||
.repo-action-label {
|
||||
margin-bottom: 0;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@media (--md-breakpoint-down) {
|
||||
.repo-action-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
20
client/web/src/repo/RepoActionInfo.tsx
Normal file
20
client/web/src/repo/RepoActionInfo.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import type { ReactNode, FC } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { Text } from '@sourcegraph/wildcard'
|
||||
|
||||
import styles from './RepoActionInfo.module.scss'
|
||||
|
||||
interface RepoActionInfoProps {
|
||||
displayName: string
|
||||
icon: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const RepoActionInfo: FC<RepoActionInfoProps> = ({ displayName, icon, className }) => (
|
||||
<>
|
||||
{icon}
|
||||
<Text className={classNames(styles.repoActionLabel, className)}>{displayName}</Text>
|
||||
</>
|
||||
)
|
||||
@ -238,7 +238,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
|
||||
settingsCascade={props.settingsCascade}
|
||||
authenticatedUser={authenticatedUser}
|
||||
platformContext={props.platformContext}
|
||||
telemetryService={props.telemetryService}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
@ -483,27 +482,27 @@ const RepoUserContainer: FC<RepoUserContainerProps> = ({
|
||||
/>
|
||||
))}
|
||||
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={1}
|
||||
id="cody"
|
||||
{...repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{() =>
|
||||
!isCodySidebarOpen ? (
|
||||
{!isCodySidebarOpen && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={1}
|
||||
id="cody"
|
||||
{...repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{() => (
|
||||
<AskCodyButton
|
||||
onClick={() => {
|
||||
logTranscriptEvent(EventName.CODY_SIDEBAR_CHAT_OPENED, { repo, path: filePath })
|
||||
setIsCodySidebarOpen(true)
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</RepoHeaderContributionPortal>
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
)}
|
||||
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={2}
|
||||
priority={3}
|
||||
id="go-to-code-host"
|
||||
{...repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
@ -530,8 +529,9 @@ const RepoUserContainer: FC<RepoUserContainerProps> = ({
|
||||
{isBrainDotVisible && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={110}
|
||||
priority={7}
|
||||
id="code-intelligence-status"
|
||||
renderInContextMenu={true}
|
||||
{...repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) =>
|
||||
|
||||
@ -12,6 +12,11 @@
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
@media (--sm-breakpoint-up) and (--lg-breakpoint-down) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:global(.navbar-nav) {
|
||||
white-space: nowrap;
|
||||
|
||||
@ -36,6 +41,7 @@
|
||||
.action-list {
|
||||
gap: 0.625rem;
|
||||
margin-left: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.action-list-item:not(:empty) {
|
||||
|
||||
@ -9,7 +9,7 @@ import { BrandedStory } from '@sourcegraph/wildcard/src/stories'
|
||||
|
||||
import type { AuthenticatedUser } from '../auth'
|
||||
|
||||
import { GoToPermalinkAction } from './actions/GoToPermalinkAction'
|
||||
import { CopyPermalinkAction } from './actions/CopyPermalinkAction'
|
||||
import { FilePathBreadcrumbs } from './FilePathBreadcrumbs'
|
||||
import { RepoHeader, type RepoHeaderContributionsLifecycleProps } from './RepoHeader'
|
||||
import { RepoRevisionContainerBreadcrumb } from './RepoRevisionContainer'
|
||||
@ -76,12 +76,12 @@ const onLifecyclePropsChange = (lifecycleProps: RepoHeaderContributionsLifecycle
|
||||
id: 'go-to-permalink',
|
||||
position: 'right',
|
||||
children: () => (
|
||||
<GoToPermalinkAction
|
||||
telemetryService={NOOP_TELEMETRY_SERVICE}
|
||||
<CopyPermalinkAction
|
||||
revision="main"
|
||||
commitID="123"
|
||||
repoName="sourcegraph/sourcegraph"
|
||||
actionType="nav"
|
||||
telemetryService={NOOP_TELEMETRY_SERVICE}
|
||||
/>
|
||||
),
|
||||
})
|
||||
@ -147,7 +147,6 @@ const createProps = (path: string, forceWrap: boolean = false): React.ComponentP
|
||||
settingsCascade: EMPTY_SETTINGS_CASCADE,
|
||||
authenticatedUser: mockUser,
|
||||
platformContext: {} as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
forceWrap,
|
||||
})
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import { useLocation } from 'react-router-dom'
|
||||
|
||||
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import type { SettingsCascadeOrError } from '@sourcegraph/shared/src/settings/settings'
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Menu, MenuList, Position, Icon } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { AuthenticatedUser } from '../auth'
|
||||
@ -15,6 +14,7 @@ import { ErrorBoundary } from '../components/ErrorBoundary'
|
||||
import { useBreakpoint } from '../util/dom'
|
||||
|
||||
import { RepoHeaderActionDropdownToggle } from './components/RepoHeaderActions'
|
||||
import { RepoHeaderContextMenu } from './RepoHeaderContextMenu'
|
||||
|
||||
import styles from './RepoHeader.module.scss'
|
||||
|
||||
@ -81,6 +81,8 @@ export interface RepoHeaderContribution {
|
||||
* Use `actionType` to determine how to render the component.
|
||||
*/
|
||||
children: (context: RepoHeaderContext) => JSX.Element | null
|
||||
|
||||
renderInContextMenu?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,7 +116,7 @@ export interface RepoHeaderContext {
|
||||
actionType: 'nav' | 'dropdown'
|
||||
}
|
||||
|
||||
interface Props extends PlatformContextProps, TelemetryProps, BreadcrumbsProps {
|
||||
interface Props extends PlatformContextProps, BreadcrumbsProps {
|
||||
/** The repoName from the URL */
|
||||
repoName: string
|
||||
|
||||
@ -177,7 +179,18 @@ export const RepoHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
|
||||
const rightActions = useMemo(
|
||||
() =>
|
||||
repoHeaderContributions
|
||||
.filter(({ position }) => position === 'right')
|
||||
.filter(({ position, renderInContextMenu }) => position === 'right' && !renderInContextMenu)
|
||||
.map(({ children, ...rest }) => ({
|
||||
...rest,
|
||||
element: children({ ...context, actionType: isLarge ? 'nav' : 'dropdown' }),
|
||||
})),
|
||||
[context, repoHeaderContributions, isLarge]
|
||||
)
|
||||
|
||||
const rightActionsInContextMenu = useMemo(
|
||||
() =>
|
||||
repoHeaderContributions
|
||||
.filter(({ position, renderInContextMenu }) => position === 'right' && renderInContextMenu)
|
||||
.map(({ children, ...rest }) => ({
|
||||
...rest,
|
||||
element: children({ ...context, actionType: isLarge ? 'nav' : 'dropdown' }),
|
||||
@ -190,7 +203,7 @@ export const RepoHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={props.breadcrumbs}
|
||||
className={classNames(
|
||||
'justify-content-start flex-grow-1',
|
||||
'justify-content-start flex-grow-1 w-auto m-0',
|
||||
!props.forceWrap ? styles.breadcrumbWrap : ''
|
||||
)}
|
||||
/>
|
||||
@ -223,6 +236,9 @@ export const RepoHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
|
||||
{a.element}
|
||||
</li>
|
||||
))}
|
||||
<li className={classNames('nav-item', styles.actionListItem)}>
|
||||
<RepoHeaderContextMenu actions={rightActionsInContextMenu} />
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="navbar-nav">
|
||||
@ -232,7 +248,7 @@ export const RepoHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
|
||||
<Icon aria-hidden={true} svgPath={mdiDotsVertical} />
|
||||
</RepoHeaderActionDropdownToggle>
|
||||
<MenuList position={Position.bottomEnd}>
|
||||
{rightActions.map(a => (
|
||||
{[...rightActionsInContextMenu, ...rightActions].map(a => (
|
||||
<React.Fragment key={a.id}>{a.element}</React.Fragment>
|
||||
))}
|
||||
</MenuList>
|
||||
|
||||
19
client/web/src/repo/RepoHeaderContextMenu.tsx
Normal file
19
client/web/src/repo/RepoHeaderContextMenu.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { Menu, MenuButton, MenuList, Position } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { RepoHeaderContribution } from './RepoHeader'
|
||||
|
||||
interface RepoHeaderContextMenuProps {
|
||||
actions: (Pick<RepoHeaderContribution, 'id' | 'priority'> & { element: JSX.Element | null })[]
|
||||
}
|
||||
|
||||
export const RepoHeaderContextMenu: FC<RepoHeaderContextMenuProps> = ({ actions }) => (
|
||||
<Menu>
|
||||
<MenuButton variant="secondary" outline={true} className="py-0">
|
||||
…
|
||||
</MenuButton>
|
||||
|
||||
<MenuList position={Position.bottom}>{actions.map(action => action.element)}</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
@ -31,7 +31,7 @@ import type { OwnConfigProps } from '../own/OwnConfigProps'
|
||||
import type { SearchStreamingProps } from '../search'
|
||||
import type { RouteV6Descriptor } from '../util/contributions'
|
||||
|
||||
import { GoToPermalinkAction } from './actions/GoToPermalinkAction'
|
||||
import { CopyPermalinkAction } from './actions/CopyPermalinkAction'
|
||||
import type { ResolvedRevision } from './backend'
|
||||
import { RepoRevisionChevronDownIcon, RepoRevisionWrapper } from './components/RepoRevision'
|
||||
import { isPackageServiceType } from './packages/isPackageServiceType'
|
||||
@ -217,19 +217,19 @@ export const RepoRevisionContainer: FC<RepoRevisionContainerProps> = props => {
|
||||
)
|
||||
)}
|
||||
</Routes>
|
||||
{resolvedRevision && !isPackage && (
|
||||
{!isPackage && (
|
||||
<RepoHeaderContributionPortal
|
||||
id="go-to-permalink"
|
||||
priority={3}
|
||||
position="right"
|
||||
priority={2}
|
||||
id="copy-permalink"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{context => (
|
||||
<GoToPermalinkAction
|
||||
key="go-to-permalink"
|
||||
<CopyPermalinkAction
|
||||
key="copy-permalink"
|
||||
telemetryService={props.telemetryService}
|
||||
revision={props.revision}
|
||||
commitID={resolvedRevision.commitID}
|
||||
commitID={resolvedRevision?.commitID}
|
||||
{...context}
|
||||
/>
|
||||
)}
|
||||
|
||||
170
client/web/src/repo/actions/CopyPermalinkAction.tsx
Normal file
170
client/web/src/repo/actions/CopyPermalinkAction.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { mdiLink, mdiChevronDown, mdiContentCopy, mdiCheckBold } from '@mdi/js'
|
||||
import { VisuallyHidden } from '@reach/visually-hidden'
|
||||
import classNames from 'classnames'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { fromEvent } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { isInputElement } from '@sourcegraph/shared/src/util/dom'
|
||||
import {
|
||||
Position,
|
||||
Icon,
|
||||
Link,
|
||||
Button,
|
||||
Menu,
|
||||
ButtonGroup,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
screenReaderAnnounce,
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import { replaceRevisionInURL } from '../../util/url'
|
||||
import { RepoHeaderActionMenuLink } from '../components/RepoHeaderActions'
|
||||
import { RepoActionInfo } from '../RepoActionInfo'
|
||||
import type { RepoHeaderContext } from '../RepoHeader'
|
||||
|
||||
import styles from './actions.module.scss'
|
||||
|
||||
interface CopyPermalinkActionProps extends RepoHeaderContext, TelemetryProps {
|
||||
/**
|
||||
* The current (possibly undefined or non-full-SHA) Git revision.
|
||||
*/
|
||||
revision?: string
|
||||
|
||||
/**
|
||||
* The commit SHA for the revision in the current location (URL).
|
||||
*/
|
||||
commitID?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A repository header action that replaces the revision in the URL with the canonical 40-character
|
||||
* Git commit SHA.
|
||||
*/
|
||||
export const CopyPermalinkAction: React.FunctionComponent<CopyPermalinkActionProps> = props => {
|
||||
const { revision, commitID, actionType, repoName, telemetryService } = props
|
||||
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const fullURL = location.pathname + location.search + location.hash
|
||||
const permalinkURL = useMemo(() => replaceRevisionInURL(fullURL, commitID || ''), [fullURL, commitID])
|
||||
const linkURL = useMemo(() => replaceRevisionInURL(fullURL, revision || ''), [fullURL, revision])
|
||||
const [copiedPermalink, setCopiedPermalink] = useState<boolean>(false)
|
||||
const [copiedLink, setCopiedLink] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger the user presses 'y'.
|
||||
const subscription = fromEvent<KeyboardEvent>(window, 'keydown')
|
||||
.pipe(
|
||||
filter(
|
||||
event =>
|
||||
// 'y' shortcut (if no input element is focused)
|
||||
event.key === 'y' && !!document.activeElement && !isInputElement(document.activeElement)
|
||||
)
|
||||
)
|
||||
.subscribe(event => {
|
||||
event.preventDefault()
|
||||
|
||||
// Replace the revision in the current URL with the new one and push to history.
|
||||
navigate(permalinkURL)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [navigate, permalinkURL])
|
||||
|
||||
const onClick = (): void => {
|
||||
telemetryService.log('PermalinkClicked', { repoName, commitID })
|
||||
}
|
||||
|
||||
if (actionType === 'dropdown') {
|
||||
return (
|
||||
<RepoHeaderActionMenuLink as={Link} file={true} to={permalinkURL} onSelect={onClick}>
|
||||
<Icon aria-hidden={true} svgPath={mdiLink} />
|
||||
<span>Permalink (with full Git commit SHA)</span>
|
||||
</RepoHeaderActionMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
const copyPermalink = (): void => {
|
||||
telemetryService.log('CopyPermalink')
|
||||
copy(permalinkURL)
|
||||
setCopiedPermalink(true)
|
||||
screenReaderAnnounce('Permalink copied to clipboard')
|
||||
|
||||
setTimeout(() => setCopiedPermalink(false), 1000)
|
||||
}
|
||||
|
||||
const copyLink = (): void => {
|
||||
telemetryService.log('CopyLink')
|
||||
copy(linkURL)
|
||||
setCopiedLink(true)
|
||||
screenReaderAnnounce('Link copied to clipboard')
|
||||
|
||||
setTimeout(() => setCopiedLink(false), 1000)
|
||||
}
|
||||
|
||||
const isRevisionTheSameAsCommitID = revision === commitID
|
||||
const copyLinkLabel = copiedLink ? 'Copied!' : isRevisionTheSameAsCommitID ? 'Copy Link' : 'Links'
|
||||
const copyLinkIcon = copiedLink ? mdiCheckBold : mdiContentCopy
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<ButtonGroup>
|
||||
<Button className={classNames('border', styles.permalinkBtn, 'pt-0 pb-0')} onClick={copyLink}>
|
||||
<RepoActionInfo
|
||||
displayName={copyLinkLabel}
|
||||
icon={
|
||||
<Icon
|
||||
svgPath={copyLinkIcon}
|
||||
aria-hidden={true}
|
||||
className={classNames({
|
||||
[styles.checkedIcon]: copiedLink,
|
||||
[styles.repoActionIcon]: !copiedLink,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
{!isRevisionTheSameAsCommitID && (
|
||||
<MenuButton variant="secondary" className={styles.chevronBtn}>
|
||||
<Icon
|
||||
className={styles.chevronBtnIcon}
|
||||
svgPath={mdiChevronDown}
|
||||
inline={false}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
<VisuallyHidden>Actions</VisuallyHidden>
|
||||
</MenuButton>
|
||||
)}
|
||||
{!isRevisionTheSameAsCommitID && (
|
||||
<MenuList position={Position.bottomEnd}>
|
||||
<MenuItem
|
||||
onSelect={copyPermalink}
|
||||
className={classNames(styles.dropdownItem, 'justify-content-start')}
|
||||
>
|
||||
<RepoActionInfo
|
||||
displayName={copiedPermalink ? 'Copied' : 'Copy permalink'}
|
||||
icon={
|
||||
<Icon
|
||||
aria-hidden={true}
|
||||
svgPath={copiedPermalink ? mdiCheckBold : mdiContentCopy}
|
||||
className={classNames({
|
||||
[styles.checkedIcon]: copiedLink,
|
||||
[styles.repoActionIcon]: !copiedLink,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
className={styles.permalinkText}
|
||||
/>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { toLower, upperFirst } from 'lodash'
|
||||
import BitbucketIcon from 'mdi-react/BitbucketIcon'
|
||||
import ExportIcon from 'mdi-react/ExportIcon'
|
||||
@ -20,8 +21,11 @@ import { type ExternalLinkFields, ExternalServiceKind, type RepositoryFields } f
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { fetchCommitMessage, fetchFileExternalLinks } from '../backend'
|
||||
import { RepoHeaderActionAnchor, RepoHeaderActionMenuLink } from '../components/RepoHeaderActions'
|
||||
import { RepoActionInfo } from '../RepoActionInfo'
|
||||
import type { RepoHeaderContext } from '../RepoHeader'
|
||||
|
||||
import styles from './actions.module.scss'
|
||||
|
||||
interface Props extends RevisionSpec, Partial<FileSpec> {
|
||||
repo?: Pick<RepositoryFields, 'name' | 'defaultBranch' | 'externalURLs' | 'externalRepository'> | null
|
||||
filePath?: string
|
||||
@ -171,8 +175,14 @@ export const GoToCodeHostAction: React.FunctionComponent<
|
||||
|
||||
return (
|
||||
<Tooltip content={descriptiveText}>
|
||||
<RepoHeaderActionAnchor {...commonProps}>
|
||||
<Icon as={exportIcon} aria-hidden={true} />
|
||||
<RepoHeaderActionAnchor
|
||||
{...commonProps}
|
||||
className={classNames(commonProps.className, 'd-flex justify-content-center align-items-center')}
|
||||
>
|
||||
<RepoActionInfo
|
||||
displayName={displayName}
|
||||
icon={<Icon as={exportIcon} aria-hidden={true} className={styles.repoActionIcon} />}
|
||||
/>
|
||||
</RepoHeaderActionAnchor>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
|
||||
import { mdiLink } from '@mdi/js'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { fromEvent } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { isInputElement } from '@sourcegraph/shared/src/util/dom'
|
||||
import { Icon, Link, Tooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { replaceRevisionInURL } from '../../util/url'
|
||||
import { RepoHeaderActionButtonLink, RepoHeaderActionMenuLink } from '../components/RepoHeaderActions'
|
||||
import type { RepoHeaderContext } from '../RepoHeader'
|
||||
|
||||
interface GoToPermalinkActionProps extends RepoHeaderContext, TelemetryProps {
|
||||
/**
|
||||
* The current (possibly undefined or non-full-SHA) Git revision.
|
||||
*/
|
||||
revision?: string
|
||||
|
||||
/**
|
||||
* The commit SHA for the revision in the current location (URL).
|
||||
*/
|
||||
commitID: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A repository header action that replaces the revision in the URL with the canonical 40-character
|
||||
* Git commit SHA.
|
||||
*/
|
||||
export const GoToPermalinkAction: React.FunctionComponent<GoToPermalinkActionProps> = props => {
|
||||
const { revision, commitID, actionType, repoName, telemetryService } = props
|
||||
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const fullURL = location.pathname + location.search + location.hash
|
||||
const permalinkURL = useMemo(() => replaceRevisionInURL(fullURL, commitID), [fullURL, commitID])
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger the user presses 'y'.
|
||||
const subscription = fromEvent<KeyboardEvent>(window, 'keydown')
|
||||
.pipe(
|
||||
filter(
|
||||
event =>
|
||||
// 'y' shortcut (if no input element is focused)
|
||||
event.key === 'y' && !!document.activeElement && !isInputElement(document.activeElement)
|
||||
)
|
||||
)
|
||||
.subscribe(event => {
|
||||
event.preventDefault()
|
||||
|
||||
// Replace the revision in the current URL with the new one and push to history.
|
||||
navigate(permalinkURL)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [navigate, permalinkURL])
|
||||
|
||||
if (revision === commitID) {
|
||||
return null // already at the permalink destination
|
||||
}
|
||||
|
||||
const onClick = (): void => {
|
||||
telemetryService.log('PermalinkClicked', { repoName, commitID })
|
||||
}
|
||||
|
||||
if (actionType === 'dropdown') {
|
||||
return (
|
||||
<RepoHeaderActionMenuLink as={Link} file={true} to={permalinkURL} onSelect={onClick}>
|
||||
<Icon aria-hidden={true} svgPath={mdiLink} />
|
||||
<span>Permalink (with full Git commit SHA)</span>
|
||||
</RepoHeaderActionMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content="Permalink (with full Git commit SHA)">
|
||||
<RepoHeaderActionButtonLink aria-label="Permalink" file={false} to={permalinkURL} onSelect={onClick}>
|
||||
<Icon aria-hidden={true} svgPath={mdiLink} />
|
||||
</RepoHeaderActionButtonLink>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { mdiAccountDetails, mdiAccountDetailsOutline } from '@mdi/js'
|
||||
import { mdiGit } from '@mdi/js'
|
||||
|
||||
import { SimpleActionItem } from '@sourcegraph/shared/src/actions/SimpleActionItem'
|
||||
import type { RenderMode } from '@sourcegraph/shared/src/util/url'
|
||||
@ -9,6 +9,9 @@ import { Button, Icon, Tooltip } from '@sourcegraph/wildcard'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { useBlameVisibility } from '../blame/useBlameVisibility'
|
||||
import { RepoHeaderActionAnchor, RepoHeaderActionMenuLink } from '../components/RepoHeaderActions'
|
||||
import { RepoActionInfo } from '../RepoActionInfo'
|
||||
|
||||
import styles from './actions.module.scss'
|
||||
|
||||
interface Props {
|
||||
source?: 'repoHeader' | 'actionItemsBar'
|
||||
@ -16,6 +19,7 @@ interface Props {
|
||||
renderMode?: RenderMode
|
||||
isPackage: boolean
|
||||
}
|
||||
|
||||
export const ToggleBlameAction: React.FC<Props> = props => {
|
||||
const [isBlameVisible, setIsBlameVisible] = useBlameVisibility(props.isPackage)
|
||||
|
||||
@ -37,9 +41,7 @@ export const ToggleBlameAction: React.FC<Props> = props => {
|
||||
}
|
||||
}, [isBlameVisible, setIsBlameVisible])
|
||||
|
||||
const icon = (
|
||||
<Icon aria-hidden={true} svgPath={isBlameVisible && !disabled ? mdiAccountDetails : mdiAccountDetailsOutline} />
|
||||
)
|
||||
const icon = <Icon aria-hidden={true} svgPath={mdiGit} />
|
||||
|
||||
if (props.source === 'actionItemsBar') {
|
||||
return (
|
||||
@ -65,8 +67,15 @@ export const ToggleBlameAction: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<Tooltip content={descriptiveText}>
|
||||
<RepoHeaderActionAnchor onSelect={toggleBlameState} disabled={disabled}>
|
||||
{icon}
|
||||
<RepoHeaderActionAnchor
|
||||
onSelect={toggleBlameState}
|
||||
disabled={disabled}
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<RepoActionInfo
|
||||
displayName="Blame"
|
||||
icon={<Icon aria-hidden={true} svgPath={mdiGit} className={styles.repoActionIcon} />}
|
||||
/>
|
||||
</RepoHeaderActionAnchor>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
37
client/web/src/repo/actions/actions.module.scss
Normal file
37
client/web/src/repo/actions/actions.module.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.repo-action-icon {
|
||||
fill: var(--icon-color) !important;
|
||||
}
|
||||
|
||||
.permalink-text {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.permalink-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.checked-icon {
|
||||
fill: var(--green) !important;
|
||||
}
|
||||
|
||||
.chevron-btn {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 1.5rem;
|
||||
background-color: transparent;
|
||||
|
||||
&-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -73,6 +73,7 @@ import type { HoverThresholdProps } from '../RepoContainer'
|
||||
import type { RepoHeaderContributionsLifecycleProps } from '../RepoHeader'
|
||||
import { RepoHeaderContributionPortal } from '../RepoHeaderContributionPortal'
|
||||
|
||||
import { GoToRawAction } from './actions/GoToRawAction'
|
||||
import { ToggleHistoryPanel } from './actions/ToggleHistoryPanel'
|
||||
import { ToggleLineWrap } from './actions/ToggleLineWrap'
|
||||
import { ToggleRenderedFileMode } from './actions/ToggleRenderedFileMode'
|
||||
@ -80,7 +81,6 @@ import { getModeFromURL } from './actions/utils'
|
||||
import { fetchBlob } from './backend'
|
||||
import { BlobLoadingSpinner } from './BlobLoadingSpinner'
|
||||
import { CodeMirrorBlob, type BlobInfo } from './CodeMirrorBlob'
|
||||
import { GoToRawAction } from './GoToRawAction'
|
||||
import { HistoryAndOwnBar } from './own/HistoryAndOwnBar'
|
||||
import { BlobPanel } from './panel/BlobPanel'
|
||||
import { RenderedFile } from './RenderedFile'
|
||||
@ -363,7 +363,7 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
{window.context.isAuthenticatedUser && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={112}
|
||||
priority={6}
|
||||
id="open-in-editor-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
@ -379,7 +379,7 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
)}
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={111}
|
||||
priority={4}
|
||||
id="toggle-blame-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
@ -410,7 +410,7 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
)}
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={20}
|
||||
priority={5}
|
||||
id="toggle-blob-panel"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
@ -427,9 +427,10 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
{renderMode === 'code' && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={99}
|
||||
priority={9}
|
||||
id="toggle-line-wrap"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
renderInContextMenu={true}
|
||||
>
|
||||
{context => <ToggleLineWrap {...context} key="toggle-line-wrap" onDidUpdate={setWrapCode} />}
|
||||
</RepoHeaderContributionPortal>
|
||||
@ -437,9 +438,10 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={30}
|
||||
priority={8}
|
||||
id="raw-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
renderInContextMenu={true}
|
||||
>
|
||||
{context => (
|
||||
<GoToRawAction
|
||||
@ -543,9 +545,10 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
{blobInfoOrError.richHTML && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={100}
|
||||
priority={10}
|
||||
id="toggle-rendered-file-mode"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
renderInContextMenu={true}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<ToggleRenderedFileMode
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { mdiFileDownloadOutline } from '@mdi/js'
|
||||
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { encodeRepoRevision, type RepoSpec, type RevisionSpec, type FileSpec } from '@sourcegraph/shared/src/util/url'
|
||||
import { Icon, Link, Tooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { RepoHeaderActionAnchor, RepoHeaderActionMenuLink } from '../components/RepoHeaderActions'
|
||||
import type { RepoHeaderContext } from '../RepoHeader'
|
||||
|
||||
interface Props extends RepoSpec, Partial<RevisionSpec>, FileSpec, RepoHeaderContext, TelemetryProps {}
|
||||
|
||||
/**
|
||||
* A repository header action that replaces the blob in the URL with the raw URL.
|
||||
*/
|
||||
export class GoToRawAction extends React.PureComponent<Props> {
|
||||
private onClick(): void {
|
||||
this.props.telemetryService.log('RawFileDownload', {
|
||||
repoName: this.props.repoName,
|
||||
filePath: this.props.filePath,
|
||||
})
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const to = `/${encodeRepoRevision(this.props)}/-/raw/${this.props.filePath}`
|
||||
const descriptiveText = 'Raw (download file)'
|
||||
|
||||
if (this.props.actionType === 'dropdown') {
|
||||
return (
|
||||
<RepoHeaderActionMenuLink
|
||||
as={Link}
|
||||
to={to}
|
||||
target="_blank"
|
||||
file={true}
|
||||
onSelect={this.onClick.bind(this)}
|
||||
download={true}
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiFileDownloadOutline} />
|
||||
<span>{descriptiveText}</span>
|
||||
</RepoHeaderActionMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={descriptiveText}>
|
||||
<RepoHeaderActionAnchor
|
||||
aria-label={descriptiveText}
|
||||
to={to}
|
||||
target="_blank"
|
||||
onClick={this.onClick.bind(this)}
|
||||
download={true}
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiFileDownloadOutline} />
|
||||
</RepoHeaderActionAnchor>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
}
|
||||
44
client/web/src/repo/blob/actions/GoToRawAction.tsx
Normal file
44
client/web/src/repo/blob/actions/GoToRawAction.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { mdiFileDownloadOutline } from '@mdi/js'
|
||||
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { encodeRepoRevision, type RepoSpec, type RevisionSpec, type FileSpec } from '@sourcegraph/shared/src/util/url'
|
||||
import { Icon, Link } from '@sourcegraph/wildcard'
|
||||
|
||||
import { RepoHeaderActionMenuLink } from '../../components/RepoHeaderActions'
|
||||
import type { RepoHeaderContext } from '../../RepoHeader'
|
||||
|
||||
import styles from './actions.module.scss'
|
||||
|
||||
interface Props extends RepoSpec, Partial<RevisionSpec>, FileSpec, RepoHeaderContext, TelemetryProps {}
|
||||
|
||||
/**
|
||||
* A repository header action that replaces the blob in the URL with the raw URL.
|
||||
*/
|
||||
export class GoToRawAction extends React.PureComponent<Props> {
|
||||
private onClick(): void {
|
||||
this.props.telemetryService.log('RawFileDownload', {
|
||||
repoName: this.props.repoName,
|
||||
filePath: this.props.filePath,
|
||||
})
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const to = `/${encodeRepoRevision(this.props)}/-/raw/${this.props.filePath}`
|
||||
return (
|
||||
<RepoHeaderActionMenuLink
|
||||
as={Link}
|
||||
to={to}
|
||||
target="_blank"
|
||||
file={true}
|
||||
onSelect={this.onClick.bind(this)}
|
||||
download={true}
|
||||
className={styles.menuItem}
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiFileDownloadOutline} className={styles.repoActionIcon} />
|
||||
<span>Raw download</span>
|
||||
</RepoHeaderActionMenuLink>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -17,9 +17,12 @@ import { Icon, Tooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { RepoHeaderActionButtonLink, RepoHeaderActionMenuItem } from '../../components/RepoHeaderActions'
|
||||
import { RepoActionInfo } from '../../RepoActionInfo'
|
||||
import type { RepoHeaderContext } from '../../RepoHeader'
|
||||
import type { BlobPanelTabID } from '../panel/BlobPanel'
|
||||
|
||||
import styles from './actions.module.scss'
|
||||
|
||||
/**
|
||||
* A repository header action that toggles the visibility of the history panel.
|
||||
*/
|
||||
@ -113,8 +116,12 @@ export class ToggleHistoryPanel extends React.PureComponent<
|
||||
file={false}
|
||||
onSelect={this.onClick}
|
||||
disabled={disabled}
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiHistory} />
|
||||
<RepoActionInfo
|
||||
displayName="History"
|
||||
icon={<Icon aria-hidden={true} svgPath={mdiHistory} className={styles.repoActionIcon} />}
|
||||
/>
|
||||
</RepoHeaderActionButtonLink>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { mdiWrap, mdiWrapDisabled } from '@mdi/js'
|
||||
import { mdiWrap } from '@mdi/js'
|
||||
import { fromEvent, Subject, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
import { WrapDisabledIcon } from '@sourcegraph/shared/src/components/icons'
|
||||
import { Icon, Tooltip } from '@sourcegraph/wildcard'
|
||||
import { Icon, Button } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { RepoHeaderActionButtonLink, RepoHeaderActionMenuItem } from '../../components/RepoHeaderActions'
|
||||
import { RepoHeaderActionMenuItem } from '../../components/RepoHeaderActions'
|
||||
import type { RepoHeaderContext } from '../../RepoHeader'
|
||||
|
||||
import styles from './actions.module.scss'
|
||||
|
||||
/**
|
||||
* A repository header action that toggles the line wrapping behavior for long lines in code files.
|
||||
*/
|
||||
@ -72,29 +74,16 @@ export class ToggleLineWrap extends React.PureComponent<
|
||||
}
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
if (this.props.actionType === 'dropdown') {
|
||||
return (
|
||||
<RepoHeaderActionMenuItem file={true} onSelect={this.onClick}>
|
||||
<Icon
|
||||
as={this.state.value ? WrapDisabledIcon : undefined}
|
||||
svgPath={!this.state.value ? mdiWrap : undefined}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
<span>{this.state.value ? 'Disable' : 'Enable'} wrapping long lines (Alt+Z/Opt+Z)</span>
|
||||
</RepoHeaderActionMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={`${this.state.value ? 'Disable' : 'Enable'} wrapping long lines (Alt+Z/Opt+Z)`}>
|
||||
<RepoHeaderActionButtonLink
|
||||
aria-label={this.state.value ? 'Disable' : 'Enable'}
|
||||
file={false}
|
||||
onSelect={this.onClick}
|
||||
>
|
||||
<Icon svgPath={this.state.value ? mdiWrapDisabled : mdiWrap} aria-hidden={true} />
|
||||
</RepoHeaderActionButtonLink>
|
||||
</Tooltip>
|
||||
<RepoHeaderActionMenuItem file={true} onSelect={this.onClick} as={Button} className={styles.menuItem}>
|
||||
<Icon
|
||||
as={this.state.value ? WrapDisabledIcon : undefined}
|
||||
svgPath={!this.state.value ? mdiWrap : undefined}
|
||||
aria-hidden={true}
|
||||
className={styles.repoActionIcon}
|
||||
/>
|
||||
<span>{this.state.value ? 'Disable' : 'Enable'} wrapping long lines</span>
|
||||
</RepoHeaderActionMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
16
client/web/src/repo/blob/actions/actions.module.scss
Normal file
16
client/web/src/repo/blob/actions/actions.module.scss
Normal file
@ -0,0 +1,16 @@
|
||||
@import 'wildcard/src/global-styles/breakpoints';
|
||||
|
||||
.repo-action-icon {
|
||||
fill: var(--icon-color) !important;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
.menu-item {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
@ -9,3 +9,4 @@ export { createTether } from './services/tether-registry'
|
||||
// Shared type interfaces
|
||||
export type { Tether } from './services/types'
|
||||
export type { TetherInstanceAPI } from './services/tether-registry'
|
||||
export { TetherAPI } from './services/tether-registry'
|
||||
|
||||
@ -1,6 +1,31 @@
|
||||
import { render } from './tether-render'
|
||||
import type { Tether } from './types'
|
||||
|
||||
interface TetherAPIObject {
|
||||
tethers: TetherInstanceAPI[]
|
||||
registerTether: (tether: TetherInstanceAPI) => void
|
||||
unregisterTether: (tether: TetherInstanceAPI) => void
|
||||
update: () => void
|
||||
}
|
||||
|
||||
export const TetherAPI: TetherAPIObject = {
|
||||
tethers: [],
|
||||
|
||||
registerTether(tether: TetherInstanceAPI) {
|
||||
this.tethers.push(tether)
|
||||
},
|
||||
|
||||
unregisterTether(tether: TetherInstanceAPI) {
|
||||
TetherAPI.tethers = TetherAPI.tethers.filter(tetherAPI => tetherAPI !== tether)
|
||||
},
|
||||
|
||||
update() {
|
||||
for (const tetherAPI of this.tethers) {
|
||||
tetherAPI.forceUpdate()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export interface TetherInstanceAPI {
|
||||
unsubscribe: () => void
|
||||
forceUpdate: () => void
|
||||
@ -30,8 +55,9 @@ export function createTether(tether: Tether): TetherInstanceAPI {
|
||||
// Synthetic runs without target for the initial tooltip positioning render
|
||||
requestAnimationFrame(() => render(tether, null))
|
||||
|
||||
return {
|
||||
const tetherInstanceAPI: TetherInstanceAPI = {
|
||||
unsubscribe: () => {
|
||||
TetherAPI.unregisterTether(tetherInstanceAPI)
|
||||
window.removeEventListener('resize', eventHandler, true)
|
||||
document.removeEventListener('scroll', eventHandler, true)
|
||||
document.removeEventListener('click', eventHandler, true)
|
||||
@ -40,4 +66,8 @@ export function createTether(tether: Tether): TetherInstanceAPI {
|
||||
},
|
||||
forceUpdate: () => requestAnimationFrame(() => render(tether, null)),
|
||||
}
|
||||
|
||||
TetherAPI.registerTether(tetherInstanceAPI)
|
||||
|
||||
return tetherInstanceAPI
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ export {
|
||||
Flipping,
|
||||
Strategy,
|
||||
Overlapping,
|
||||
TetherAPI,
|
||||
} from './Popover'
|
||||
export { Collapse, CollapseHeader, CollapsePanel } from './Collapse'
|
||||
export {
|
||||
@ -109,11 +110,10 @@ export type { ButtonLinkProps } from './ButtonLink'
|
||||
export type { SelectProps, InputProps } from './Form'
|
||||
export type { Series, SeriesLikeChart, CategoricalLikeChart, LineChartProps, BarChartProps } from './Charts'
|
||||
export type { LinkProps } from './Link'
|
||||
export type { PopoverOpenEvent, Rectangle, TetherInstanceAPI } from './Popover'
|
||||
export type { PopoverOpenEvent, Rectangle, TetherInstanceAPI, Point } from './Popover'
|
||||
export type { MenuLinkProps, MenuItemProps } from './Menu'
|
||||
export type { TabsProps, TabListProps, TabProps, TabPanelProps, TabPanelsProps } from './Tabs'
|
||||
export type { IconProps, IconType } from './Icon'
|
||||
export type { Point } from './Popover'
|
||||
export type { TreeNode, TreeProps, TreeRef } from './Tree'
|
||||
export type { TooltipProps, TooltipOpenEvent } from './Tooltip'
|
||||
export type { HeadingProps, HeadingElement } from './Typography'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user