Add extension hovers to Bitbucket Server/Data Center v7+ (#25020)

This commit is contained in:
TJ Kandala 2021-10-06 17:38:39 -04:00 committed by GitHub
parent 246c9c6581
commit 3b08d82a89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 170 additions and 45 deletions

View File

@ -12,7 +12,7 @@ import { createNotificationClassNameGetter } from '../shared/getNotificationClas
import { ViewResolver } from '../shared/views'
import { getContext } from './context'
import { diffDOMFunctions, singleFileDOMFunctions } from './domFunctions'
import { diffDOMFunctions, newDiffDOMFunctions, singleFileDOMFunctions } from './domFunctions'
import {
resolveCommitViewFileInfo,
resolveCompareFileInfo,
@ -25,13 +25,16 @@ import { isCommitsView, isCompareView, isPullRequestView, isSingleFileView } fro
/**
* Gets or creates the toolbar mount for allcode views.
*/
export const getToolbarMount = (codeView: HTMLElement): HTMLElement => {
export const getToolbarMount = (
codeView: HTMLElement,
fileToolbarSelector = '.file-toolbar .secondary'
): HTMLElement => {
const existingMount = codeView.querySelector<HTMLElement>('.sg-toolbar-mount')
if (existingMount) {
return existingMount
}
const fileActions = codeView.querySelector<HTMLElement>('.file-toolbar .secondary')
const fileActions = codeView.querySelector<HTMLElement>(fileToolbarSelector)
if (!fileActions) {
throw new Error('Unable to find mount location')
}
@ -80,10 +83,6 @@ const createPositionAdjuster = (
return of(newPosition)
}
const toolbarButtonProps = {
className: 'aui-button',
}
/**
* A code view spec for single file code view in the "source" view (not diff).
*/
@ -92,14 +91,14 @@ const singleFileSourceCodeView: Omit<CodeView, 'element'> = {
dom: singleFileDOMFunctions,
resolveFileInfo: resolveFileInfoForSingleFileSourceView,
getPositionAdjuster: () => createPositionAdjuster(singleFileDOMFunctions),
toolbarButtonProps,
}
const baseDiffCodeView: Omit<CodeView, 'element' | 'resolveFileInfo'> = {
getToolbarMount,
dom: diffDOMFunctions,
getPositionAdjuster: () => createPositionAdjuster(diffDOMFunctions),
toolbarButtonProps,
// Bitbucket diff views are not tokenized.
overrideTokenize: true,
}
/**
* A code view spec for a single file "diff to previous" view
@ -158,6 +157,31 @@ const codeViewResolver: ViewResolver<CodeView> = {
},
}
/**
* New diff code view resolver.
* As of Bitbucket v7.11.2, this is only used for the pull request page.
*/
const diffCodeViewResolver: ViewResolver<CodeView> = {
selector: '.change-view',
resolveView: element => ({ element, ...newDiffCodeView }),
}
const newDiffToolbarButtonProps = {
listItemClass: 'action-nav-item--bitbucket-server-new-diff',
actionItemClass: 'action-item--bitbucket-server-new-diff',
}
/**
* New diff code view element.
* As of Bitbucket v7.11.2, this is only used for the pull request page.
*/
const newDiffCodeView: Omit<CodeView, 'element'> = {
resolveFileInfo: resolvePullRequestFileInfo,
getToolbarMount: codeView => getToolbarMount(codeView, '.change-header .diff-actions'),
toolbarButtonProps: newDiffToolbarButtonProps,
dom: newDiffDOMFunctions,
}
const getCommandPaletteMount: MountGetter = (container: HTMLElement): HTMLElement | null => {
const headerElement = querySelectorOrSelf(container, '.aui-header-primary .aui-nav')
if (!headerElement) {
@ -208,7 +232,7 @@ export const bitbucketServerCodeHost: CodeHost = {
type: 'bitbucket-server',
name: 'Bitbucket Server',
check: checkIsBitbucket,
codeViewResolvers: [codeViewResolver],
codeViewResolvers: [codeViewResolver, diffCodeViewResolver],
getCommandPaletteMount,
notificationClassNames,
commandPaletteClassProps: {

View File

@ -106,3 +106,71 @@ export const diffDOMFunctions: DOMFunctions = {
},
isFirstCharacterDiffIndicator: () => false,
}
const newGetDiffLineElementFromLineNumber = (codeView: HTMLElement, line: number, part?: DiffPart): HTMLElement => {
for (const lineNumberElement of codeView.querySelectorAll<HTMLElement>('.diff-line-number')) {
// For unchanged lines, `textContent` will be e.g. ' 4 4 ' (in one element).
const lineNumberString = lineNumberElement.textContent?.trim().split(' ')[0]
if (!lineNumberString) {
continue
}
const lineNumber = parseInt(lineNumberString, 10)
if (!isNaN(lineNumber) && lineNumber === line) {
const lineElement = lineNumberElement
.closest('tr')
?.querySelector<HTMLElement>(`.diff-line[data-diff-line="${part === 'head' ? 'TO' : 'FROM'}"]`)
if (!lineElement) {
throw new Error('Could not find lineElem from lineNumElem')
}
return lineElement
}
}
throw new Error(`Could not locate line number element for line ${line}, part: ${String(part)}`)
}
export const newDiffDOMFunctions: DOMFunctions = {
getCodeElementFromTarget: target => {
const container = target.closest<HTMLElement>('.diff-line')
return container
},
getLineNumberFromCodeElement: codeElement => {
const lineNumberElement = codeElement
.closest('tr')
?.querySelector<HTMLElement>('.diff-gutter .diff-line-number')
if (lineNumberElement) {
const lineNumber = parseInt((lineNumberElement.textContent || '').trim(), 10)
if (!isNaN(lineNumber)) {
return lineNumber
}
}
throw new Error('Could not find line number element for code element')
},
getDiffCodePart: codeElement => {
const diffLines = codeElement.closest('tr')?.querySelectorAll<HTMLElement>('.diff-line')
if (!diffLines || diffLines.length === 0) {
// Shouldn't happen since `codeElement` should have the .diff-line class.
throw new Error('Could not find diff lines for code element')
}
if (diffLines.length === 1) {
return codeElement.classList.contains('added-line') ? 'head' : 'base'
}
const [baseLine, headLine] = diffLines
if (codeElement === baseLine) {
return 'base'
}
if (codeElement === headLine) {
return 'head'
}
// Fallback: we can use head hovers for unchanged lines from base side.
return codeElement.classList.contains('removed-line') ? 'base' : 'head'
},
getLineElementFromLineNumber: newGetDiffLineElementFromLineNumber,
getCodeElementFromLineNumber: newGetDiffLineElementFromLineNumber,
}

View File

@ -267,7 +267,8 @@ export const getFileInfoWithoutCommitIDsFromMultiFileDiffCodeView = (
baseFilePath: string
} => {
// Get the file path from the breadcrumbs
const breadcrumbsElement = codeViewElement.querySelector('.breadcrumbs')
const breadcrumbsElement =
codeViewElement.querySelector('.breadcrumbs') ?? codeViewElement.querySelector('.file-breadcrumbs')
if (!breadcrumbsElement) {
throw new Error('Could not find diff code view breadcrumbs element through selector .breadcrumbs')
}

View File

@ -40,6 +40,27 @@
margin-top: 5px;
}
.action-nav-item--bitbucket-server-new-diff {
margin: 0 8px 0 2px;
}
.action-item--bitbucket-server-new-diff {
background-color: rgba(9, 30, 66, 0.04);
color: rgba(66, 82, 110);
transition: background 0.1s ease-out 0s, box-shadow 0.15s cubic-bezier(0.47, 0.03, 0.49, 1.38) 0s;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 3px;
height: 100%;
width: 24px;
padding: 0 2px;
&:hover {
background: rgba(9, 30, 66, 0.08);
}
}
// Use flexbox instead of float, so we can handle wrapping action items
.file-toolbar {
display: flex;

View File

@ -68,15 +68,10 @@ export function createFileActionsToolbarMount(codeView: HTMLElement): HTMLElemen
return mountElement
}
const toolbarButtonProps = {
className: 'btn btn-sm tooltipped tooltipped-s',
}
const diffCodeView: Omit<CodeView, 'element'> = {
dom: diffDomFunctions,
getToolbarMount: createFileActionsToolbarMount,
resolveFileInfo: resolveDiffFileInfo,
toolbarButtonProps,
getScrollBoundaries: codeView => {
const fileHeader = codeView.querySelector<HTMLElement>('.file-header')
if (!fileHeader) {
@ -95,7 +90,6 @@ const singleFileCodeView: Omit<CodeView, 'element'> = {
dom: singleFileDOMFunctions,
getToolbarMount: createFileActionsToolbarMount,
resolveFileInfo,
toolbarButtonProps,
getSelections: getSelectionsFromHash,
observeSelections: observeSelectionsFromHash,
}
@ -138,7 +132,6 @@ const searchResultCodeViewResolver = toCodeViewResolver('.code-list-item', {
dom: searchCodeSnippetDOMFunctions,
getPositionAdjuster: getSnippetPositionAdjuster,
resolveFileInfo: resolveSnippetFileInfo,
toolbarButtonProps,
})
const snippetCodeView: Omit<CodeView, 'element'> = {

View File

@ -15,10 +15,6 @@ import { getCommandPaletteMount } from './extensions'
import { resolveCommitFileInfo, resolveDiffFileInfo, resolveFileInfo } from './fileInfo'
import { getPageInfo, GitLabPageKind, getFilePathsFromCodeView } from './scrape'
const toolbarButtonProps = {
className: 'btn btn-default btn-sm',
}
export function checkIsGitlab(): boolean {
return !!document.head.querySelector('meta[content="GitLab"]')
}
@ -65,7 +61,6 @@ const singleFileCodeView: Omit<CodeView, 'element'> = {
dom: singleFileDOMFunctions,
getToolbarMount,
resolveFileInfo,
toolbarButtonProps,
getSelections: getSelectionsFromHash,
observeSelections: observeSelectionsFromHash,
}
@ -82,7 +77,6 @@ const mergeRequestCodeView: Omit<CodeView, 'element'> = {
dom: diffDOMFunctions,
getToolbarMount,
resolveFileInfo: resolveDiffFileInfo,
toolbarButtonProps,
getScrollBoundaries: getFileTitle,
}
@ -90,7 +84,6 @@ const commitCodeView: Omit<CodeView, 'element'> = {
dom: diffDOMFunctions,
getToolbarMount,
resolveFileInfo: resolveCommitFileInfo,
toolbarButtonProps,
getScrollBoundaries: getFileTitle,
}

View File

@ -81,9 +81,6 @@ const getPositionAdjuster = (
})
)
const toolbarButtonProps = {
className: 'button grey button-grey has-icon has-text phui-button-default msl',
}
export const commitCodeView = {
dom: diffDomFunctions,
resolveFileInfo: resolveRevisionFileInfo,
@ -106,7 +103,6 @@ export const commitCodeView = {
return mount
},
toolbarButtonProps,
}
export const diffCodeView = {
@ -129,7 +125,6 @@ export const diffCodeView = {
mountLocation.prepend(mount, ' ')
return mount
},
toolbarButtonProps,
isDiff: true,
}
@ -163,7 +158,6 @@ const diffusionSourceCodeViewResolver = toCodeViewResolver('.diffusion-source',
return mount
},
toolbarButtonProps,
})
// Matches Diffusion single file code views on recent Phabricator versions.

View File

@ -1000,6 +1000,7 @@ export function handleCodeHost({
getPositionAdjuster,
getToolbarMount,
toolbarButtonProps,
overrideTokenize,
} = codeViewEvent
const initializeModelAndViewerForFileInfo = async (
@ -1211,7 +1212,9 @@ export function handleCodeHost({
positionEvents: of(element).pipe(
findPositionsFromEvents({
domFunctions,
tokenize: codeHost.codeViewsRequireTokenization !== false,
tokenize: !!(typeof overrideTokenize === 'boolean'
? overrideTokenize
: codeHost.codeViewsRequireTokenization),
})
),
resolveContext,
@ -1219,6 +1222,7 @@ export function handleCodeHost({
scrollBoundaries: codeViewEvent.getScrollBoundaries
? codeViewEvent.getScrollBoundaries(codeViewEvent.element)
: [],
overrideTokenize,
})
}
})
@ -1232,6 +1236,10 @@ export function handleCodeHost({
render(
<CodeViewToolbar
{...codeHost.codeViewToolbarClassProps}
actionItemClass={
codeViewEvent.toolbarButtonProps?.actionItemClass ??
codeHost.codeViewToolbarClassProps?.actionItemClass
}
hideActions={hideActions}
fileInfoOrError={diffOrBlobInfo}
sourcegraphURL={sourcegraphURL}

View File

@ -37,6 +37,11 @@ export interface CodeView {
element: HTMLElement
/** The DOMFunctions for the code view. */
dom: DOMFunctions
/**
* Whether this code view needs to be tokenized.
* Used in favor of the `codeViewsRequireTokenization` value for the code host.
*/
overrideTokenize?: boolean
/**
* Finds or creates a DOM element where we should inject the
* `CodeViewToolbar`. This function is responsible for ensuring duplicate

View File

@ -19,7 +19,8 @@ import { OpenDiffOnSourcegraph } from './OpenDiffOnSourcegraph'
import { OpenOnSourcegraph } from './OpenOnSourcegraph'
export interface ButtonProps {
className?: string
listItemClass?: string
actionItemClass?: string
}
export interface CodeViewToolbarClassProps extends ActionNavItemsClassProps {
@ -46,6 +47,9 @@ export interface CodeViewToolbarProps
*/
fileInfoOrError: DiffOrBlobInfo<FileInfoWithContent> | ErrorLike
/**
* Code-view specific className overrides.
*/
buttonProps?: ButtonProps
onSignInClose: () => void
location: H.Location
@ -57,7 +61,11 @@ export const CodeViewToolbar: React.FunctionComponent<CodeViewToolbarProps> = pr
{!props.hideActions && (
<ActionsNavItems
{...props}
listItemClass={classNames('code-view-toolbar__item', props.listItemClass)}
listItemClass={classNames(
'code-view-toolbar__item',
props.buttonProps?.listItemClass ?? props.listItemClass
)}
actionItemClass={classNames(props.buttonProps?.actionItemClass ?? props.actionItemClass)}
menu={ContributableMenu.EditorTitle}
extensionsController={props.extensionsController}
platformContext={props.platformContext}
@ -70,18 +78,23 @@ export const CodeViewToolbar: React.FunctionComponent<CodeViewToolbarProps> = pr
<SignInButton
sourcegraphURL={props.sourcegraphURL}
onSignInClose={props.onSignInClose}
className={props.actionItemClass}
className={classNames(props.buttonProps?.actionItemClass ?? props.actionItemClass)}
iconClassName={props.actionItemIconClass}
/>
) : null
) : (
<>
{!('blob' in props.fileInfoOrError) && props.fileInfoOrError.head && props.fileInfoOrError.base && (
<li className={classNames('code-view-toolbar__item', props.listItemClass)}>
<li
className={classNames(
'code-view-toolbar__item',
props.buttonProps?.listItemClass ?? props.listItemClass
)}
>
<OpenDiffOnSourcegraph
ariaLabel="View file diff on Sourcegraph"
platformContext={props.platformContext}
className={props.actionItemClass}
className={classNames(props.buttonProps?.actionItemClass ?? props.actionItemClass)}
iconClassName={props.actionItemIconClass}
openProps={{
sourcegraphURL: props.sourcegraphURL,
@ -100,10 +113,15 @@ export const CodeViewToolbar: React.FunctionComponent<CodeViewToolbarProps> = pr
// Only show the "View file" button if we were able to fetch the file contents
// from the Sourcegraph instance
'blob' in props.fileInfoOrError && props.fileInfoOrError.blob.content !== undefined && (
<li className={classNames('code-view-toolbar__item', props.listItemClass)}>
<li
className={classNames(
'code-view-toolbar__item',
props.buttonProps?.actionItemClass ?? props.listItemClass
)}
>
<OpenOnSourcegraph
ariaLabel="View file on Sourcegraph"
className={props.actionItemClass}
className={classNames(props.buttonProps?.actionItemClass ?? props.actionItemClass)}
iconClassName={props.actionItemIconClass}
openProps={{
sourcegraphURL: props.sourcegraphURL,

View File

@ -327,7 +327,7 @@
"@reach/visually-hidden": "^0.15.2",
"@sentry/browser": "^6.13.2",
"@slimsag/react-shortcuts": "^1.2.1",
"@sourcegraph/codeintellify": "^7.2.2",
"@sourcegraph/codeintellify": "^7.3.0",
"@sourcegraph/extension-api-classes": "^1.1.0",
"@sourcegraph/react-loading-spinner": "0.0.7",
"@sqs/jsonc-parser": "^1.0.3",

View File

@ -3383,16 +3383,16 @@
dependencies:
prop-types "^15.6.2"
"@sourcegraph/codeintellify@^7.2.2":
version "7.2.2"
resolved "https://registry.npmjs.org/@sourcegraph/codeintellify/-/codeintellify-7.2.2.tgz#6035801a6928d9f61d69fd0dcc12d2ad37284b3b"
integrity sha512-L88Bdid3gjADRgRSutO+tZkRgEbHPH/tOWoqNC+NTh6onCGpsBSBWpy7mauvDI3GTyhuGsgFBFsXpksp8oHbZA==
"@sourcegraph/codeintellify@^7.3.0":
version "7.3.0"
resolved "https://registry.npmjs.org/@sourcegraph/codeintellify/-/codeintellify-7.3.0.tgz#bfe3018f9a7db6741120724495ca47e971f51603"
integrity sha512-Y8b66rxNiUr7SbT9agyjGgGFVivlwF5KMScnDYrRyuvkXmjxEankMdlRczjGXAxHdiH5+SFPOq86ILe9i+qnVA==
dependencies:
"@sourcegraph/event-positions" "^1.0.4"
"@sourcegraph/extension-api-types" "^2.1.0"
lodash "^4.17.10"
rxjs "^6.5.5"
sourcegraph "^24.0.0 || ^25.0.0"
sourcegraph "^24.0.0 || ^25.0.0 || ^25.0.0"
ts-key-enum "^2.0.0"
"@sourcegraph/eslint-config@^0.25.1":