mirror of
https://github.com/harness/drone.git
synced 2025-05-12 06:59:54 +08:00
Refactor collapse/expand PR comments per new design (#2046)
This commit is contained in:
parent
a0ac3e86c5
commit
5fe08d4743
@ -29,12 +29,14 @@ import css from './CodeCommentStatusSelect.module.scss'
|
|||||||
|
|
||||||
interface CodeCommentStatusSelectProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullReqMetadata'> {
|
interface CodeCommentStatusSelectProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullReqMetadata'> {
|
||||||
comment: { commentItems: CommentItem<TypesPullReqActivity>[] }
|
comment: { commentItems: CommentItem<TypesPullReqActivity>[] }
|
||||||
|
rowElement?: HTMLTableRowElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeCommentStatusSelect: React.FC<CodeCommentStatusSelectProps> = ({
|
export const CodeCommentStatusSelect: React.FC<CodeCommentStatusSelectProps> = ({
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
pullReqMetadata,
|
pullReqMetadata,
|
||||||
comment: { commentItems }
|
comment: { commentItems },
|
||||||
|
rowElement
|
||||||
}) => {
|
}) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { showError } = useToaster()
|
const { showError } = useToaster()
|
||||||
@ -87,6 +89,15 @@ export const CodeCommentStatusSelect: React.FC<CodeCommentStatusSelectProps> = (
|
|||||||
}
|
}
|
||||||
}, [parentComment?.id, randomClass])
|
}, [parentComment?.id, randomClass])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function updateRowElement() {
|
||||||
|
if (rowElement) {
|
||||||
|
rowElement.dataset.commentThreadStatus = codeCommentStatus.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[rowElement, codeCommentStatus]
|
||||||
|
)
|
||||||
|
|
||||||
return parentComment?.deleted ? null : (
|
return parentComment?.deleted ? null : (
|
||||||
<Select
|
<Select
|
||||||
className={cx(css.select, randomClass)}
|
className={cx(css.select, randomClass)}
|
||||||
|
@ -154,3 +154,24 @@
|
|||||||
padding-top: 2px !important;
|
padding-top: 2px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleComment {
|
||||||
|
all: unset;
|
||||||
|
border: none !important;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 5px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: attr(data-threads-count);
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
top: -2px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -30,4 +30,5 @@ export declare const optionMenuIcon: string
|
|||||||
export declare const outdated: string
|
export declare const outdated: string
|
||||||
export declare const outletTopOfFirstOfComment: string
|
export declare const outletTopOfFirstOfComment: string
|
||||||
export declare const replyPlaceHolder: string
|
export declare const replyPlaceHolder: string
|
||||||
|
export declare const toggleComment: string
|
||||||
export declare const viewer: string
|
export declare const viewer: string
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import type { EditorView } from '@codemirror/view'
|
import type { EditorView } from '@codemirror/view'
|
||||||
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
||||||
import { Container, Layout, Avatar, TextInput, Text, FlexExpander, Button, useIsMounted } from '@harnessio/uicore'
|
import { Container, Layout, Avatar, TextInput, Text, FlexExpander, Button, useIsMounted } from '@harnessio/uicore'
|
||||||
@ -30,10 +30,12 @@ import type { TypesRepository } from 'services/code'
|
|||||||
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||||
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||||
import { ButtonRoleProps } from 'utils/Utils'
|
import { ButtonRoleProps, CodeCommentState } from 'utils/Utils'
|
||||||
import { useResizeObserver } from 'hooks/useResizeObserver'
|
import { useResizeObserver } from 'hooks/useResizeObserver'
|
||||||
import { useCustomEventListener } from 'hooks/useEventListener'
|
import { useCustomEventListener } from 'hooks/useEventListener'
|
||||||
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
|
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
|
||||||
|
import commentActiveIconUrl from './comment.svg?url'
|
||||||
|
import commentResolvedIconUrl from './comment-resolved.svg?url'
|
||||||
import css from './CommentBox.module.scss'
|
import css from './CommentBox.module.scss'
|
||||||
|
|
||||||
export interface CommentItem<T = unknown> {
|
export interface CommentItem<T = unknown> {
|
||||||
@ -331,227 +333,259 @@ const CommentsThread = <T = unknown,>({
|
|||||||
},
|
},
|
||||||
[editIndexes]
|
[editIndexes]
|
||||||
)
|
)
|
||||||
// const collapseResolvedComments = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems])
|
const isCommentThreadResolved = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems])
|
||||||
// const shouldCollapsedResolvedComments = useMemo(
|
const domRef = useRef<HTMLElement>()
|
||||||
// () =>
|
const show = useRef(isCommentThreadResolved ? false : true)
|
||||||
// collapseResolvedComments &&
|
const internalFlags = useRef({ initialized: false })
|
||||||
// !(commentItems.length === 1 && shorten(commentItems[0].content) === commentItems[0].content),
|
|
||||||
// [commentItems, collapseResolvedComments]
|
useEffect(
|
||||||
// )
|
function renderToggleCommentsButton() {
|
||||||
// const [collapsed, setCollapsed] = useState(collapseResolvedComments)
|
// Get the row that contains the comment. If the comment is spanned for multiple lines, the
|
||||||
|
// row is the last row
|
||||||
|
let annotatedRow = domRef.current?.closest('tr') as HTMLTableRowElement
|
||||||
|
|
||||||
|
// Make sure annotatedRow is not one of rows which renders a comment
|
||||||
|
while (annotatedRow && !annotatedRow.dataset.sourceLineNumber) {
|
||||||
|
annotatedRow = annotatedRow?.previousElementSibling as HTMLTableRowElement
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotatedRow) {
|
||||||
|
const lineNumColDOM = annotatedRow.firstElementChild as HTMLElement
|
||||||
|
const sourceLineNumber = annotatedRow.dataset.sourceLineNumber
|
||||||
|
const button: HTMLButtonElement = lineNumColDOM?.querySelector('button') || document.createElement('button')
|
||||||
|
|
||||||
|
if (!button.onclick) {
|
||||||
|
const toggleHidden = (dom: Element) => {
|
||||||
|
if (show.current) dom.setAttribute('hidden', '')
|
||||||
|
else dom.removeAttribute('hidden')
|
||||||
|
}
|
||||||
|
const toggleComments = (e: KeyboardEvent | MouseEvent) => {
|
||||||
|
let commentRow = annotatedRow.nextElementSibling as HTMLElement
|
||||||
|
|
||||||
|
while (commentRow?.dataset?.annotatedLine) {
|
||||||
|
toggleHidden(commentRow)
|
||||||
|
|
||||||
|
// Toggle opposite place-holder as well
|
||||||
|
const diffParent = commentRow.closest('.d2h-code-wrapper')?.parentElement
|
||||||
|
const oppositeDiv = diffParent?.classList.contains('right')
|
||||||
|
? diffParent.previousElementSibling
|
||||||
|
: diffParent?.nextElementSibling
|
||||||
|
const oppositePlaceHolders = oppositeDiv?.querySelectorAll(
|
||||||
|
`[data-place-holder-for-line="${sourceLineNumber}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
oppositePlaceHolders?.forEach(dom => toggleHidden(dom))
|
||||||
|
|
||||||
|
commentRow = commentRow.nextElementSibling as HTMLElement
|
||||||
|
}
|
||||||
|
show.current = !show.current
|
||||||
|
|
||||||
|
if (!show.current) button.dataset.threadsCount = String(activeThreads + resolvedThreads)
|
||||||
|
else delete button.dataset.threadsCount
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
button.classList.add(css.toggleComment)
|
||||||
|
button.title = getString('pr.toggleComments')
|
||||||
|
|
||||||
|
button.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') toggleComments(e)
|
||||||
|
})
|
||||||
|
button.onclick = toggleComments
|
||||||
|
|
||||||
|
lineNumColDOM.appendChild(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentRow = annotatedRow.nextElementSibling as HTMLElement
|
||||||
|
let resolvedThreads = 0
|
||||||
|
let activeThreads = 0
|
||||||
|
|
||||||
|
while (commentRow?.dataset?.annotatedLine) {
|
||||||
|
if (commentRow.dataset.commentThreadStatus == CodeCommentState.RESOLVED) {
|
||||||
|
resolvedThreads++
|
||||||
|
if (!internalFlags.current.initialized) show.current = false
|
||||||
|
} else activeThreads++
|
||||||
|
|
||||||
|
commentRow = commentRow.nextElementSibling as HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
button.style.backgroundImage = `url("${activeThreads ? commentActiveIconUrl : commentResolvedIconUrl}")`
|
||||||
|
|
||||||
|
if (!internalFlags.current.initialized) {
|
||||||
|
internalFlags.current.initialized = true
|
||||||
|
|
||||||
|
if (!show.current && resolvedThreads) button.dataset.threadsCount = String(resolvedThreads)
|
||||||
|
else delete button.dataset.threadsCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isCommentThreadResolved, getString]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Render when={commentItems.length}>
|
<Render when={commentItems.length}>
|
||||||
<Container className={css.viewer} padding="xlarge">
|
<Container className={css.viewer} padding="xlarge" ref={domRef}>
|
||||||
{commentItems
|
{commentItems.map((commentItem, index) => {
|
||||||
// .filter((_commentItem, index) => {
|
const isLastItem = index === commentItems.length - 1
|
||||||
// return collapseResolvedComments && collapsed ? index === 0 : true
|
|
||||||
// })
|
|
||||||
.map((commentItem, index) => {
|
|
||||||
const isLastItem = index === commentItems.length - 1
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadSection
|
<ThreadSection
|
||||||
key={index}
|
key={index}
|
||||||
title={
|
title={
|
||||||
<Layout.Horizontal
|
<Layout.Horizontal
|
||||||
spacing="small"
|
spacing="small"
|
||||||
style={{ alignItems: 'center' }}
|
style={{ alignItems: 'center' }}
|
||||||
data-outdated={commentItem?.outdated}>
|
data-outdated={commentItem?.outdated}>
|
||||||
<Text inline icon="code-chat"></Text>
|
<Text inline icon="code-chat"></Text>
|
||||||
<Avatar name={commentItem?.author} size="small" hoverCard={false} />
|
<Avatar name={commentItem?.author} size="small" hoverCard={false} />
|
||||||
<Text inline>
|
<Text inline>
|
||||||
<strong>{commentItem?.author}</strong>
|
<strong>{commentItem?.author}</strong>
|
||||||
</Text>
|
</Text>
|
||||||
<PipeSeparator height={8} />
|
<PipeSeparator height={8} />
|
||||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||||
<TimePopoverWithLocal
|
<TimePopoverWithLocal
|
||||||
time={defaultTo(commentItem?.edited as number, 0)}
|
time={defaultTo(commentItem?.edited as number, 0)}
|
||||||
inline={false}
|
inline={false}
|
||||||
font={{ variation: FontVariation.SMALL }}
|
font={{ variation: FontVariation.SMALL }}
|
||||||
color={Color.GREY_400}
|
color={Color.GREY_400}
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Render when={commentItem?.edited !== commentItem?.created || !!commentItem?.deleted}>
|
<Render when={commentItem?.edited !== commentItem?.created || !!commentItem?.deleted}>
|
||||||
<>
|
<>
|
||||||
<PipeSeparator height={8} />
|
<PipeSeparator height={8} />
|
||||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||||
{getString(commentItem?.deleted ? 'deleted' : 'edited')}
|
{getString(commentItem?.deleted ? 'deleted' : 'edited')}
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
</Render>
|
|
||||||
|
|
||||||
<Render when={commentItem?.outdated}>
|
|
||||||
<Text inline font={{ variation: FontVariation.SMALL }} className={css.outdated}>
|
|
||||||
{getString('pr.outdated')}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Render>
|
</>
|
||||||
|
|
||||||
<FlexExpander />
|
|
||||||
<Layout.Horizontal>
|
|
||||||
<Render when={index === 0 && outlets[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]}>
|
|
||||||
<Container padding={{ right: 'medium' }}>
|
|
||||||
{outlets[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]}
|
|
||||||
</Container>
|
|
||||||
</Render>
|
|
||||||
<Render when={!commentItem?.deleted}>
|
|
||||||
<OptionsMenuButton
|
|
||||||
isDark={true}
|
|
||||||
icon="Options"
|
|
||||||
iconProps={{ size: 14 }}
|
|
||||||
style={{ padding: '5px' }}
|
|
||||||
width="100px"
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
hasIcon: true,
|
|
||||||
className: cx(css.optionMenuIcon, css.edit),
|
|
||||||
iconName: 'Edit',
|
|
||||||
text: getString('edit'),
|
|
||||||
onClick: () => setEditIndexes({ ...editIndexes, ...{ [index]: true } })
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hasIcon: true,
|
|
||||||
className: css.optionMenuIcon,
|
|
||||||
iconName: 'code-quote',
|
|
||||||
text: getString('quote'),
|
|
||||||
onClick: () => onQuote(commentItem?.content)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hasIcon: true,
|
|
||||||
className: css.optionMenuIcon,
|
|
||||||
iconName: 'code-copy',
|
|
||||||
text: getString('pr.copyLinkToComment'),
|
|
||||||
onClick: () => copyLinkToComment(commentItem.id, commentItems[0])
|
|
||||||
},
|
|
||||||
'-',
|
|
||||||
{
|
|
||||||
className: css.deleteIcon,
|
|
||||||
hasIcon: true,
|
|
||||||
iconName: 'main-trash',
|
|
||||||
isDanger: true,
|
|
||||||
text: getString('delete'),
|
|
||||||
onClick: async () => {
|
|
||||||
if (await handleAction(CommentAction.DELETE, '', commentItem)) {
|
|
||||||
resetStateAtIndex(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Render>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
}
|
|
||||||
hideGutter={isLastItem /*|| (collapseResolvedComments && collapsed)*/}>
|
|
||||||
<Container padding={{ bottom: isLastItem ? undefined : 'xsmall' }} data-comment-id={commentItem.id}>
|
|
||||||
<Render when={index === 0 && outlets[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]}>
|
|
||||||
<Container className={css.outletTopOfFirstOfComment}>
|
|
||||||
{outlets[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]}
|
|
||||||
</Container>
|
|
||||||
</Render>
|
</Render>
|
||||||
|
|
||||||
<Match expr={editIndexes[index]}>
|
<Render when={commentItem?.outdated}>
|
||||||
<Truthy>
|
<Text inline font={{ variation: FontVariation.SMALL }} className={css.outdated}>
|
||||||
<Container className={css.editCommentContainer} data-comment-editor-shown="true">
|
{getString('pr.outdated')}
|
||||||
<MarkdownEditorWithPreview
|
</Text>
|
||||||
routingId={routingId}
|
</Render>
|
||||||
standalone={standalone}
|
|
||||||
repoMetadata={repoMetadata}
|
|
||||||
value={commentItem?.content}
|
|
||||||
onSave={async value => {
|
|
||||||
if (await handleAction(CommentAction.UPDATE, value, commentItem)) {
|
|
||||||
commentItem.content = value
|
|
||||||
resetStateAtIndex(index)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCancel={() => resetStateAtIndex(index)}
|
|
||||||
setDirty={_dirty => {
|
|
||||||
setDirty(index, _dirty)
|
|
||||||
}}
|
|
||||||
i18n={{
|
|
||||||
placeHolder: getString('leaveAComment'),
|
|
||||||
tabEdit: getString('write'),
|
|
||||||
tabPreview: getString('preview'),
|
|
||||||
save: getString('save'),
|
|
||||||
cancel: getString('cancel')
|
|
||||||
}}
|
|
||||||
autoFocusAndPosition
|
|
||||||
suggestionBlock={suggestionBlock}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
</Truthy>
|
|
||||||
<Else>
|
|
||||||
<Match expr={commentItem?.deleted}>
|
|
||||||
<Truthy>
|
|
||||||
<Text className={css.deleted}>{getString('commentDeleted')}</Text>
|
|
||||||
</Truthy>
|
|
||||||
<Else>
|
|
||||||
<MarkdownViewer
|
|
||||||
source={
|
|
||||||
/* collapseResolvedComments && collapsed
|
|
||||||
? shorten(commentItem?.content)
|
|
||||||
:*/ commentItem?.content
|
|
||||||
}
|
|
||||||
suggestionBlock={Object.assign(
|
|
||||||
{
|
|
||||||
commentId: commentItem.id,
|
|
||||||
appliedCheckSum: get(commentItem, 'payload.metadata.suggestions.applied_check_sum', ''),
|
|
||||||
appliedCommitSha: get(
|
|
||||||
commentItem,
|
|
||||||
'payload.metadata.suggestions.applied_commit_sha',
|
|
||||||
''
|
|
||||||
)
|
|
||||||
},
|
|
||||||
suggestionBlock
|
|
||||||
)}
|
|
||||||
suggestionCheckSums={get(commentItem, 'payload.metadata.suggestions.check_sums', [])}
|
|
||||||
/>
|
|
||||||
</Else>
|
|
||||||
</Match>
|
|
||||||
</Else>
|
|
||||||
</Match>
|
|
||||||
</Container>
|
|
||||||
</ThreadSection>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* <Render when={shouldCollapsedResolvedComments}>
|
<FlexExpander />
|
||||||
<Container
|
<Layout.Horizontal>
|
||||||
flex={{ justifyContent: 'space-around' }}
|
<Render when={index === 0 && outlets[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]}>
|
||||||
padding={{ bottom: 'xsmall' }}
|
<Container padding={{ right: 'medium' }}>
|
||||||
data-comments-collapsed={collapsed}>
|
{outlets[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]}
|
||||||
<Layout.Horizontal>
|
</Container>
|
||||||
{collapsed && commentItems.length > 1 && (
|
</Render>
|
||||||
<AvatarGroup
|
<Render when={!commentItem?.deleted}>
|
||||||
avatars={uniq(
|
<OptionsMenuButton
|
||||||
commentItems.filter((_, index) => !!index).map((item, index) => ({ name: item.author, index }))
|
isDark={true}
|
||||||
)}
|
icon="Options"
|
||||||
overlap={true}
|
iconProps={{ size: 14 }}
|
||||||
restrictLengthTo={10}
|
style={{ padding: '5px' }}
|
||||||
avatarGroupProps={{
|
width="100px"
|
||||||
hoverCard: false
|
items={[
|
||||||
}}
|
{
|
||||||
size="small"
|
hasIcon: true,
|
||||||
/>
|
className: cx(css.optionMenuIcon, css.edit),
|
||||||
)}
|
iconName: 'Edit',
|
||||||
<Button
|
text: getString('edit'),
|
||||||
rightIcon={collapsed ? 'main-chevron-down' : 'main-chevron-up'}
|
onClick: () => setEditIndexes({ ...editIndexes, ...{ [index]: true } })
|
||||||
iconProps={{ size: 14, margin: { left: 'xsmall' } }}
|
},
|
||||||
variation={ButtonVariation.LINK}
|
{
|
||||||
size={ButtonSize.SMALL}
|
hasIcon: true,
|
||||||
onClick={() => setCollapsed(!collapsed)}>
|
className: css.optionMenuIcon,
|
||||||
<StringSubstitute
|
iconName: 'code-quote',
|
||||||
str={getString(collapsed ? 'pr.moreComments' : 'showLessMatches')}
|
text: getString('quote'),
|
||||||
vars={{
|
onClick: () => onQuote(commentItem?.content)
|
||||||
num: commentItems.length <= 2 ? ' ' : commentItems.length - 1,
|
},
|
||||||
count: commentItems.length - 1
|
{
|
||||||
}}
|
hasIcon: true,
|
||||||
/>
|
className: css.optionMenuIcon,
|
||||||
</Button>
|
iconName: 'code-copy',
|
||||||
</Layout.Horizontal>
|
text: getString('pr.copyLinkToComment'),
|
||||||
</Container>
|
onClick: () => copyLinkToComment(commentItem.id, commentItems[0])
|
||||||
</Render> */}
|
},
|
||||||
|
'-',
|
||||||
|
{
|
||||||
|
className: css.deleteIcon,
|
||||||
|
hasIcon: true,
|
||||||
|
iconName: 'main-trash',
|
||||||
|
isDanger: true,
|
||||||
|
text: getString('delete'),
|
||||||
|
onClick: async () => {
|
||||||
|
if (await handleAction(CommentAction.DELETE, '', commentItem)) {
|
||||||
|
resetStateAtIndex(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Render>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
}
|
||||||
|
hideGutter={isLastItem}>
|
||||||
|
<Container padding={{ bottom: isLastItem ? undefined : 'xsmall' }} data-comment-id={commentItem.id}>
|
||||||
|
<Render when={index === 0 && outlets[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]}>
|
||||||
|
<Container className={css.outletTopOfFirstOfComment}>
|
||||||
|
{outlets[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]}
|
||||||
|
</Container>
|
||||||
|
</Render>
|
||||||
|
|
||||||
|
<Match expr={editIndexes[index]}>
|
||||||
|
<Truthy>
|
||||||
|
<Container className={css.editCommentContainer} data-comment-editor-shown="true">
|
||||||
|
<MarkdownEditorWithPreview
|
||||||
|
routingId={routingId}
|
||||||
|
standalone={standalone}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
|
value={commentItem?.content}
|
||||||
|
onSave={async value => {
|
||||||
|
if (await handleAction(CommentAction.UPDATE, value, commentItem)) {
|
||||||
|
commentItem.content = value
|
||||||
|
resetStateAtIndex(index)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => resetStateAtIndex(index)}
|
||||||
|
setDirty={_dirty => {
|
||||||
|
setDirty(index, _dirty)
|
||||||
|
}}
|
||||||
|
i18n={{
|
||||||
|
placeHolder: getString('leaveAComment'),
|
||||||
|
tabEdit: getString('write'),
|
||||||
|
tabPreview: getString('preview'),
|
||||||
|
save: getString('save'),
|
||||||
|
cancel: getString('cancel')
|
||||||
|
}}
|
||||||
|
autoFocusAndPosition
|
||||||
|
suggestionBlock={suggestionBlock}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Truthy>
|
||||||
|
<Else>
|
||||||
|
<Match expr={commentItem?.deleted}>
|
||||||
|
<Truthy>
|
||||||
|
<Text className={css.deleted}>{getString('commentDeleted')}</Text>
|
||||||
|
</Truthy>
|
||||||
|
<Else>
|
||||||
|
<MarkdownViewer
|
||||||
|
source={commentItem?.content}
|
||||||
|
suggestionBlock={Object.assign(
|
||||||
|
{
|
||||||
|
commentId: commentItem.id,
|
||||||
|
appliedCheckSum: get(commentItem, 'payload.metadata.suggestions.applied_check_sum', ''),
|
||||||
|
appliedCommitSha: get(commentItem, 'payload.metadata.suggestions.applied_commit_sha', '')
|
||||||
|
},
|
||||||
|
suggestionBlock
|
||||||
|
)}
|
||||||
|
suggestionCheckSums={get(commentItem, 'payload.metadata.suggestions.check_sums', [])}
|
||||||
|
/>
|
||||||
|
</Else>
|
||||||
|
</Match>
|
||||||
|
</Else>
|
||||||
|
</Match>
|
||||||
|
</Container>
|
||||||
|
</ThreadSection>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</Container>
|
</Container>
|
||||||
</Render>
|
</Render>
|
||||||
)
|
)
|
||||||
@ -561,11 +595,4 @@ export const CommentBox = React.memo(CommentBoxInternal)
|
|||||||
|
|
||||||
export const customEventForCommentWithId = (id: number) => `CommentBoxCustomEvent-${id}`
|
export const customEventForCommentWithId = (id: number) => `CommentBoxCustomEvent-${id}`
|
||||||
|
|
||||||
// const shorten = (str = '', maxLen = 140, separator = ' ') => {
|
|
||||||
// const s = str.split('\n')[0]
|
|
||||||
// const sub = s.length <= maxLen ? s : s.substr(0, s.lastIndexOf(separator, maxLen))
|
|
||||||
|
|
||||||
// return sub.length < str.length ? sub + '...' : sub
|
|
||||||
// }
|
|
||||||
|
|
||||||
const CRLF = '\n'
|
const CRLF = '\n'
|
||||||
|
1
web/src/components/CommentBox/comment-resolved.svg
Normal file
1
web/src/components/CommentBox/comment-resolved.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" height="20" maskUnits="userSpaceOnUse" width="20" x="0" y="0"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#fff" fill-rule="evenodd"/></mask><mask id="b" fill="#fff"><path clip-rule="evenodd" d="m7.33325 7.91797c-.27614 0-.5.22386-.5.5s.22386.5.5.5h5.33335c.2761 0 .5-.22386.5-.5s-.2239-.5-.5-.5zm0 1.83333c-.27614 0-.5.22386-.5.5 0 .2761.22386.5.5.5h4.66665c.2762 0 .5-.2239.5-.5 0-.27614-.2238-.5-.5-.5z" fill="#fff" fill-rule="evenodd"/></mask><g opacity=".9"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#d8d8d8" fill-rule="evenodd"/><g mask="url(#a)"><path d="m0 0h20v20h-20z" fill="#ff661a"/><path d="m7.33341 5.33203h5.33329c1.4728 0 2.6667 1.19391 2.6667 2.66667v2.6667c0 1.4727-1.1939 2.6666-2.6667 2.6666h-2.6666l-2.66669 2v-2c-1.47275 0-2.66666-1.1939-2.66666-2.6666v-2.6667c0-1.47276 1.19391-2.66667 2.66666-2.66667z" stroke="#f3f3fa" stroke-linecap="round" stroke-linejoin="round"/><path d="m7.83325 8.41797c0 .27614-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .82843.67157 1.5 1.5 1.5zm5.33335 0h-5.33335v2h5.33335zm-.5.5c0-.27614.2238-.5.5-.5v2c.8284 0 1.5-.67157 1.5-1.5zm.5.5c-.2762 0-.5-.22386-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-5.33335 0h5.33335v-2h-5.33335zm.5 1.33333c0 .2761-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .8284.67157 1.5 1.5 1.5zm4.66665 0h-4.66665v2h4.66665zm-.5.5c0-.27614.2239-.5.5-.5v2c.8284 0 1.5-.6716 1.5-1.5zm.5.5c-.2761 0-.5-.2239-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-4.66665 0h4.66665v-2h-4.66665z" fill="#f3f3fa" mask="url(#b)" opacity=".5"/></g></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
web/src/components/CommentBox/comment.svg
Normal file
1
web/src/components/CommentBox/comment.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" height="20" maskUnits="userSpaceOnUse" width="20" x="0" y="0"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#fff" fill-rule="evenodd"/></mask><mask id="b" fill="#fff"><path clip-rule="evenodd" d="m7.33325 7.91797c-.27614 0-.5.22386-.5.5s.22386.5.5.5h5.33335c.2761 0 .5-.22386.5-.5s-.2239-.5-.5-.5zm0 1.83333c-.27614 0-.5.22386-.5.5 0 .2761.22386.5.5.5h4.66665c.2762 0 .5-.2239.5-.5 0-.27614-.2238-.5-.5-.5z" fill="#fff" fill-rule="evenodd"/></mask><g opacity=".9"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#d8d8d8" fill-rule="evenodd"/><g mask="url(#a)"><path d="m0 0h20v20h-20z" fill="#0278d5"/><path d="m7.33341 5.33203h5.33329c1.4728 0 2.6667 1.19391 2.6667 2.66667v2.6667c0 1.4727-1.1939 2.6666-2.6667 2.6666h-2.6666l-2.66669 2v-2c-1.47275 0-2.66666-1.1939-2.66666-2.6666v-2.6667c0-1.47276 1.19391-2.66667 2.66666-2.66667z" stroke="#f3f3fa" stroke-linecap="round" stroke-linejoin="round"/><path d="m7.83325 8.41797c0 .27614-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .82843.67157 1.5 1.5 1.5zm5.33335 0h-5.33335v2h5.33335zm-.5.5c0-.27614.2238-.5.5-.5v2c.8284 0 1.5-.67157 1.5-1.5zm.5.5c-.2762 0-.5-.22386-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-5.33335 0h5.33335v-2h-5.33335zm.5 1.33333c0 .2761-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .8284.67157 1.5 1.5 1.5zm4.66665 0h-4.66665v2h4.66665zm-.5.5c0-.27614.2239-.5.5-.5v2c.8284 0 1.5-.6716 1.5-1.5zm.5.5c-.2761 0-.5-.2239-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-4.66665 0h4.66665v-2h-4.66665z" fill="#f3f3fa" mask="url(#b)" opacity=".5"/></g></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -77,7 +77,8 @@
|
|||||||
&[data-annotated-line] {
|
&[data-annotated-line] {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
|
|
||||||
& + [data-annotated-line] {
|
& + [data-annotated-line][hidden] + [data-annotated-line],
|
||||||
|
&:not([hidden]) + [data-annotated-line] {
|
||||||
[data-comment-thread-id] {
|
[data-comment-thread-id] {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,14 @@ import Selecto from 'selecto'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useToaster, ButtonProps, Utils } from '@harnessio/uicore'
|
import { useToaster, ButtonProps, Utils } from '@harnessio/uicore'
|
||||||
import { findLastIndex, isEqual, max, noop, random, uniq } from 'lodash-es'
|
import { findLastIndex, get, isEqual, max, noop, random, uniq } from 'lodash-es'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
import type { DiffFileEntry } from 'utils/types'
|
import type { DiffFileEntry } from 'utils/types'
|
||||||
import { useConfirmAct } from 'hooks/useConfirmAction'
|
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import type { OpenapiCommentCreatePullReqRequest, TypesPullReq, TypesPullReqActivity } from 'services/code'
|
import type { OpenapiCommentCreatePullReqRequest, TypesPullReq, TypesPullReqActivity } from 'services/code'
|
||||||
import { PullRequestSection, filenameToLanguage, getErrorMessage } from 'utils/Utils'
|
import { CodeCommentState, PullRequestSection, filenameToLanguage, getErrorMessage } from 'utils/Utils'
|
||||||
import { AppWrapper } from 'App'
|
import { AppWrapper } from 'App'
|
||||||
import { CodeCommentStatusButton } from 'components/CodeCommentStatusButton/CodeCommentStatusButton'
|
import { CodeCommentStatusButton } from 'components/CodeCommentStatusButton/CodeCommentStatusButton'
|
||||||
import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondarySaveButton/CodeCommentSecondarySaveButton'
|
import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondarySaveButton/CodeCommentSecondarySaveButton'
|
||||||
@ -231,9 +231,13 @@ export function usePullReqComments({
|
|||||||
// Add `selected` class into selected lines / rows (only if selected lines count > 1)
|
// Add `selected` class into selected lines / rows (only if selected lines count > 1)
|
||||||
markSelectedLines(comment, lineInfo.rowElement, true)
|
markSelectedLines(comment, lineInfo.rowElement, true)
|
||||||
|
|
||||||
|
lineInfo.rowElement.dataset.sourceLineNumber = String(comment.lineNumberEnd)
|
||||||
|
|
||||||
// Annotate that the row is taken. We only support one thread per line as now
|
// Annotate that the row is taken. We only support one thread per line as now
|
||||||
updateDataCommentIds(lineInfo.rowElement, comment.inner.id as number, true)
|
updateDataCommentIds(lineInfo.rowElement, comment.inner.id as number, true)
|
||||||
|
|
||||||
|
const isCommentThreadResolved = !!get(comment.commentItems[0], 'payload.resolved', false)
|
||||||
|
|
||||||
// Create placeholder for opposite row (id < 0 means this is a new thread). This placeholder
|
// Create placeholder for opposite row (id < 0 means this is a new thread). This placeholder
|
||||||
// expands itself when CommentBox's height is changed (in split view)
|
// expands itself when CommentBox's height is changed (in split view)
|
||||||
const oppositeRowPlaceHolder = createCommentOppositePlaceHolder(comment.lineNumberEnd, commentThreadId < 0)
|
const oppositeRowPlaceHolder = createCommentOppositePlaceHolder(comment.lineNumberEnd, commentThreadId < 0)
|
||||||
@ -249,6 +253,16 @@ export function usePullReqComments({
|
|||||||
commentRowElement.innerHTML = `<td colspan="2"></td>`
|
commentRowElement.innerHTML = `<td colspan="2"></td>`
|
||||||
lineInfo.rowElement.after(commentRowElement)
|
lineInfo.rowElement.after(commentRowElement)
|
||||||
|
|
||||||
|
// Set both place-holder and comment box hidden when comment thread is resolved
|
||||||
|
if (isCommentThreadResolved) {
|
||||||
|
oppositeRowPlaceHolder.setAttribute('hidden', '')
|
||||||
|
commentRowElement.setAttribute('hidden', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
commentRowElement.dataset.commentThreadStatus = isCommentThreadResolved
|
||||||
|
? CodeCommentState.RESOLVED
|
||||||
|
: CodeCommentState.ACTIVE
|
||||||
|
|
||||||
// `element` is where CommentBox will be mounted
|
// `element` is where CommentBox will be mounted
|
||||||
const element = commentRowElement.firstElementChild as HTMLTableCellElement
|
const element = commentRowElement.firstElementChild as HTMLTableCellElement
|
||||||
|
|
||||||
@ -434,6 +448,7 @@ export function usePullReqComments({
|
|||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
pullReqMetadata={pullReqMetadata as TypesPullReq}
|
pullReqMetadata={pullReqMetadata as TypesPullReq}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
|
rowElement={commentRowElement}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[CommentBoxOutletPosition.RIGHT_OF_REPLY_PLACEHOLDER]: (
|
[CommentBoxOutletPosition.RIGHT_OF_REPLY_PLACEHOLDER]: (
|
||||||
|
@ -682,6 +682,7 @@ export interface StringsMap {
|
|||||||
'pr.titleChangedTable': string
|
'pr.titleChangedTable': string
|
||||||
'pr.titleIsRequired': string
|
'pr.titleIsRequired': string
|
||||||
'pr.titlePlaceHolder': string
|
'pr.titlePlaceHolder': string
|
||||||
|
'pr.toggleComments': string
|
||||||
'pr.unified': string
|
'pr.unified': string
|
||||||
'prChecks.error': string
|
'prChecks.error': string
|
||||||
'prChecks.failure': string
|
'prChecks.failure': string
|
||||||
|
@ -242,6 +242,7 @@ diff: Diff
|
|||||||
draft: Draft
|
draft: Draft
|
||||||
conversation: Conversation
|
conversation: Conversation
|
||||||
pr:
|
pr:
|
||||||
|
toggleComments: Toggle comments
|
||||||
suggestedChange: Suggested change
|
suggestedChange: Suggested change
|
||||||
addSuggestion: Add suggestion to batch
|
addSuggestion: Add suggestion to batch
|
||||||
removeSuggestion: Remove suggestion from batch
|
removeSuggestion: Remove suggestion from batch
|
||||||
|
Loading…
Reference in New Issue
Block a user