mirror of
https://github.com/harness/drone.git
synced 2025-05-10 22:21:22 +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'> {
|
||||
comment: { commentItems: CommentItem<TypesPullReqActivity>[] }
|
||||
rowElement?: HTMLTableRowElement
|
||||
}
|
||||
|
||||
export const CodeCommentStatusSelect: React.FC<CodeCommentStatusSelectProps> = ({
|
||||
repoMetadata,
|
||||
pullReqMetadata,
|
||||
comment: { commentItems }
|
||||
comment: { commentItems },
|
||||
rowElement
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const { showError } = useToaster()
|
||||
@ -87,6 +89,15 @@ export const CodeCommentStatusSelect: React.FC<CodeCommentStatusSelectProps> = (
|
||||
}
|
||||
}, [parentComment?.id, randomClass])
|
||||
|
||||
useEffect(
|
||||
function updateRowElement() {
|
||||
if (rowElement) {
|
||||
rowElement.dataset.commentThreadStatus = codeCommentStatus.value
|
||||
}
|
||||
},
|
||||
[rowElement, codeCommentStatus]
|
||||
)
|
||||
|
||||
return parentComment?.deleted ? null : (
|
||||
<Select
|
||||
className={cx(css.select, randomClass)}
|
||||
|
@ -154,3 +154,24 @@
|
||||
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 outletTopOfFirstOfComment: string
|
||||
export declare const replyPlaceHolder: string
|
||||
export declare const toggleComment: string
|
||||
export declare const viewer: string
|
||||
|
@ -14,7 +14,7 @@
|
||||
* 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 { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
||||
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 { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { ButtonRoleProps } from 'utils/Utils'
|
||||
import { ButtonRoleProps, CodeCommentState } from 'utils/Utils'
|
||||
import { useResizeObserver } from 'hooks/useResizeObserver'
|
||||
import { useCustomEventListener } from 'hooks/useEventListener'
|
||||
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'
|
||||
|
||||
export interface CommentItem<T = unknown> {
|
||||
@ -331,227 +333,259 @@ const CommentsThread = <T = unknown,>({
|
||||
},
|
||||
[editIndexes]
|
||||
)
|
||||
// const collapseResolvedComments = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems])
|
||||
// const shouldCollapsedResolvedComments = useMemo(
|
||||
// () =>
|
||||
// collapseResolvedComments &&
|
||||
// !(commentItems.length === 1 && shorten(commentItems[0].content) === commentItems[0].content),
|
||||
// [commentItems, collapseResolvedComments]
|
||||
// )
|
||||
// const [collapsed, setCollapsed] = useState(collapseResolvedComments)
|
||||
const isCommentThreadResolved = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems])
|
||||
const domRef = useRef<HTMLElement>()
|
||||
const show = useRef(isCommentThreadResolved ? false : true)
|
||||
const internalFlags = useRef({ initialized: false })
|
||||
|
||||
useEffect(
|
||||
function renderToggleCommentsButton() {
|
||||
// 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 (
|
||||
<Render when={commentItems.length}>
|
||||
<Container className={css.viewer} padding="xlarge">
|
||||
{commentItems
|
||||
// .filter((_commentItem, index) => {
|
||||
// return collapseResolvedComments && collapsed ? index === 0 : true
|
||||
// })
|
||||
.map((commentItem, index) => {
|
||||
const isLastItem = index === commentItems.length - 1
|
||||
<Container className={css.viewer} padding="xlarge" ref={domRef}>
|
||||
{commentItems.map((commentItem, index) => {
|
||||
const isLastItem = index === commentItems.length - 1
|
||||
|
||||
return (
|
||||
<ThreadSection
|
||||
key={index}
|
||||
title={
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
style={{ alignItems: 'center' }}
|
||||
data-outdated={commentItem?.outdated}>
|
||||
<Text inline icon="code-chat"></Text>
|
||||
<Avatar name={commentItem?.author} size="small" hoverCard={false} />
|
||||
<Text inline>
|
||||
<strong>{commentItem?.author}</strong>
|
||||
</Text>
|
||||
<PipeSeparator height={8} />
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||
<TimePopoverWithLocal
|
||||
time={defaultTo(commentItem?.edited as number, 0)}
|
||||
inline={false}
|
||||
font={{ variation: FontVariation.SMALL }}
|
||||
color={Color.GREY_400}
|
||||
/>
|
||||
</Text>
|
||||
return (
|
||||
<ThreadSection
|
||||
key={index}
|
||||
title={
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
style={{ alignItems: 'center' }}
|
||||
data-outdated={commentItem?.outdated}>
|
||||
<Text inline icon="code-chat"></Text>
|
||||
<Avatar name={commentItem?.author} size="small" hoverCard={false} />
|
||||
<Text inline>
|
||||
<strong>{commentItem?.author}</strong>
|
||||
</Text>
|
||||
<PipeSeparator height={8} />
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||
<TimePopoverWithLocal
|
||||
time={defaultTo(commentItem?.edited as number, 0)}
|
||||
inline={false}
|
||||
font={{ variation: FontVariation.SMALL }}
|
||||
color={Color.GREY_400}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Render when={commentItem?.edited !== commentItem?.created || !!commentItem?.deleted}>
|
||||
<>
|
||||
<PipeSeparator height={8} />
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||
{getString(commentItem?.deleted ? 'deleted' : 'edited')}
|
||||
</Text>
|
||||
</>
|
||||
</Render>
|
||||
|
||||
<Render when={commentItem?.outdated}>
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} className={css.outdated}>
|
||||
{getString('pr.outdated')}
|
||||
<Render when={commentItem?.edited !== commentItem?.created || !!commentItem?.deleted}>
|
||||
<>
|
||||
<PipeSeparator height={8} />
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||
{getString(commentItem?.deleted ? 'deleted' : 'edited')}
|
||||
</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>
|
||||
|
||||
<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={
|
||||
/* 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={commentItem?.outdated}>
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} className={css.outdated}>
|
||||
{getString('pr.outdated')}
|
||||
</Text>
|
||||
</Render>
|
||||
|
||||
{/* <Render when={shouldCollapsedResolvedComments}>
|
||||
<Container
|
||||
flex={{ justifyContent: 'space-around' }}
|
||||
padding={{ bottom: 'xsmall' }}
|
||||
data-comments-collapsed={collapsed}>
|
||||
<Layout.Horizontal>
|
||||
{collapsed && commentItems.length > 1 && (
|
||||
<AvatarGroup
|
||||
avatars={uniq(
|
||||
commentItems.filter((_, index) => !!index).map((item, index) => ({ name: item.author, index }))
|
||||
)}
|
||||
overlap={true}
|
||||
restrictLengthTo={10}
|
||||
avatarGroupProps={{
|
||||
hoverCard: false
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
rightIcon={collapsed ? 'main-chevron-down' : 'main-chevron-up'}
|
||||
iconProps={{ size: 14, margin: { left: 'xsmall' } }}
|
||||
variation={ButtonVariation.LINK}
|
||||
size={ButtonSize.SMALL}
|
||||
onClick={() => setCollapsed(!collapsed)}>
|
||||
<StringSubstitute
|
||||
str={getString(collapsed ? 'pr.moreComments' : 'showLessMatches')}
|
||||
vars={{
|
||||
num: commentItems.length <= 2 ? ' ' : commentItems.length - 1,
|
||||
count: commentItems.length - 1
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
</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}>
|
||||
<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>
|
||||
</Render>
|
||||
)
|
||||
@ -561,11 +595,4 @@ export const CommentBox = React.memo(CommentBoxInternal)
|
||||
|
||||
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'
|
||||
|
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] {
|
||||
background-color: var(--white);
|
||||
|
||||
& + [data-annotated-line] {
|
||||
& + [data-annotated-line][hidden] + [data-annotated-line],
|
||||
&:not([hidden]) + [data-annotated-line] {
|
||||
[data-comment-thread-id] {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
@ -20,14 +20,14 @@ import Selecto from 'selecto'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
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 type { GitInfoProps } from 'utils/GitUtils'
|
||||
import type { DiffFileEntry } from 'utils/types'
|
||||
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||
import { useAppContext } from 'AppContext'
|
||||
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 { CodeCommentStatusButton } from 'components/CodeCommentStatusButton/CodeCommentStatusButton'
|
||||
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)
|
||||
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
|
||||
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
|
||||
// expands itself when CommentBox's height is changed (in split view)
|
||||
const oppositeRowPlaceHolder = createCommentOppositePlaceHolder(comment.lineNumberEnd, commentThreadId < 0)
|
||||
@ -249,6 +253,16 @@ export function usePullReqComments({
|
||||
commentRowElement.innerHTML = `<td colspan="2"></td>`
|
||||
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
|
||||
const element = commentRowElement.firstElementChild as HTMLTableCellElement
|
||||
|
||||
@ -434,6 +448,7 @@ export function usePullReqComments({
|
||||
repoMetadata={repoMetadata}
|
||||
pullReqMetadata={pullReqMetadata as TypesPullReq}
|
||||
comment={comment}
|
||||
rowElement={commentRowElement}
|
||||
/>
|
||||
),
|
||||
[CommentBoxOutletPosition.RIGHT_OF_REPLY_PLACEHOLDER]: (
|
||||
|
@ -682,6 +682,7 @@ export interface StringsMap {
|
||||
'pr.titleChangedTable': string
|
||||
'pr.titleIsRequired': string
|
||||
'pr.titlePlaceHolder': string
|
||||
'pr.toggleComments': string
|
||||
'pr.unified': string
|
||||
'prChecks.error': string
|
||||
'prChecks.failure': string
|
||||
|
@ -242,6 +242,7 @@ diff: Diff
|
||||
draft: Draft
|
||||
conversation: Conversation
|
||||
pr:
|
||||
toggleComments: Toggle comments
|
||||
suggestedChange: Suggested change
|
||||
addSuggestion: Add suggestion to batch
|
||||
removeSuggestion: Remove suggestion from batch
|
||||
|
Loading…
Reference in New Issue
Block a user