PR Page changes per demo feedbacks (#2056)

BT-7922-1
Tan Nhu 2024-05-22 16:16:52 +00:00 committed by Harness
parent 0d1e536531
commit 2f2388aa42
11 changed files with 186 additions and 112 deletions

View File

@ -60,6 +60,7 @@ import { InViewDiffBlockRenderer } from 'components/DiffViewer/InViewDiffBlockRe
import Config from 'Config'
import { PullReqSuggestionsBatch } from 'components/PullReqSuggestionsBatch/PullReqSuggestionsBatch'
import { PullReqCustomEvent } from 'pages/PullRequest/PullRequestUtils'
import { useCollapseHarnessNav } from 'hooks/useIsSidebarExpanded'
import { ChangesDropdown } from './ChangesDropdown'
import { DiffViewConfiguration } from './DiffViewConfiguration'
import ReviewSplitButton from './ReviewSplitButton/ReviewSplitButton'
@ -442,6 +443,7 @@ const ChangesInternal: React.FC<ChangesProps> = ({
}, [diffs, setPullReqChangesCount])
useShowRequestError(errorFileViews, 0)
useCollapseHarnessNav()
return (
<Container className={cx(css.container, className)} {...(!!loadingRawDiff || !!error ? { flex: true } : {})}>

View File

@ -84,3 +84,9 @@
}
}
}
.formContainer {
form + div {
display: none !important;
}
}

View File

@ -18,6 +18,7 @@
// This is an auto-generated file
export declare const directly: string
export declare const extendedDescription: string
export declare const formContainer: string
export declare const main: string
export declare const newBranch: string
export declare const newBranchContainer: string

View File

@ -89,7 +89,7 @@ export function useCommitSuggestionsModal({
{title || getString('commitChanges')}
</Heading>
<Container margin={{ right: 'xxlarge' }}>
<Container margin={{ right: 'xxlarge' }} className={css.formContainer}>
<Formik<FormData>
initialValues={{
commitMessage,
@ -113,7 +113,7 @@ export function useCommitSuggestionsModal({
placeholder={extendedDescription || getString('optionalExtendedDescription')}
/>
<Layout.Horizontal spacing="small" padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }}>
<Layout.Horizontal spacing="small" padding={{ top: 'xxlarge', bottom: 'large' }}>
<Button
type="submit"
variation={ButtonVariation.PRIMARY}

View File

@ -35,7 +35,6 @@ import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/Code
import { dispatchCustomEvent } from 'hooks/useEventListener'
import { UseGetPullRequestInfoResult, usePullReqActivities } from 'pages/PullRequest/useGetPullRequestInfo'
import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration'
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
import {
activitiesToDiffCommentItems,
activityToCommentItem,
@ -288,14 +287,18 @@ export function usePullReqComments({
// update to the latest data
comment._commentItems = structuredClone(comment.commentItems)
const suggestionBlock: SuggestionBlock = {
source:
comment.codeBlockContent ||
(lineElements?.length
? lineElements.map(td => td.nextElementSibling?.querySelector('.d2h-code-line-ctn')?.textContent).join('\n')
: lineInfo.rowElement?.lastElementChild?.querySelector('.d2h-code-line-ctn')?.textContent || ''),
lang: filenameToLanguage(diff.filePath.split('/').pop())
}
const suggestionBlock = comment.left
? undefined
: {
source:
comment.codeBlockContent ||
(lineElements?.length
? lineElements
.map(td => td.nextElementSibling?.querySelector('.d2h-code-line-ctn')?.textContent)
.join('\n')
: lineInfo.rowElement?.lastElementChild?.querySelector('.d2h-code-line-ctn')?.textContent || ''),
lang: filenameToLanguage(diff.filePath.split('/').pop())
}
// Note: CommentBox is rendered as an independent React component.
// Everything passed to it must be either values, or refs.

View File

@ -75,6 +75,10 @@
pre {
margin-bottom: 0 !important;
border-radius: 0 !important;
code {
padding: var(--spacing-small) !important;
}
}
.removed pre {

View File

@ -17,14 +17,14 @@
import { useHistory } from 'react-router-dom'
import { Container, Utils } from '@harnessio/uicore'
import rehypeSanitize from 'rehype-sanitize'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { isEmpty } from 'lodash-es'
import cx from 'classnames'
import { getCodeString } from 'rehype-rewrite'
import MarkdownPreview from '@uiw/react-markdown-preview'
import rehypeVideo from 'rehype-video'
import rehypeExternalLinks, { Element } from 'rehype-external-links'
import { INITIAL_ZOOM_LEVEL, generateAlphaNumericHash } from 'utils/Utils'
import rehypeExternalLinks from 'rehype-external-links'
import { INITIAL_ZOOM_LEVEL } from 'utils/Utils'
import ImageCarousel from 'components/ImageCarousel/ImageCarousel'
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
import { CodeSuggestionBlock } from './CodeSuggestionBlock'
@ -32,12 +32,9 @@ import css from './MarkdownViewer.module.scss'
interface MarkdownViewerProps {
source: string
inDescriptionBox?: boolean
className?: string
maxHeight?: string | number
darkMode?: boolean
handleDescUpdate?: (payload: string) => void
setOriginalContent?: React.Dispatch<React.SetStateAction<string>>
suggestionBlock?: SuggestionBlock
suggestionCheckSums?: string[]
}
@ -47,9 +44,6 @@ export function MarkdownViewer({
className,
maxHeight,
darkMode,
setOriginalContent,
handleDescUpdate,
inDescriptionBox = false,
suggestionBlock,
suggestionCheckSums
}: MarkdownViewerProps) {
@ -59,7 +53,6 @@ export function MarkdownViewer({
const [imgEvent, setImageEvent] = useState<string[]>([])
const refRootHref = useMemo(() => document.getElementById('repository-ref-root')?.getAttribute('href'), [])
const ref = useRef<HTMLDivElement>()
const [markdown, setMarkdown] = useState(source)
const interceptClickEventOnViewerContainer = useCallback(
event => {
@ -100,42 +93,6 @@ export function MarkdownViewer({
},
[history]
)
const [flag, setFlag] = useState(false)
const handleCheckboxChange = useCallback(
async (lineNumber: number) => {
const newMarkdown = source
.split('\n')
.map((line, index) => {
if (index === lineNumber) {
return line.startsWith('- [ ]') ? line.replace('- [ ]', '- [x]') : line.replace('- [x]', '- [ ]')
}
return line
})
.join('\n')
setOriginalContent?.(newMarkdown)
setFlag(true)
setMarkdown(newMarkdown)
handleDescUpdate?.(newMarkdown)
},
[source]
)
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLInputElement
if (target.type === 'checkbox') {
const lineNumber = parseInt(target.getAttribute('data-line-number') || '0', 10)
handleCheckboxChange(lineNumber)
}
}
document.addEventListener('click', handleClick)
return () => {
document.removeEventListener('click', handleClick)
}
}, [source])
const hash = generateAlphaNumericHash(6)
return (
<Container
@ -144,8 +101,7 @@ export function MarkdownViewer({
style={{ maxHeight: maxHeight }}
ref={ref}>
<MarkdownPreview
key={flag ? hash : 0}
source={markdown}
source={source}
skipHtml={false}
warpperElement={{ 'data-color-mode': darkMode ? 'dark' : 'light' }}
rehypeRewrite={(node, _index, parent) => {
@ -195,16 +151,6 @@ export function MarkdownViewer({
}
}
}
if (
(node as unknown as HTMLDivElement).tagName === 'input' &&
(node as Unknown as Element)?.properties?.type === 'checkbox'
) {
const lineNumber = parent?.position?.start?.line ? parent?.position?.start?.line - 1 : 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const element = node as any
element.properties['data-line-number'] = lineNumber.toString()
element.properties.disabled = !inDescriptionBox
}
}}
rehypePlugins={[
[rehypeSanitize],
@ -219,7 +165,7 @@ export function MarkdownViewer({
if (
typeof code === 'string' &&
typeof _className === 'string' &&
/^language-suggestion/.test(_className.toLocaleLowerCase())
'language-suggestion' === _className.toLocaleLowerCase()
) {
return (
<CodeSuggestionBlock

View File

@ -14,8 +14,9 @@
* limitations under the License.
*/
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { CustomEventName } from 'utils/Utils'
import { useAppContext } from 'AppContext'
import { useCustomEventListener } from './useEventListener'
/**
@ -35,3 +36,47 @@ export function useIsSidebarExpanded() {
return isSidebarExpanded
}
export function useCollapseHarnessNav() {
const { standalone } = useAppContext()
const isSidebarExpanded = useIsSidebarExpanded()
const handled = useRef(!standalone && isSidebarExpanded)
const internalFlags = useRef({
initialized: false
})
useEffect(() => {
if (handled.current) {
const nav = document.getElementById('main-side-nav')
const pullReqNavItem = nav?.querySelector('[data-code-repo-section="pull-requests"]')
const toggleNavButton = nav?.querySelector('span[icon][class*="SideNavToggleButton"]') as HTMLElement
if (pullReqNavItem && toggleNavButton) {
const isCollapsed = pullReqNavItem.clientWidth <= 64
if (!isCollapsed) {
setTimeout(() => {
toggleNavButton.click()
internalFlags.current.initialized = true
}, 0)
}
}
return () => {
if (handled.current) {
toggleNavButton.click()
}
}
}
}, [])
useEffect(() => {
if (internalFlags.current.initialized && !isSidebarExpanded) {
internalFlags.current.initialized = false
if (handled.current) {
handled.current = false
}
}
}, [isSidebarExpanded])
}

View File

@ -34,6 +34,13 @@
display: inline-block;
}
}
:global {
.task-list-item,
.task-list-item input {
cursor: pointer;
}
}
}
}

View File

@ -265,10 +265,13 @@ export const Conversation: React.FC<ConversationProps> = ({
const comment = activitiesToDiffCommentItems(activity?.code_comment?.path as string, [
activity as TypesPullReqActivity
])[0]
const suggestionBlock = {
source: comment.codeBlockContent as string,
lang: filenameToLanguage(activity?.code_comment?.path?.split('/').pop())
}
const suggestionBlock = comment.left
? undefined
: {
source: comment.codeBlockContent as string,
lang: filenameToLanguage(activity?.code_comment?.path?.split('/').pop())
}
return (
<ThreadSection

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Button, ButtonSize, ButtonVariation, Container, Layout, useToaster, Text } from '@harnessio/uicore'
import cx from 'classnames'
import { useParams } from 'react-router-dom'
@ -80,18 +80,78 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
setFlag(true)
}, [])
const handleDescUpdate = useCallback((markdown: string) => {
const payload: OpenapiUpdatePullReqRequest = {
title: pullReqMetadata.title,
description: markdown || ''
}
setOriginalContent(markdown)
mutate(payload)
.then(() => {
setContent(markdown)
})
.catch(exception => showError(getErrorMessage(exception), 0, getString('pr.failedToUpdate')))
}, [])
const handleDescUpdate = useCallback(
(markdown: string) => {
const payload: OpenapiUpdatePullReqRequest = {
title: pullReqMetadata.title,
description: markdown || ''
}
setOriginalContent(markdown)
mutate(payload)
.then(() => {
setContent(markdown)
})
.catch(exception => showError(getErrorMessage(exception), 0, getString('pr.failedToUpdate')))
},
[getString, mutate, pullReqMetadata.title, showError]
)
const viewerDOMRef = useRef<HTMLElement>()
useEffect(
function toggleTodoCheck() {
const dom = viewerDOMRef.current
const TODO_LIST_MARKER = 'data-todo-index'
const TODO_LIST_ITEM_CLASS = 'task-list-item'
if (dom && !edit) {
const handleClick = (e: MouseEvent) => {
const targetIsListItem = (e.target as HTMLElement).classList.contains(TODO_LIST_ITEM_CLASS)
const target = (e.target as HTMLElement)?.closest?.(`.${TODO_LIST_ITEM_CLASS}`)
const input = target?.firstElementChild as HTMLInputElement
const checked = targetIsListItem ? !input?.checked : input?.checked
let sourceIndex = -1
if (!input) return
const index = Number(target?.getAttribute(TODO_LIST_MARKER))
const newContent = originalContent
.split('\n')
.map(line => {
if (line.startsWith('- [ ]') || line.startsWith('- [x]')) {
sourceIndex++
if (index === sourceIndex) {
return checked ? line.replace('- [ ]', '- [x]') : line.replace('- [x]', '- [ ]')
}
}
return line
})
.join('\n')
setContent(newContent)
setOriginalContent(newContent)
e.preventDefault()
e.stopPropagation()
handleDescUpdate(newContent)
}
// Enable all check inputs to allow clicking
dom.querySelectorAll(`.${TODO_LIST_ITEM_CLASS} input`)?.forEach((input, index) => {
input.removeAttribute('disabled')
input.parentElement?.setAttribute(TODO_LIST_MARKER, String(index))
})
dom.addEventListener('click', handleClick)
return () => dom.removeEventListener('click', handleClick)
}
},
[edit, handleDescUpdate, originalContent]
)
return (
<Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
@ -184,31 +244,28 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
autoFocusAndPosition={true}
/>
)) || (
<Container className={css.mdWrapper}>
<MarkdownViewer
inDescriptionBox={true}
setOriginalContent={setOriginalContent}
source={content}
handleDescUpdate={handleDescUpdate}
/>
<Container className={css.menuWrapper}>
<OptionsMenuButton
isDark={true}
icon="Options"
iconProps={{ size: 14 }}
style={{ padding: '5px' }}
items={[
{
text: getString('edit'),
className: css.optionMenuIcon,
hasIcon: true,
iconName: 'Edit',
onClick: () => setEdit(true)
}
]}
/>
<React.Fragment key={originalContent}>
<Container className={css.mdWrapper} ref={viewerDOMRef}>
<MarkdownViewer source={content} />
<Container className={css.menuWrapper}>
<OptionsMenuButton
isDark={true}
icon="Options"
iconProps={{ size: 14 }}
style={{ padding: '5px' }}
items={[
{
text: getString('edit'),
className: css.optionMenuIcon,
hasIcon: true,
iconName: 'Edit',
onClick: () => setEdit(true)
}
]}
/>
</Container>
</Container>
</Container>
</React.Fragment>
)}
</Container>
<NavigationCheck when={dirty} />