Implememnt diff view in editing file

This commit is contained in:
“tan-nhu” 2023-04-28 10:23:12 -07:00
parent 615ac704d6
commit 821d79d53c
12 changed files with 193 additions and 162 deletions

View File

@ -23,12 +23,7 @@ import { useEventListener } from 'hooks/useEventListener'
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import type { DiffFileEntry } from 'utils/types'
import {
DIFF2HTML_CONFIG,
PR_CODE_COMMENT_PAYLOAD_VERSION,
PullRequestCodeCommentPayload,
ViewStyle
} from 'components/DiffViewer/DiffViewerUtils'
import { DIFF2HTML_CONFIG, PullRequestCodeCommentPayload, ViewStyle } from 'components/DiffViewer/DiffViewerUtils'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import type { TypesPullReq, TypesPullReqActivity } from 'services/code'
import { useShowRequestError } from 'hooks/useShowRequestError'
@ -118,14 +113,10 @@ export const Changes: React.FC<ChangesProps> = ({
const fileId = changedFileId([diff.oldName, diff.newName])
const containerId = `container-${fileId}`
const contentId = `content-${fileId}`
const fileTitle = diff.isDeleted
? diff.oldName
: diff.isRename
? `${diff.oldName} -> ${diff.newName}`
: diff.newName
const filePath = diff.isDeleted ? diff.oldName : diff.newName
const fileActivities: TypesPullReqActivity[] | undefined = activities?.filter(activity => {
const payload = activity.payload as PullRequestCodeCommentPayload
return payload?.file_id === fileId && payload?.version === PR_CODE_COMMENT_PAYLOAD_VERSION
return payload?.file_id === fileId
})
return {
@ -133,7 +124,7 @@ export const Changes: React.FC<ChangesProps> = ({
containerId,
contentId,
fileId,
fileTitle,
filePath,
fileActivities: fileActivities || [],
activities: activities || []
}

View File

@ -24,7 +24,7 @@ import type { DiffFileEntry } from 'utils/types'
import { useConfirmAct } from 'hooks/useConfirmAction'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { useAppContext } from 'AppContext'
import type { TypesPullReq, TypesPullReqActivity } from 'services/code'
import type { OpenapiCommentCreatePullReqRequest, TypesPullReq, TypesPullReqActivity } from 'services/code'
import { getErrorMessage } from 'utils/Utils'
import { CopyButton } from 'components/CopyButton/CopyButton'
import { AppWrapper } from 'App'
@ -36,9 +36,6 @@ import {
DiffCommentItem,
DIFF_VIEWER_HEADER_HEIGHT,
getCommentLineInfo,
getDiffHTMLSnapshotFromRow,
getRawTextInRange,
PR_CODE_COMMENT_PAYLOAD_VERSION,
PullRequestCodeCommentPayload,
renderCommentOppositePlaceHolder,
ViewStyle
@ -308,22 +305,33 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
switch (action) {
case CommentAction.NEW: {
// lineNumberRange can be used to allow multiple-line selection when commenting in the future
const lineNumberRange = [comment.lineNumber]
const payload: PullRequestCodeCommentPayload = {
type: CommentType.CODE_COMMENT,
version: PR_CODE_COMMENT_PAYLOAD_VERSION,
file_id: diff.fileId,
file_title: diff.fileTitle,
language: diff.language || '',
is_on_left: comment.left,
at_line_number: comment.lineNumber,
line_number_range: lineNumberRange,
range_text_content: getRawTextInRange(diff, lineNumberRange),
diff_html_snapshot: getDiffHTMLSnapshotFromRow(rowElement)
const commentPayload: OpenapiCommentCreatePullReqRequest = {
line_start: comment.lineNumber,
line_end: comment.lineNumber,
line_start_new: !comment.left,
line_end_new: !comment.left,
path: diff.filePath,
source_commit_sha: pullRequestMetadata?.merge_sha || '',
target_commit_sha: pullRequestMetadata?.merge_target_sha || '',
text: value
}
await saveComment({ type: CommentType.CODE_COMMENT, text: value, payload })
// lineNumberRange can be used to allow multiple-line selection when commenting in the future
// const lineNumberRange = [comment.lineNumber]
// const payload: PullRequestCodeCommentPayload = {
// type: CommentType.CODE_COMMENT,
// version: PR_CODE_COMMENT_PAYLOAD_VERSION,
// file_id: diff.fileId,
// file_title: diff.filePath,
// language: diff.language || '',
// is_on_left: comment.left,
// at_line_number: comment.lineNumber,
// line_number_range: lineNumberRange,
// range_text_content: getRawTextInRange(diff, lineNumberRange),
// diff_html_snapshot: getDiffHTMLSnapshotFromRow(rowElement)
// }
await saveComment({ type: CommentType.CODE_COMMENT, ...commentPayload })
.then((newComment: TypesPullReqActivity) => {
updatedItem = activityToCommentItem(newComment)
})
@ -472,12 +480,12 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
to={routes.toCODERepository({
repoPath: repoMetadata.path as string,
gitRef: pullRequestMetadata?.source_branch,
resourcePath: diff.fileTitle
resourcePath: diff.isRename ? diff.newName : diff.filePath
})}>
{diff.fileTitle}
{diff.isRename ? `${diff.oldName} -> ${diff.newName}` : diff.filePath}
</Link>
</Text>
<CopyButton content={diff.fileTitle} icon={CodeIcon.Copy} size={ButtonSize.SMALL} />
<CopyButton content={diff.filePath} icon={CodeIcon.Copy} size={ButtonSize.SMALL} />
<FlexExpander />
<Render when={!readOnly}>

View File

@ -21,8 +21,6 @@ export enum CommentType {
STATE_CHANGE = 'state-change'
}
export const PR_CODE_COMMENT_PAYLOAD_VERSION = '0.1'
export interface PullRequestCodeCommentPayload {
type: CommentType
version: string // used to avoid rendering old payload structure
@ -200,78 +198,6 @@ export const activityToCommentItem = (activity: TypesPullReqActivity): CommentIt
payload: activity
})
/**
* Take a small HTML snapshot of a diff in order to render code comment.
* @param atRow Row element where the comment is placed.
* @param maxNumberOfLines Maximum number of lines to take.
* @returns HTML content of the diff.
*/
export function getDiffHTMLSnapshotFromRow(atRow: HTMLTableRowElement, maxNumberOfLines = 5) {
let linesCapturedCount = 0
const diffSnapshot = [atRow.outerHTML.trim()]
let prev = atRow.previousElementSibling
while (prev && linesCapturedCount < maxNumberOfLines) {
if (!prev.hasAttribute('data-annotated-line') && !prev.hasAttribute('data-place-holder-for-line')) {
// Don't count empty lines
const textContent = prev.textContent?.replace(/\s/g, '')
if (textContent?.length && textContent !== '+') {
linesCapturedCount++
}
if (textContent !== '+') {
diffSnapshot.unshift((prev.outerHTML || '').trim())
}
}
prev = prev.previousElementSibling
}
return diffSnapshot.join('')
}
// export function getDiffHTMLSnapshotFromDiff(diff: DiffFileEntry, lineNumberRange: number[], isOnLeft: boolean) {
// const lines = diff?.blocks.reduce((group, item) => {
// group = group.concat(item.lines)
// return group
// }, [] as DiffLine[])
// const lastIndex = lines.findIndex(line =>
// lineNumberRange.includes((isOnLeft ? line.oldNumber : line.newNumber) as number)
// )
// const startIndex = Math.max(0, lastIndex - 5)
// const copiedLines = lines.slice(startIndex, lastIndex)
// const copiedDiff: DiffFileEntry = {
// ...diff,
// blocks: [
// {
// header: '',
// lines: copiedLines,
// newStartLine: copiedLines[0].newNumber as number,
// oldStartLine: copiedLines[0].oldNumber as number
// }
// ]
// }
// // console.log({ isOnLeft, startIndex, lastIndex, copiedLines, lines, lineNumberRange })
// const div = document.createElement('div')
// new Diff2HtmlUI(div, [copiedDiff], Object.assign({}, DIFF2HTML_CONFIG, { outputFormat: 'line-by-line' })).draw()
// return div.querySelector('table')?.outerHTML || ''
// }
export function getRawTextInRange(diff: DiffFileEntry, lineNumberRange: number[]) {
return (
// TODO: This is wrong, blocks can have multiple items, not one
(diff?.blocks[0]?.lines || [])
.filter(line => lineNumberRange.includes(line.newNumber as number))
.map(line => line.content)
.join('\n') || ''
)
}
export function activitiesToDiffCommentItems(diff: DiffFileEntry): DiffCommentItem<TypesPullReqActivity>[] {
return (
diff.fileActivities?.map(activity => {

View File

@ -63,7 +63,8 @@ export const OptionsMenuButton = ({
item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>,
'isDanger',
'hasIcon',
'iconName'
'iconName',
'iconSize'
)}
/>
)

View File

@ -1,9 +1,9 @@
import React, { useEffect } from 'react'
import { Container } from '@harness/uicore'
import React, { useEffect, useState } from 'react'
import type monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'
import MonacoEditor from 'react-monaco-editor'
import MonacoEditor, { MonacoDiffEditor } from 'react-monaco-editor'
import { noop } from 'lodash-es'
import { SourceCodeEditorProps, PLAIN_TEXT } from 'utils/Utils'
import { useEventListener } from 'hooks/useEventListener'
export const MonacoEditorOptions = {
ignoreTrimWhitespace: true,
@ -58,12 +58,12 @@ export default function MonacoSourceCodeEditor({
language = PLAIN_TEXT,
lineNumbers = true,
readOnly = false,
className,
height,
autoHeight,
wordWrap = true,
onChange = noop
}: SourceCodeEditorProps) {
const [editor, setEditor] = useState<monacoEditor.editor.IStandaloneCodeEditor>()
const scrollbar = autoHeight ? 'hidden' : 'auto'
useEffect(() => {
@ -72,8 +72,12 @@ export default function MonacoSourceCodeEditor({
monaco.languages.typescript?.typescriptDefaults?.setCompilerOptions?.(compilerOptions)
}, [])
useEventListener('resize', () => {
editor?.layout({ width: 0, height: 0 })
window.requestAnimationFrame(() => editor?.layout())
})
return (
<Container className={className}>
<MonacoEditor
language={language}
theme="vs-light"
@ -94,9 +98,54 @@ export default function MonacoSourceCodeEditor({
if (autoHeight) {
autoAdjustEditorHeight(_editor)
}
setEditor(_editor)
}}
onChange={onChange}
/>
</Container>
)
}
interface DiffEditorProps extends Omit<SourceCodeEditorProps, 'autoHeight'> {
original: string
}
export function DiffEditor({
source,
original,
language = PLAIN_TEXT,
lineNumbers = true,
readOnly = false,
height,
wordWrap = true,
onChange = noop
}: DiffEditorProps) {
const [editor, setEditor] = useState<monacoEditor.editor.IStandaloneDiffEditor>()
useEventListener('resize', () => {
editor?.layout({ width: 0, height: 0 })
window.requestAnimationFrame(() => editor?.layout())
})
return (
<MonacoDiffEditor
language={language}
theme="vs-light"
original={original}
value={source}
height={height}
options={{
...MonacoEditorOptions,
readOnly,
wordWrap: toOnOff(wordWrap),
lineNumbers: toOnOff(lineNumbers),
scrollbar: {
vertical: 'auto',
horizontal: 'auto',
alwaysConsumeMouseWheel: false
}
}}
editorDidMount={setEditor}
onChange={onChange}
/>
)
}

View File

@ -38,6 +38,7 @@ export interface StringsMap {
branches: string
browse: string
cancel: string
changes: string
checkRuns: string
checkSuites: string
checks: string
@ -62,6 +63,7 @@ export interface StringsMap {
confirmDeleteWebhook: string
confirmation: string
content: string
contents: string
conversation: string
copy: string
copyCommitSHA: string

View File

@ -192,7 +192,7 @@ pr:
failedToDeleteComment: Failed to delete comment. Please try again.
prMerged: This Pull Request was merged
reviewSubmitted: Review submitted.
prReviewSubmit: '{user} {state|approved:approved, rejected:rejected,changereq:requested changes to, reviewed} the pull request. {time}'
prReviewSubmit: '{user} {state|approved:approved, rejected:rejected,changereq:requested to, reviewed} the pull request. {time}'
prMergedInfo: '{user} merged branch {source} into {target} {time}.'
prBranchPushInfo: '{user} pushed a new commit {commit}.'
prStateChanged: '{user} changed pull request state from {old} to {new}.'
@ -375,3 +375,5 @@ manageCredText: You can also manage your git credential {URL}
blame: Blame
viewRaw: View Raw
download: Download
changes: Changes
contents: Contents

View File

@ -1,9 +1,13 @@
.container {
--header-height: 96px;
--heading-height: 58px;
--tabs-height: 36px;
background-color: var(--white) !important;
margin-top: 0 !important;
// box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
// border-radius: 4px;
height: calc(100vh - 96px);
height: calc(100vh - var(--header-height));
overflow: hidden;
.heading {
@ -11,7 +15,7 @@
// border-top-right-radius: 4px;
align-items: center;
padding: 0 var(--spacing-xlarge) !important;
height: 58px;
height: var(--heading-height);
background-color: var(--grey-100);
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-bottom: 1px solid var(--grey-200);
@ -40,13 +44,42 @@
}
}
.content {
.tabs {
height: var(--tabs-height);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--grey-50);
--tab-height: 18px;
.selectedView {
height: var(--tab-height);
> div {
align-items: center;
display: flex;
height: var(--tab-height) !important;
width: auto;
padding: 0 var(--spacing-large);
font-weight: 700;
font-size: 10px;
text-transform: uppercase;
&[class*='selected'] {
background-color: var(--primary-9);
}
&:not([class*='selected']) {
color: var(--primary-9);
}
}
}
}
.editorContainer {
padding-left: var(--spacing-medium) !important;
overflow: hidden;
.editorContainer {
height: calc(100vh - 96px - 58px);
overflow: hidden;
}
height: calc(100vh - var(--header-height) - var(--heading-height) - var(--tabs-height));
}
}

View File

@ -6,7 +6,8 @@ declare const styles: {
readonly path: string
readonly inputContainer: string
readonly refLink: string
readonly content: string
readonly tabs: string
readonly selectedView: string
readonly editorContainer: string
}
export default styles

View File

@ -1,5 +1,17 @@
import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Button, ButtonVariation, Color, Container, FlexExpander, Icon, Layout, Text, TextInput } from '@harness/uicore'
import {
Button,
ButtonVariation,
Color,
Container,
FlexExpander,
Icon,
Layout,
Text,
TextInput,
VisualYamlSelectedView,
VisualYamlToggle
} from '@harness/uicore'
import { Link, useHistory } from 'react-router-dom'
import ReactJoin from 'react-join'
import cx from 'classnames'
@ -11,6 +23,7 @@ import { useStrings } from 'framework/strings'
import { filenameToLanguage, FILE_SEPERATOR } from 'utils/Utils'
import { useGetResourceContent } from 'hooks/useGetResourceContent'
import { CommitModalButton } from 'components/CommitModalButton/CommitModalButton'
import { DiffEditor } from 'components/SourceCodeEditor/MonacoSourceCodeEditor'
import css from './FileEditor.module.scss'
interface EditorProps extends Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath'> {
@ -83,6 +96,7 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
// Make API call to verify if fileResourcePath is an existing folder
verifyFolder().then(() => setStartVerifyFolder(true))
}, [fileName, parentPath, language, content, verifyFolder])
const [selectedView, setSelectedView] = useState(VisualYamlSelectedView.VISUAL)
// Calculate file name input field width based on number of characters inside
useEffect(() => {
@ -111,16 +125,14 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
}
}
}, [isNew, name])
return (
<Container className={css.container}>
<Layout.Horizontal className={css.heading}>
<Container>
<Layout.Horizontal spacing="small" className={css.path}>
<Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })}>
<Icon name="main-folder" padding={{ right: 'xsmall' }} />
{/* <Text color={Color.GREY_900} inline>
{repoMetadata.uid}
</Text> */}
<Icon name="code-folder" padding={{ right: 'xsmall' }} />
</Link>
<PathSeparator />
{parentPath && (
@ -148,7 +160,7 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
<TextInput
autoFocus={isNew}
value={fileName}
inputRef={ref => (inputRef.current = ref)}
inputRef={_ref => (inputRef.current = _ref)}
wrapperClassName={css.inputContainer}
placeholder={getString('nameYourFile')}
onInput={(event: ChangeEvent<HTMLInputElement>) => {
@ -231,15 +243,22 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
</Container>
</Layout.Horizontal>
<Container className={cx(css.content, language)}>
<SourceCodeEditor
className={css.editorContainer}
height="100%"
language={language}
source={originalContent}
onChange={setContent}
<Container className={css.tabs}>
<VisualYamlToggle
onChange={setSelectedView}
selectedView={selectedView}
labels={{ visual: getString('contents'), yaml: getString('changes') }}
className={css.selectedView}
/>
</Container>
<Container className={cx(css.editorContainer, language)}>
{selectedView === VisualYamlSelectedView.VISUAL ? (
<SourceCodeEditor language={language} source={content} onChange={setContent} />
) : (
<DiffEditor language={language} original={originalContent} source={content} onChange={setContent} />
)}
</Container>
</Container>
)
}

View File

@ -49,7 +49,6 @@ export interface SourceCodeEditorProps {
lineNumbers?: boolean
readOnly?: boolean
highlightLines?: string // i.e: {1,3-4}, TODO: not yet supported
className?: string
height?: number | string
autoHeight?: boolean
wordWrap?: boolean

View File

@ -3,7 +3,7 @@ import type { TypesPullReqActivity } from 'services/code'
export interface DiffFileEntry extends DiffFile {
fileId: string
fileTitle: string
filePath: string
containerId: string
contentId: string
fileActivities?: TypesPullReqActivity[]