mirror of
https://github.com/harness/drone.git
synced 2025-05-04 23:40:24 +08:00
feat: [CODE-350]: Add some PR Comment improvements
This commit is contained in:
parent
7c097a7680
commit
3afec4c78b
@ -103,8 +103,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
.reviewButton {
|
||||
&.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ declare const styles: {
|
||||
readonly menuItem: string
|
||||
readonly menuReviewItem: string
|
||||
readonly reviewIcon: string
|
||||
readonly btn: string
|
||||
readonly reviewButton: string
|
||||
readonly hide: string
|
||||
readonly disabled: string
|
||||
}
|
||||
export default styles
|
||||
|
@ -26,6 +26,7 @@ import { DIFF2HTML_CONFIG, ViewStyle } from 'components/DiffViewer/DiffViewerUti
|
||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||
import type { TypesPullReq, TypesPullReqActivity } from 'services/code'
|
||||
import { useShowRequestError } from 'hooks/useShowRequestError'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { ChangesDropdown } from './ChangesDropdown'
|
||||
import { DiffViewConfiguration } from './DiffViewConfiguration'
|
||||
@ -88,9 +89,10 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
lazy: !pullRequestMetadata?.number
|
||||
})
|
||||
const [activities, setActivities] = useState<TypesPullReqActivity[]>()
|
||||
const showSpinner = useMemo(() => {
|
||||
return loading || (loadingActivities && !activities)
|
||||
}, [loading, loadingActivities, activities])
|
||||
const showSpinner = useMemo(
|
||||
() => loading || (loadingActivities && !activities),
|
||||
[loading, loadingActivities, activities]
|
||||
)
|
||||
const diffStats = useMemo(
|
||||
() =>
|
||||
(diffs || []).reduce(
|
||||
@ -103,6 +105,16 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
),
|
||||
[diffs]
|
||||
)
|
||||
const shouldHideReviewButton = useMemo(
|
||||
() => readOnly || pullRequestMetadata?.state === 'merged' || pullRequestMetadata?.state === 'closed',
|
||||
[readOnly, pullRequestMetadata?.state]
|
||||
)
|
||||
const { currentUser } = useAppContext()
|
||||
const isActiveUserPROwner = useMemo(() => {
|
||||
return (
|
||||
!!currentUser?.uid && !!pullRequestMetadata?.author?.uid && currentUser?.uid === pullRequestMetadata?.author?.uid
|
||||
)
|
||||
}, [currentUser, pullRequestMetadata])
|
||||
|
||||
// Optimization to avoid showing unnecessary loading spinner. The trick is to
|
||||
// show only the spinner when the component is mounted and not when refetching
|
||||
@ -208,10 +220,11 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
<FlexExpander />
|
||||
|
||||
<ReviewSplitButton
|
||||
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
|
||||
shouldHide={shouldHideReviewButton}
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
refreshPr={voidFn(noop)}
|
||||
disabled={isActiveUserPROwner}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
|
@ -32,9 +32,10 @@ interface ReviewSplitButtonProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
||||
shouldHide: boolean
|
||||
pullRequestMetadata?: TypesPullReq
|
||||
refreshPr: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
const ReviewSplitButton = (props: ReviewSplitButtonProps) => {
|
||||
const { pullRequestMetadata, repoMetadata, shouldHide, refreshPr } = props
|
||||
const { pullRequestMetadata, repoMetadata, shouldHide, refreshPr, disabled } = props
|
||||
const { getString } = useStrings()
|
||||
const { showError, showSuccess } = useToaster()
|
||||
|
||||
@ -75,7 +76,11 @@ const ReviewSplitButton = (props: ReviewSplitButtonProps) => {
|
||||
.catch(exception => showError(getErrorMessage(exception)))
|
||||
}, [decisionOption, mutate, showError, showSuccess, getString, refreshPr, pullRequestMetadata?.source_sha])
|
||||
return (
|
||||
<Container className={cx(css.btn, { [css.hide]: shouldHide })}>
|
||||
<Container
|
||||
className={cx(css.reviewButton, {
|
||||
[css.hide]: shouldHide,
|
||||
[css.disabled]: disabled
|
||||
})}>
|
||||
<SplitButton
|
||||
text={decisionOption.title}
|
||||
disabled={loading}
|
||||
@ -93,7 +98,7 @@ const ReviewSplitButton = (props: ReviewSplitButtonProps) => {
|
||||
<Menu.Item
|
||||
key={option.method}
|
||||
className={css.menuReviewItem}
|
||||
disabled={option.disabled}
|
||||
disabled={disabled || option.disabled}
|
||||
text={
|
||||
<Layout.Horizontal>
|
||||
<Icon
|
||||
|
@ -0,0 +1,74 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useMutate } from 'restful-react'
|
||||
import { useToaster, Button, ButtonVariation, ButtonSize, ButtonProps, useIsMounted } from '@harness/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import type { TypesPullReqActivity } from 'services/code'
|
||||
import { useEmitCodeCommentStatus } from 'hooks/useEmitCodeCommentStatus'
|
||||
import { CodeCommentState, getErrorMessage } from 'utils/Utils'
|
||||
import type { CommentItem } from '../CommentBox/CommentBox'
|
||||
|
||||
interface CodeCommentSecondarySaveButtonProps
|
||||
extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>,
|
||||
ButtonProps {
|
||||
commentItems: CommentItem<TypesPullReqActivity>[]
|
||||
}
|
||||
|
||||
export const CodeCommentSecondarySaveButton: React.FC<CodeCommentSecondarySaveButtonProps> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
commentItems,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const isMounted = useIsMounted()
|
||||
const { showError } = useToaster()
|
||||
const path = useMemo(
|
||||
() => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/comments`,
|
||||
[repoMetadata.path, pullRequestMetadata?.number]
|
||||
)
|
||||
const { mutate: updateCodeCommentStatus } = useMutate({ verb: 'PUT', path: ({ id }) => `${path}/${id}/status` })
|
||||
const [resolved, setResolved] = useState(commentItems[0]?.payload?.resolved ? true : false)
|
||||
const emitCodeCommentStatus = useEmitCodeCommentStatus({
|
||||
id: commentItems[0]?.payload?.id,
|
||||
onMatch: status => {
|
||||
if (isMounted.current) {
|
||||
setResolved(status === CodeCommentState.RESOLVED)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
text={getString(resolved ? 'replyAndReactivate' : 'replyAndResolve')}
|
||||
variation={ButtonVariation.TERTIARY}
|
||||
size={ButtonSize.MEDIUM}
|
||||
onClick={async () => {
|
||||
const status = resolved ? CodeCommentState.ACTIVE : CodeCommentState.RESOLVED
|
||||
const payload = { status }
|
||||
const id = commentItems[0]?.payload?.id
|
||||
|
||||
updateCodeCommentStatus(payload, { pathParams: { id } })
|
||||
.then(() => {
|
||||
if (commentItems[0]?.payload) {
|
||||
if (resolved) {
|
||||
commentItems[0].payload.resolved = 0
|
||||
} else {
|
||||
commentItems[0].payload.resolved = Date.now()
|
||||
}
|
||||
}
|
||||
if (isMounted.current) {
|
||||
setResolved(!resolved)
|
||||
}
|
||||
emitCodeCommentStatus(status)
|
||||
;(onClick as () => void)()
|
||||
})
|
||||
.catch(_exception => {
|
||||
showError(getErrorMessage(_exception), 0, getString('pr.failedToUpdateCommentStatus'))
|
||||
})
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useMutate } from 'restful-react'
|
||||
import { useToaster, Button, ButtonVariation, ButtonSize } from '@harness/uicore'
|
||||
import { useToaster, Button, ButtonVariation, ButtonSize, useIsMounted } from '@harness/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import type { TypesPullReqActivity } from 'services/code'
|
||||
@ -19,6 +19,7 @@ export const CodeCommentStatusButton: React.FC<CodeCommentStatusButtonProps> = (
|
||||
commentItems,
|
||||
onCommentUpdate
|
||||
}) => {
|
||||
const isMounted = useIsMounted()
|
||||
const { getString } = useStrings()
|
||||
const { showError } = useToaster()
|
||||
const path = useMemo(
|
||||
@ -30,13 +31,24 @@ export const CodeCommentStatusButton: React.FC<CodeCommentStatusButtonProps> = (
|
||||
const emitCodeCommentStatus = useEmitCodeCommentStatus({
|
||||
id: commentItems[0]?.payload?.id,
|
||||
onMatch: status => {
|
||||
setResolved(status === CodeCommentState.RESOLVED)
|
||||
if (isMounted.current) {
|
||||
const isResolved = status === CodeCommentState.RESOLVED
|
||||
setResolved(isResolved)
|
||||
|
||||
if (commentItems[0]?.payload) {
|
||||
if (isResolved) {
|
||||
commentItems[0].payload.resolved = Date.now()
|
||||
} else {
|
||||
commentItems[0].payload.resolved = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
text={getString(resolved ? 'unresolve' : 'resolve')}
|
||||
text={getString(resolved ? 'reactivate' : 'resolve')}
|
||||
variation={ButtonVariation.TERTIARY}
|
||||
size={ButtonSize.MEDIUM}
|
||||
onClick={async () => {
|
||||
@ -47,8 +59,11 @@ export const CodeCommentStatusButton: React.FC<CodeCommentStatusButtonProps> = (
|
||||
updateCodeCommentStatus(payload, { pathParams: { id } })
|
||||
.then(() => {
|
||||
onCommentUpdate()
|
||||
setResolved(!resolved)
|
||||
emitCodeCommentStatus(status)
|
||||
|
||||
if (isMounted.current) {
|
||||
setResolved(!resolved)
|
||||
}
|
||||
})
|
||||
.catch(_exception => {
|
||||
showError(getErrorMessage(_exception), 0, getString('pr.failedToUpdateCommentStatus'))
|
||||
|
@ -59,14 +59,21 @@ export const CodeCommentStatusSelect: React.FC<CodeCommentStatusSelectProps> = (
|
||||
const status = newState.value as CodeCommentState
|
||||
const payload = { status }
|
||||
const id = commentItems[0]?.payload?.id
|
||||
const isActive = status === CodeCommentState.ACTIVE
|
||||
|
||||
updateCodeCommentStatus(payload, { pathParams: { id } })
|
||||
.then(() => {
|
||||
onCommentUpdate()
|
||||
setCodeCommentStatus(
|
||||
status === CodeCommentState.ACTIVE ? codeCommentStatusItems[0] : codeCommentStatusItems[1]
|
||||
)
|
||||
setCodeCommentStatus(isActive ? codeCommentStatusItems[0] : codeCommentStatusItems[1])
|
||||
emitCodeCommentStatus(status)
|
||||
|
||||
if (commentItems[0]?.payload) {
|
||||
if (isActive) {
|
||||
commentItems[0].payload.resolved = 0
|
||||
} else {
|
||||
commentItems[0].payload.resolved = Date.now()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(_exception => {
|
||||
showError(getErrorMessage(_exception), 0, getString('pr.failedToUpdateCommentStatus'))
|
||||
|
@ -35,6 +35,18 @@
|
||||
display: inline-block;
|
||||
border-radius: var(--box-radius);
|
||||
}
|
||||
|
||||
.outdated {
|
||||
background: #fcf4e3 !important;
|
||||
color: #c05809 !important;
|
||||
border: 1px solid var(--yellow-300) !important;
|
||||
padding: 4px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
line-height: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.replyPlaceHolder {
|
||||
|
@ -6,6 +6,7 @@ declare const styles: {
|
||||
readonly box: string
|
||||
readonly viewer: string
|
||||
readonly deleted: string
|
||||
readonly outdated: string
|
||||
readonly replyPlaceHolder: string
|
||||
readonly newCommentContainer: string
|
||||
readonly hasThread: string
|
||||
|
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useResizeDetector } from 'react-resize-detector'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
||||
import { Container, Layout, Avatar, TextInput, Text, Color, FontVariation, FlexExpander } from '@harness/uicore'
|
||||
import { Container, Layout, Avatar, TextInput, Text, Color, FontVariation, FlexExpander, Button } from '@harness/uicore'
|
||||
import cx from 'classnames'
|
||||
import ReactTimeago from 'react-timeago'
|
||||
import { noop } from 'lodash-es'
|
||||
@ -21,6 +21,7 @@ export interface CommentItem<T = unknown> {
|
||||
created: string | number
|
||||
updated: string | number
|
||||
deleted: string | number
|
||||
outdated: boolean
|
||||
content: string
|
||||
payload?: T // optional payload for callers to handle on callback calls
|
||||
}
|
||||
@ -31,7 +32,7 @@ export enum CommentAction {
|
||||
REPLY = 'reply',
|
||||
DELETE = 'delete',
|
||||
RESOLVE = 'resolve',
|
||||
UNRESOLVE = 'unresolve'
|
||||
REACTIVATE = 'reactivate'
|
||||
}
|
||||
|
||||
// Outlets are used to insert additional components into CommentBox
|
||||
@ -41,7 +42,8 @@ export enum CommentBoxOutletPosition {
|
||||
TOP_OF_FIRST_COMMENT = 'top_of_first_comment',
|
||||
BOTTOM_OF_COMMENT_EDITOR = 'bottom_of_comment_editor',
|
||||
LEFT_OF_OPTIONS_MENU = 'left_of_options_menu',
|
||||
LEFT_OF_REPLY_PLACEHOLDER = 'left_of_reply_placeholder'
|
||||
LEFT_OF_REPLY_PLACEHOLDER = 'left_of_reply_placeholder',
|
||||
BETWEEN_SAVE_AND_CANCEL_BUTTONS = 'between_save_and_cancel_buttons'
|
||||
}
|
||||
|
||||
interface CommentBoxProps<T> {
|
||||
@ -172,7 +174,7 @@ export const CommentBox = <T = unknown,>({
|
||||
placeHolder: getString(comments.length ? 'replyHere' : 'leaveAComment'),
|
||||
tabEdit: getString('write'),
|
||||
tabPreview: getString('preview'),
|
||||
save: getString('comment'),
|
||||
save: getString(comments.length ? 'reply' : 'comment'),
|
||||
cancel: getString('cancel')
|
||||
}}
|
||||
value={markdown}
|
||||
@ -205,6 +207,11 @@ export const CommentBox = <T = unknown,>({
|
||||
console.error('handleAction must be implemented...') // eslint-disable-line no-console
|
||||
}
|
||||
}}
|
||||
secondarySaveButton={
|
||||
comments.length
|
||||
? (outlets[CommentBoxOutletPosition.BETWEEN_SAVE_AND_CANCEL_BUTTONS] as typeof Button)
|
||||
: undefined
|
||||
}
|
||||
onCancel={_onCancel}
|
||||
hideCancel={hideCancel}
|
||||
setDirty={_dirty => {
|
||||
@ -274,6 +281,12 @@ const CommentsThread = <T = unknown,>({
|
||||
</>
|
||||
</Render>
|
||||
|
||||
<Render when={commentItem?.outdated}>
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} className={css.outdated}>
|
||||
{getString('pr.outdated')}
|
||||
</Text>
|
||||
</Render>
|
||||
|
||||
<FlexExpander />
|
||||
<Layout.Horizontal>
|
||||
<Render when={index === 0 && outlets[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]}>
|
||||
|
@ -9,10 +9,12 @@ import {
|
||||
ButtonVariation,
|
||||
ButtonSize,
|
||||
Button,
|
||||
FlexExpander
|
||||
FlexExpander,
|
||||
StringSubstitute
|
||||
} from '@harness/uicore'
|
||||
import type { CellProps, Column } from 'react-table'
|
||||
import { noop, orderBy } from 'lodash-es'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import type { TypesCommit } from 'services/code'
|
||||
@ -21,6 +23,7 @@ import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
||||
import { formatDate } from 'utils/Utils'
|
||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||
import type { CODERoutes } from 'RouteDefinitions'
|
||||
import css from './CommitsView.module.scss'
|
||||
|
||||
interface CommitsViewProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
||||
@ -63,7 +66,7 @@ export function CommitsView({
|
||||
Cell: ({ row }: CellProps<TypesCommit>) => {
|
||||
return (
|
||||
<Text color={Color.BLACK} lineClamp={1} className={css.rowText}>
|
||||
{row.original.message}
|
||||
{renderPullRequestLinkFromCommitMessage(repoMetadata, routes, row.original.message)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@ -85,7 +88,7 @@ export function CommitsView({
|
||||
}
|
||||
}
|
||||
],
|
||||
[repoMetadata.path, routes]
|
||||
[repoMetadata, routes]
|
||||
)
|
||||
const commitsGroupedByDate: Record<string, TypesCommit[]> = useMemo(
|
||||
() =>
|
||||
@ -142,3 +145,36 @@ export function CommitsView({
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function renderPullRequestLinkFromCommitMessage(
|
||||
repoMetadata: GitInfoProps['repoMetadata'],
|
||||
routes: CODERoutes,
|
||||
commitMessage = ''
|
||||
) {
|
||||
let message: string | JSX.Element = commitMessage
|
||||
const match = message.match(/\(#\d+\)$/)
|
||||
|
||||
if (match?.length) {
|
||||
message = message.replace(match[0], '({URL})')
|
||||
const pullRequestId = match[0].replace('(#', '').replace(')', '')
|
||||
|
||||
message = (
|
||||
<StringSubstitute
|
||||
str={message}
|
||||
vars={{
|
||||
URL: (
|
||||
<Link
|
||||
to={routes.toCODEPullRequest({
|
||||
repoPath: repoMetadata.path as string,
|
||||
pullRequestId
|
||||
})}>
|
||||
#{pullRequestId}
|
||||
</Link>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
Layout,
|
||||
Text,
|
||||
ButtonSize,
|
||||
useToaster
|
||||
useToaster,
|
||||
ButtonProps
|
||||
} from '@harness/uicore'
|
||||
import cx from 'classnames'
|
||||
import { Render } from 'react-jsx-match'
|
||||
@ -30,6 +31,7 @@ import { CopyButton } from 'components/CopyButton/CopyButton'
|
||||
import { AppWrapper } from 'App'
|
||||
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
|
||||
import { CodeCommentStatusButton } from 'components/CodeCommentStatusButton/CodeCommentStatusButton'
|
||||
import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondarySaveButton/CodeCommentSecondarySaveButton'
|
||||
import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/CodeCommentStatusSelect'
|
||||
import {
|
||||
activitiesToDiffCommentItems,
|
||||
@ -93,7 +95,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
const { mutate: saveComment } = useMutate({ verb: 'POST', path })
|
||||
const { mutate: updateComment } = useMutate({ verb: 'PATCH', path: ({ id }) => `${path}/${id}` })
|
||||
const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` })
|
||||
const [comments, setComments] = useState<DiffCommentItem<TypesPullReqActivity>[]>(activitiesToDiffCommentItems(diff))
|
||||
const [comments, setComments] = useState<DiffCommentItem<TypesPullReqActivity>[]>([])
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const commentsRef = useRef<DiffCommentItem<TypesPullReqActivity>[]>(comments)
|
||||
const setContainerRef = useCallback(
|
||||
@ -130,6 +132,19 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
[diffRenderer, renderCustomContent]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// For some unknown reason, comments is [] when we switch to Changes tab very quickly sometimes,
|
||||
// but diff is not empty, and activitiesToDiffCommentItems(diff) is not []. So assigning
|
||||
// comments = activitiesToDiffCommentItems(diff) from the useState() is not enough.
|
||||
if (diff) {
|
||||
const _comments = activitiesToDiffCommentItems(diff)
|
||||
|
||||
if (_comments.length > 0 && !comments.length) {
|
||||
setComments(_comments)
|
||||
}
|
||||
}
|
||||
}, [diff, comments])
|
||||
|
||||
useEffect(
|
||||
function createDiffRenderer() {
|
||||
if (inView && !diffRenderer) {
|
||||
@ -231,7 +246,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
)
|
||||
|
||||
useEffect(
|
||||
function renderAnnotatations() {
|
||||
function renderCodeComments() {
|
||||
if (readOnly) {
|
||||
return
|
||||
}
|
||||
@ -274,6 +289,28 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
rowElement.after(commentRowElement)
|
||||
|
||||
const element = commentRowElement.firstElementChild as HTMLTableCellElement
|
||||
const resetCommentState = (ignoreCurrentComment = true) => {
|
||||
// Clean up CommentBox rendering and reset states bound to lineInfo
|
||||
ReactDOM.unmountComponentAtNode(element as HTMLDivElement)
|
||||
commentRowElement.parentElement?.removeChild(commentRowElement)
|
||||
lineInfo.oppositeRowElement?.parentElement?.removeChild(
|
||||
lineInfo.oppositeRowElement?.nextElementSibling as Element
|
||||
)
|
||||
delete lineInfo.rowElement.dataset.annotated
|
||||
|
||||
setTimeout(
|
||||
() =>
|
||||
setComments(
|
||||
commentsRef.current.filter(item => {
|
||||
if (ignoreCurrentComment) {
|
||||
return item !== comment
|
||||
}
|
||||
return true
|
||||
})
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// Note: CommentBox is rendered as an independent React component
|
||||
// everything passed to it must be either values, or refs. If you
|
||||
@ -284,7 +321,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
<AppWrapper>
|
||||
<CommentBox
|
||||
commentItems={comment.commentItems}
|
||||
initialContent={getInitialCommentContentFromSelection(diff)}
|
||||
initialContent={''}
|
||||
width={isSideBySide ? 'calc(100vw / 2 - 163px)' : undefined} // TODO: Re-calcualte for standalone version
|
||||
onHeightChange={boxHeight => {
|
||||
if (comment.height !== boxHeight) {
|
||||
@ -292,16 +329,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
setTimeout(() => setComments([...commentsRef.current]), 0)
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
// Clean up CommentBox rendering and reset states bound to lineInfo
|
||||
ReactDOM.unmountComponentAtNode(element as HTMLDivElement)
|
||||
commentRowElement.parentElement?.removeChild(commentRowElement)
|
||||
lineInfo.oppositeRowElement?.parentElement?.removeChild(
|
||||
lineInfo.oppositeRowElement?.nextElementSibling as Element
|
||||
)
|
||||
delete lineInfo.rowElement.dataset.annotated
|
||||
setTimeout(() => setComments(commentsRef.current.filter(item => item !== comment)), 0)
|
||||
}}
|
||||
onCancel={resetCommentState}
|
||||
setDirty={setDirty}
|
||||
currentUserName={currentUser.display_name}
|
||||
handleAction={async (action, value, commentItem) => {
|
||||
@ -325,6 +353,9 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
await saveComment(payload)
|
||||
.then((newComment: TypesPullReqActivity) => {
|
||||
updatedItem = activityToCommentItem(newComment)
|
||||
diff.fileActivities?.push(newComment)
|
||||
comment.commentItems.push(updatedItem)
|
||||
resetCommentState(false)
|
||||
})
|
||||
.catch(exception => {
|
||||
result = false
|
||||
@ -334,24 +365,19 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
}
|
||||
|
||||
case CommentAction.REPLY: {
|
||||
const parentComment = diff.fileActivities?.find(
|
||||
activity => diff.filePath === activity.code_comment?.path
|
||||
)
|
||||
|
||||
if (parentComment) {
|
||||
await saveComment({
|
||||
type: CommentType.CODE_COMMENT,
|
||||
text: value,
|
||||
parent_id: Number(parentComment.id as number)
|
||||
parent_id: Number(commentItem?.payload?.id as number)
|
||||
})
|
||||
.then(newComment => {
|
||||
updatedItem = activityToCommentItem(newComment)
|
||||
diff.fileActivities?.push(newComment)
|
||||
})
|
||||
.catch(exception => {
|
||||
result = false
|
||||
showError(getErrorMessage(exception), 0)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@ -408,6 +434,14 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
onCommentUpdate={onCommentUpdate}
|
||||
commentItems={comment.commentItems}
|
||||
/>
|
||||
),
|
||||
[CommentBoxOutletPosition.BETWEEN_SAVE_AND_CANCEL_BUTTONS]: (props: ButtonProps) => (
|
||||
<CodeCommentSecondarySaveButton
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata as TypesPullReq}
|
||||
commentItems={comment.commentItems}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
autoFocusAndPositioning
|
||||
@ -541,7 +575,3 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function getInitialCommentContentFromSelection(_diff: DiffFileEntry) {
|
||||
return ''
|
||||
}
|
||||
|
@ -196,6 +196,7 @@ export const activityToCommentItem = (activity: TypesPullReqActivity): CommentIt
|
||||
created: activity.created as number,
|
||||
updated: activity.edited as number,
|
||||
deleted: activity.deleted as number,
|
||||
outdated: !!activity.code_comment?.outdated,
|
||||
content: (activity.text || (activity.payload as Unknown)?.Message) as string,
|
||||
payload: activity
|
||||
})
|
||||
|
@ -53,6 +53,7 @@ interface MarkdownEditorWithPreviewProps {
|
||||
editorHeight?: string
|
||||
noBorder?: boolean
|
||||
viewRef?: React.MutableRefObject<EditorView | undefined>
|
||||
secondarySaveButton?: typeof Button
|
||||
|
||||
// When set to true, the editor will be scrolled to center of screen
|
||||
// and cursor is set to the end of the document
|
||||
@ -71,7 +72,8 @@ export function MarkdownEditorWithPreview({
|
||||
editorHeight,
|
||||
noBorder,
|
||||
viewRef: viewRefProp,
|
||||
autoFocusAndPositioning
|
||||
autoFocusAndPositioning,
|
||||
secondarySaveButton: SecondarySaveButton
|
||||
}: MarkdownEditorWithPreviewProps) {
|
||||
const [selectedTab, setSelectedTab] = useState(MarkdownEditorTab.WRITE)
|
||||
const viewRef = useRef<EditorView>()
|
||||
@ -281,6 +283,12 @@ export function MarkdownEditorWithPreview({
|
||||
onClick={() => onSave?.(viewRef.current?.state.doc.toString() || '')}
|
||||
text={i18n.save}
|
||||
/>
|
||||
{SecondarySaveButton && (
|
||||
<SecondarySaveButton
|
||||
disabled={!dirty}
|
||||
onClick={async () => await onSave?.(viewRef.current?.state.doc.toString() || '')}
|
||||
/>
|
||||
)}
|
||||
{!hideCancel && <Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={i18n.cancel} />}
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
|
@ -222,8 +222,10 @@ export interface StringsMap {
|
||||
'pr.modalTitle': string
|
||||
'pr.notMergeableWithUnresolvedComment': string
|
||||
'pr.openForReview': string
|
||||
'pr.outdated': string
|
||||
'pr.prBranchPushInfo': string
|
||||
'pr.prCanBeMerged': string
|
||||
'pr.prClosed': string
|
||||
'pr.prMerged': string
|
||||
'pr.prMergedInfo': string
|
||||
'pr.prReviewSubmit': string
|
||||
@ -255,12 +257,16 @@ export interface StringsMap {
|
||||
pullRequestEmpty: string
|
||||
pullRequests: string
|
||||
quote: string
|
||||
reactivate: string
|
||||
readMe: string
|
||||
refresh: string
|
||||
reject: string
|
||||
rejected: string
|
||||
remove: string
|
||||
renameFile: string
|
||||
reply: string
|
||||
replyAndReactivate: string
|
||||
replyAndResolve: string
|
||||
replyHere: string
|
||||
repoCloneHeader: string
|
||||
repoCloneLabel: string
|
||||
@ -284,6 +290,7 @@ export interface StringsMap {
|
||||
resetZoom: string
|
||||
resolve: string
|
||||
resolved: string
|
||||
resolvedComments: string
|
||||
reviewers: string
|
||||
samplePayloadUrl: string
|
||||
save: string
|
||||
@ -306,7 +313,7 @@ export interface StringsMap {
|
||||
tags: string
|
||||
title: string
|
||||
tooltipRepoEdit: string
|
||||
unresolve: string
|
||||
unrsolvedComment: string
|
||||
'unsavedChanges.leave': string
|
||||
'unsavedChanges.message': string
|
||||
'unsavedChanges.stay': string
|
||||
|
@ -2,29 +2,29 @@ import { useCallback, useEffect } from 'react'
|
||||
import type { CodeCommentState } from 'utils/Utils'
|
||||
|
||||
const PR_COMMENT_STATUS_CHANGED_EVENT = 'PR_COMMENT_STATUS_CHANGED_EVENT'
|
||||
export const PULL_REQUEST_ALL_COMMENTS_ID = -99999
|
||||
|
||||
interface UseEmitCodeCommentStatusProps {
|
||||
id?: number
|
||||
onMatch: (status: CodeCommentState) => void
|
||||
onMatch: (status: CodeCommentState, id: number) => void
|
||||
}
|
||||
|
||||
export function useEmitCodeCommentStatus({ id, onMatch }: UseEmitCodeCommentStatusProps) {
|
||||
const callback = useCallback(
|
||||
event => {
|
||||
if (id && event.detail.id === id) {
|
||||
onMatch(event.detail.status)
|
||||
if ((id && event.detail.id === id) || id === PULL_REQUEST_ALL_COMMENTS_ID) {
|
||||
onMatch(event.detail.status, event.detail.id)
|
||||
}
|
||||
},
|
||||
[id, onMatch]
|
||||
)
|
||||
const updateStatus = useCallback(
|
||||
const emitCodeCommentStatus = useCallback(
|
||||
(status: CodeCommentState) => {
|
||||
const event = new CustomEvent(PR_COMMENT_STATUS_CHANGED_EVENT, { detail: { id, status } })
|
||||
document.dispatchEvent(event)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener(PR_COMMENT_STATUS_CHANGED_EVENT, callback)
|
||||
|
||||
@ -33,5 +33,5 @@ export function useEmitCodeCommentStatus({ id, onMatch }: UseEmitCodeCommentStat
|
||||
}
|
||||
}, [callback])
|
||||
|
||||
return updateStatus
|
||||
return emitCodeCommentStatus
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ import { useCallback, useState } from 'react'
|
||||
export enum UserPreference {
|
||||
DIFF_VIEW_STYLE = 'DIFF_VIEW_STYLE',
|
||||
DIFF_LINE_BREAKS = 'DIFF_LINE_BREAKS',
|
||||
PULL_REQUESTS_FILTER_SELECTED_OPTIONS = 'PULL_REQUESTS_FILTER_SELECTED_OPTIONS'
|
||||
PULL_REQUESTS_FILTER_SELECTED_OPTIONS = 'PULL_REQUESTS_FILTER_SELECTED_OPTIONS',
|
||||
PULL_REQUEST_MERGE_STRATEGY = 'PULL_REQUEST_MERGE_STRATEGY'
|
||||
}
|
||||
|
||||
export function useUserPreference<T = string>(key: UserPreference, defaultValue: T): [T, (val: T) => void] {
|
||||
|
@ -193,6 +193,7 @@ pr:
|
||||
failedToDeleteComment: Failed to delete comment. Please try again.
|
||||
failedToUpdateCommentStatus: Failed to update comment status. Please try again.
|
||||
prMerged: This Pull Request was merged
|
||||
prClosed: This Pull Request was closed.
|
||||
reviewSubmitted: Review submitted.
|
||||
prReviewSubmit: '{user} {state|approved:approved, rejected:rejected,changereq:requested to, reviewed} the pull request. {time}'
|
||||
prMergedInfo: '{user} merged branch {source} into {target} {time}.'
|
||||
@ -218,6 +219,7 @@ pr:
|
||||
closeDesc: Close this pull request. You can still re-open the request after closing.
|
||||
forceMergeWithUnresolvedComment: This pull request has {unrsolvedComment} unresolved {unrsolvedComment|1:comment,comments}. As an administrator, you can still merge it.\nAre you sure you want to force merge it?
|
||||
notMergeableWithUnresolvedComment: This pull request has {unrsolvedComment} unresolved {unrsolvedComment|1:comment,comments}. Please resolve them before merging.
|
||||
outdated: Outdated
|
||||
prState:
|
||||
draftHeading: This pull request is still a work in progress
|
||||
draftDesc: Draft pull requests cannot be merged.
|
||||
@ -238,6 +240,9 @@ viewed: Viewed
|
||||
comment: Comment
|
||||
addComment: Add comment
|
||||
replyHere: Reply here...
|
||||
reply: Reply
|
||||
replyAndResolve: Reply & Resolve
|
||||
replyAndReactivate: Reply & Reactivate
|
||||
leaveAComment: Leave a comment...
|
||||
lineBreaks: Line Breaks
|
||||
quote: Quote
|
||||
@ -352,6 +357,8 @@ showEverything: Show everything
|
||||
allComments: All comments
|
||||
whatsNew: What's new
|
||||
myComments: My comments/replies
|
||||
resolvedComments: Resolved comments
|
||||
unrsolvedComment: Unresolved comment
|
||||
resetZoom: Reset Zoom
|
||||
zoomIn: Zoom In
|
||||
zoomOut: Zoom Out
|
||||
@ -367,7 +374,7 @@ repoUpdate: Repository Updated
|
||||
deleteRepoText: Are you sure you want to delete the repository '{REPONAME}'?
|
||||
deleteRepoTitle: Delete the repository
|
||||
resolve: Resolve
|
||||
unresolve: Unresolve
|
||||
reactivate: Reactivate
|
||||
generateCloneCred: + Generate Clone Credential
|
||||
generateCloneText: 'Please generate clone credential if it’s your first time'
|
||||
getMyCloneTitle: Get My Clone Credential
|
||||
|
@ -15,7 +15,7 @@ interface CodeCommentHeaderProps {
|
||||
}
|
||||
|
||||
export const CodeCommentHeader: React.FC<CodeCommentHeaderProps> = ({ commentItems, threadId }) => {
|
||||
const _isCodeComment = isCodeComment(commentItems)
|
||||
const _isCodeComment = isCodeComment(commentItems) && !commentItems[0].deleted
|
||||
const id = `code-comment-snapshot-${threadId}`
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -8,7 +8,7 @@ import { useAppContext } from 'AppContext'
|
||||
import type { TypesPullReqActivity } from 'services/code'
|
||||
import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from 'components/CommentBox/CommentBox'
|
||||
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||
import { getErrorMessage, orderSortDate, dayAgoInMS, ButtonRoleProps } from 'utils/Utils'
|
||||
import { getErrorMessage, orderSortDate, ButtonRoleProps } from 'utils/Utils'
|
||||
import { activityToCommentItem } from 'components/DiffViewer/DiffViewerUtils'
|
||||
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
|
||||
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
||||
@ -28,13 +28,6 @@ export interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | '
|
||||
prHasChanged?: boolean
|
||||
}
|
||||
|
||||
export enum prSortState {
|
||||
SHOW_EVERYTHING = 'showEverything',
|
||||
ALL_COMMENTS = 'allComments',
|
||||
WHATS_NEW = 'whatsNew',
|
||||
MY_COMMENTS = 'myComments'
|
||||
}
|
||||
|
||||
export const Conversation: React.FC<ConversationProps> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
@ -57,10 +50,8 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
})
|
||||
const { showError } = useToaster()
|
||||
const [dateOrderSort, setDateOrderSort] = useState<boolean | 'desc' | 'asc'>(orderSortDate.ASC)
|
||||
const [prShowState, setPrShowState] = useState<SelectOption>({
|
||||
label: `Show Everything `,
|
||||
value: 'showEverything'
|
||||
})
|
||||
const activityFilters = useActivityFilters()
|
||||
const [activityFilter, setActivityFilter] = useState<SelectOption>(activityFilters[0] as SelectOption)
|
||||
const activityBlocks = useMemo(() => {
|
||||
// Each block may have one or more activities which are grouped into it. For example, one comment block
|
||||
// contains a parent comment and multiple replied comments
|
||||
@ -86,61 +77,30 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
blocks.push(parentActivity.map(activityToCommentItem))
|
||||
})
|
||||
|
||||
// Group title-change events into one single block
|
||||
// Disabled for now, @see https://harness.atlassian.net/browse/SCM-79
|
||||
// const titleChangeItems =
|
||||
// blocks.filter(
|
||||
// _activities => isSystemComment(_activities) && _activities[0].payload?.type === CommentType.TITLE_CHANGE
|
||||
// ) || []
|
||||
switch (activityFilter.value) {
|
||||
case PRCommentFilterType.ALL_COMMENTS:
|
||||
return blocks.filter(_activities => !isSystemComment(_activities))
|
||||
|
||||
// titleChangeItems.forEach((value, index) => {
|
||||
// if (index > 0) {
|
||||
// titleChangeItems[0].push(...value)
|
||||
// }
|
||||
// })
|
||||
// titleChangeItems.shift()
|
||||
// return blocks.filter(_activities => !titleChangeItems.includes (_activities))
|
||||
case PRCommentFilterType.RESOLVED_COMMENTS:
|
||||
return blocks.filter(_activities => isCodeComment(_activities) && _activities[0].payload?.resolved)
|
||||
|
||||
if (prShowState.value === prSortState.ALL_COMMENTS) {
|
||||
const allCommentBlock = blocks.filter(_activities => !isSystemComment(_activities))
|
||||
return allCommentBlock
|
||||
}
|
||||
case PRCommentFilterType.UNRESOLVED_COMMENTS:
|
||||
return blocks.filter(_activities => isCodeComment(_activities) && !_activities[0].payload?.resolved)
|
||||
|
||||
if (prShowState.value === prSortState.WHATS_NEW) {
|
||||
// get current time in seconds and subtract it by a day and see if comments are newer than a day
|
||||
const lastComment = blocks[blocks.length - 1]
|
||||
const lastCommentTime = lastComment[lastComment.length - 1].payload?.edited
|
||||
if (lastCommentTime !== undefined) {
|
||||
const currentTime = lastCommentTime - dayAgoInMS
|
||||
|
||||
const newestBlock = blocks.filter(_activities => {
|
||||
const mostRecentComment = _activities[_activities.length - 1]
|
||||
if (mostRecentComment?.payload?.edited !== undefined) {
|
||||
return mostRecentComment?.payload?.edited > currentTime
|
||||
}
|
||||
})
|
||||
return newestBlock
|
||||
}
|
||||
}
|
||||
|
||||
// show only comments made by user or replies in threads by user
|
||||
if (prShowState.value === prSortState.MY_COMMENTS) {
|
||||
case PRCommentFilterType.MY_COMMENTS: {
|
||||
const allCommentBlock = blocks.filter(_activities => !isSystemComment(_activities))
|
||||
const userCommentsOnly = allCommentBlock.filter(_activities => {
|
||||
const userCommentReply = _activities.filter(
|
||||
authorIsUser => authorIsUser.payload?.author?.uid === currentUser.uid
|
||||
)
|
||||
if (userCommentReply.length !== 0) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return userCommentReply.length !== 0
|
||||
})
|
||||
return userCommentsOnly
|
||||
}
|
||||
}
|
||||
|
||||
return blocks
|
||||
}, [activities, dateOrderSort, prShowState, currentUser.uid])
|
||||
}, [activities, dateOrderSort, activityFilter, currentUser.uid])
|
||||
const path = useMemo(
|
||||
() => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/comments`,
|
||||
[repoMetadata.path, pullRequestMetadata.number]
|
||||
@ -183,28 +143,11 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
<Layout.Horizontal className={css.sortContainer} padding={{ top: 'xxlarge', bottom: 'medium' }}>
|
||||
<Container>
|
||||
<Select
|
||||
items={[
|
||||
{
|
||||
label: getString('showEverything'),
|
||||
value: prSortState.SHOW_EVERYTHING
|
||||
},
|
||||
{
|
||||
label: getString('allComments'),
|
||||
value: prSortState.ALL_COMMENTS
|
||||
},
|
||||
{
|
||||
label: getString('whatsNew'),
|
||||
value: prSortState.WHATS_NEW
|
||||
},
|
||||
{
|
||||
label: getString('myComments'),
|
||||
value: prSortState.MY_COMMENTS
|
||||
}
|
||||
]}
|
||||
value={prShowState}
|
||||
items={activityFilters}
|
||||
value={activityFilter}
|
||||
className={css.selectButton}
|
||||
onChange={newState => {
|
||||
setPrShowState(newState)
|
||||
setActivityFilter(newState)
|
||||
refetchActivities()
|
||||
}}
|
||||
/>
|
||||
@ -387,3 +330,41 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
</PullRequestTabContentWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export enum PRCommentFilterType {
|
||||
SHOW_EVERYTHING = 'showEverything',
|
||||
ALL_COMMENTS = 'allComments',
|
||||
MY_COMMENTS = 'myComments',
|
||||
RESOLVED_COMMENTS = 'resolvedComments',
|
||||
UNRESOLVED_COMMENTS = 'unresolvedComments'
|
||||
}
|
||||
|
||||
function useActivityFilters() {
|
||||
const { getString } = useStrings()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
label: getString('showEverything'),
|
||||
value: PRCommentFilterType.SHOW_EVERYTHING
|
||||
},
|
||||
{
|
||||
label: getString('allComments'),
|
||||
value: PRCommentFilterType.ALL_COMMENTS
|
||||
},
|
||||
{
|
||||
label: getString('myComments'),
|
||||
value: PRCommentFilterType.MY_COMMENTS
|
||||
},
|
||||
{
|
||||
label: getString('unrsolvedComment'),
|
||||
value: PRCommentFilterType.UNRESOLVED_COMMENTS
|
||||
},
|
||||
{
|
||||
label: getString('resolvedComments'),
|
||||
value: PRCommentFilterType.RESOLVED_COMMENTS
|
||||
}
|
||||
],
|
||||
[getString]
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,14 @@
|
||||
background-color: var(--red-50) !important;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
background-color: var(--grey-100) !important;
|
||||
}
|
||||
|
||||
&.draft {
|
||||
background-color: var(--orange-100) !important;
|
||||
}
|
||||
|
||||
&.unchecked {
|
||||
background-color: #fcf4e3 !important; // Note: No UICore color variable for this background
|
||||
}
|
||||
@ -48,12 +56,16 @@
|
||||
line-height: 20px !important;
|
||||
color: var(--green-800) !important;
|
||||
|
||||
&.closed {
|
||||
color: var(--grey-600) !important;
|
||||
}
|
||||
|
||||
&.merged {
|
||||
color: var(--purple-700) !important;
|
||||
}
|
||||
|
||||
&.draft {
|
||||
color: var(--grey-700) !important;
|
||||
color: var(--orange-900) !important;
|
||||
}
|
||||
|
||||
&.unmergeable {
|
||||
@ -106,6 +118,11 @@
|
||||
--background-color-active: var(--grey-100) !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
--background-color: var(--green-800) !important;
|
||||
|
@ -4,19 +4,21 @@ declare const styles: {
|
||||
readonly main: string
|
||||
readonly merged: string
|
||||
readonly error: string
|
||||
readonly closed: string
|
||||
readonly draft: string
|
||||
readonly unchecked: string
|
||||
readonly layout: string
|
||||
readonly secondaryButton: string
|
||||
readonly btn: string
|
||||
readonly heading: string
|
||||
readonly sub: string
|
||||
readonly draft: string
|
||||
readonly unmergeable: string
|
||||
readonly popover: string
|
||||
readonly menuItem: string
|
||||
readonly menuReviewItem: string
|
||||
readonly btnWrapper: string
|
||||
readonly hasError: string
|
||||
readonly disabled: string
|
||||
readonly mergeContainer: string
|
||||
}
|
||||
export default styles
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
Button,
|
||||
ButtonVariation,
|
||||
@ -32,6 +32,7 @@ import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { Images } from 'images'
|
||||
import { getErrorMessage, MergeCheckStatus, permissionProps } from 'utils/Utils'
|
||||
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
||||
import ReviewSplitButton from 'components/Changes/ReviewSplitButton/ReviewSplitButton'
|
||||
import css from './PullRequestActionsBox.module.scss'
|
||||
|
||||
@ -53,6 +54,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const { showError } = useToaster()
|
||||
const { currentUser } = useAppContext()
|
||||
const { hooks, standalone } = useAppContext()
|
||||
const space = useGetSpaceParam()
|
||||
const { mutate: mergePR, loading } = useMutate({
|
||||
@ -67,9 +69,11 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
() => pullRequestMetadata.merge_check_status === MergeCheckStatus.MERGEABLE,
|
||||
[pullRequestMetadata]
|
||||
)
|
||||
const isClosed = pullRequestMetadata.state === PullRequestState.CLOSED
|
||||
const isOpen = pullRequestMetadata.state === PullRequestState.OPEN
|
||||
const unchecked = useMemo(
|
||||
() => pullRequestMetadata.merge_check_status === MergeCheckStatus.UNCHECKED,
|
||||
[pullRequestMetadata]
|
||||
() => pullRequestMetadata.merge_check_status === MergeCheckStatus.UNCHECKED && !isClosed,
|
||||
[pullRequestMetadata, isClosed]
|
||||
)
|
||||
const isDraft = pullRequestMetadata.is_draft
|
||||
const mergeOptions: PRMergeOption[] = [
|
||||
@ -77,18 +81,19 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
method: 'squash',
|
||||
title: getString('pr.mergeOptions.squashAndMerge'),
|
||||
desc: getString('pr.mergeOptions.squashAndMergeDesc'),
|
||||
disabled: false
|
||||
disabled: mergeable === false
|
||||
},
|
||||
{
|
||||
method: 'merge',
|
||||
title: getString('pr.mergeOptions.createMergeCommit'),
|
||||
desc: getString('pr.mergeOptions.createMergeCommitDesc')
|
||||
desc: getString('pr.mergeOptions.createMergeCommitDesc'),
|
||||
disabled: mergeable === false
|
||||
},
|
||||
{
|
||||
method: 'rebase',
|
||||
title: getString('pr.mergeOptions.rebaseAndMerge'),
|
||||
desc: getString('pr.mergeOptions.rebaseAndMergeDesc'),
|
||||
disabled: false
|
||||
disabled: mergeable === false
|
||||
},
|
||||
{
|
||||
method: 'close',
|
||||
@ -106,7 +111,11 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
},
|
||||
[space]
|
||||
)
|
||||
const [mergeOption, setMergeOption] = useState<PRMergeOption>(mergeOptions[1])
|
||||
|
||||
const [mergeOption, setMergeOption] = useUserPreference<PRMergeOption>(
|
||||
UserPreference.PULL_REQUEST_MERGE_STRATEGY,
|
||||
mergeOptions[mergeable === false ? 3 : 1]
|
||||
)
|
||||
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||
{
|
||||
resource: {
|
||||
@ -116,6 +125,11 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
},
|
||||
[space]
|
||||
)
|
||||
const isActiveUserPROwner = useMemo(() => {
|
||||
return (
|
||||
!!currentUser?.uid && !!pullRequestMetadata?.author?.uid && currentUser?.uid === pullRequestMetadata?.author?.uid
|
||||
)
|
||||
}, [currentUser, pullRequestMetadata])
|
||||
|
||||
if (pullRequestMetadata.state === PullRequestFilterOption.MERGED) {
|
||||
return <MergeInfo pullRequestMetadata={pullRequestMetadata} />
|
||||
@ -124,31 +138,46 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
return (
|
||||
<Container
|
||||
className={cx(css.main, {
|
||||
[css.error]: mergeable === false && !unchecked,
|
||||
[css.unchecked]: unchecked
|
||||
[css.error]: mergeable === false && !unchecked && !isClosed && !isDraft,
|
||||
[css.unchecked]: unchecked,
|
||||
[css.closed]: isClosed,
|
||||
[css.draft]: isDraft
|
||||
})}>
|
||||
<Layout.Vertical spacing="xlarge">
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }} className={css.layout}>
|
||||
{(unchecked && <img src={Images.PrUnchecked} width={20} height={20} />) || (
|
||||
<Icon
|
||||
name={isDraft ? CodeIcon.Draft : mergeable === false ? 'warning-sign' : 'tick-circle'}
|
||||
name={
|
||||
isDraft ? CodeIcon.Draft : isClosed ? 'issue' : mergeable === false ? 'warning-sign' : 'tick-circle'
|
||||
}
|
||||
size={20}
|
||||
color={isDraft ? Color.ORANGE_900 : mergeable === false ? Color.RED_500 : Color.GREEN_700}
|
||||
color={
|
||||
isDraft
|
||||
? Color.ORANGE_900
|
||||
: isClosed
|
||||
? Color.GREY_500
|
||||
: mergeable === false
|
||||
? Color.RED_500
|
||||
: Color.GREEN_700
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
className={cx(css.sub, {
|
||||
[css.unchecked]: unchecked,
|
||||
[css.draft]: isDraft,
|
||||
[css.unmergeable]: mergeable === false
|
||||
[css.closed]: isClosed,
|
||||
[css.unmergeable]: mergeable === false && isOpen
|
||||
})}>
|
||||
{getString(
|
||||
isDraft
|
||||
? 'prState.draftHeading'
|
||||
: isClosed
|
||||
? 'pr.prClosed'
|
||||
: unchecked
|
||||
? 'pr.checkingToMerge'
|
||||
: mergeable === false
|
||||
: mergeable === false && isOpen
|
||||
? 'pr.cantBeMerged'
|
||||
: 'pr.branchHasNoConflicts'
|
||||
)}
|
||||
@ -196,6 +225,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
refreshPr={onPRStateChanged}
|
||||
disabled={isActiveUserPROwner}
|
||||
/>
|
||||
<Container
|
||||
inline
|
||||
@ -265,29 +295,6 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
.catch(exception => showError(getErrorMessage(exception)))
|
||||
}
|
||||
}}>
|
||||
{/* TODO: These two items are used for creating a PR
|
||||
<Menu.Item
|
||||
className={css.menuItem}
|
||||
text={
|
||||
<>
|
||||
<BIcon icon="blank" />
|
||||
<strong>Create pull request</strong>
|
||||
<p>Open a pull request that is ready for review</p>
|
||||
<p>Automatically request reviews from code owners</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Menu.Item
|
||||
className={css.menuItem}
|
||||
text={
|
||||
<>
|
||||
<BIcon icon="blank" />
|
||||
<strong>Create draft pull request</strong>
|
||||
<p>Does not request code reviews and cannot be merged</p>
|
||||
<p>Cannot be merged until marked ready for review</p>
|
||||
</>
|
||||
}
|
||||
/> */}
|
||||
{mergeOptions.map(option => {
|
||||
return (
|
||||
<Menu.Item
|
||||
|
@ -23,7 +23,7 @@ import { useAppContext } from 'AppContext'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { voidFn, getErrorMessage, MergeCheckStatus } from 'utils/Utils'
|
||||
import { voidFn, getErrorMessage } from 'utils/Utils'
|
||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||
import type { TypesPullReq, TypesPullReqStats, TypesRepository } from 'services/code'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
@ -77,9 +77,6 @@ export default function PullRequest() {
|
||||
}
|
||||
return false
|
||||
}, [prData?.stats, stats])
|
||||
const mergeable = useMemo(() => {
|
||||
return prData?.merge_check_status === MergeCheckStatus.MERGEABLE
|
||||
}, [prData])
|
||||
|
||||
useEffect(
|
||||
function setStatsIfNotSet() {
|
||||
@ -105,14 +102,14 @@ export default function PullRequest() {
|
||||
const fn = () => {
|
||||
if (repoMetadata) {
|
||||
refetchPullRequest().then(() => {
|
||||
interval = window.setTimeout(fn, mergeable ? PR_POLLING_INTERVAL : PR_POLLING_INTERVAL_WHEN_NOT_MERGEABLE)
|
||||
interval = window.setTimeout(fn, PR_POLLING_INTERVAL)
|
||||
})
|
||||
}
|
||||
}
|
||||
let interval = window.setTimeout(fn, mergeable ? PR_POLLING_INTERVAL : PR_POLLING_INTERVAL_WHEN_NOT_MERGEABLE)
|
||||
let interval = window.setTimeout(fn, PR_POLLING_INTERVAL)
|
||||
|
||||
return () => window.clearTimeout(interval)
|
||||
}, [repoMetadata, refetchPullRequest, path, mergeable])
|
||||
}, [repoMetadata, refetchPullRequest, path])
|
||||
|
||||
const activeTab = useMemo(
|
||||
() =>
|
||||
@ -359,5 +356,4 @@ enum PullRequestSection {
|
||||
CHECKS = 'checks'
|
||||
}
|
||||
|
||||
const PR_POLLING_INTERVAL = 15000
|
||||
const PR_POLLING_INTERVAL_WHEN_NOT_MERGEABLE = 5000
|
||||
const PR_POLLING_INTERVAL = 10000
|
||||
|
@ -90,7 +90,7 @@ export function FileContent({
|
||||
title: getString('content'),
|
||||
panel: (
|
||||
<Container className={css.fileContent}>
|
||||
<Layout.Vertical spacing="small">
|
||||
<Layout.Vertical spacing="small" style={{ maxWidth: '100%' }}>
|
||||
<LatestCommitForFile
|
||||
repoMetadata={repoMetadata}
|
||||
latestCommit={resourceContent.latest_commit}
|
||||
|
@ -184,6 +184,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
|
||||
{getString('dangerDeleteRepo')}
|
||||
</Text>
|
||||
<Button
|
||||
disabled={true} // TODO: Disable until backend has soft delete
|
||||
intent={Intent.DANGER}
|
||||
onClick={() => {
|
||||
confirmDeleteBranch()
|
||||
|
Loading…
Reference in New Issue
Block a user