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