API integration for different type of merges (#268)

Co-authored-by: Tan Nhu <tnhu@users.noreply.github.com>
jobatzil/rename
Tan Nhu 2023-01-31 08:41:46 -08:00 committed by GitHub
parent e2b350e704
commit a2cac1e82b
49 changed files with 1948 additions and 1446 deletions

View File

@ -16,7 +16,7 @@ const ExactSharedPackages = [
'@harness/monaco-yaml',
'monaco-editor',
'monaco-editor-core',
'monaco-languages',
// 'monaco-languages',
'monaco-plugin-helpers',
'react-monaco-editor'
]

View File

@ -37,10 +37,10 @@
"@blueprintjs/datetime": "3.13.0",
"@blueprintjs/select": "3.12.3",
"@harness/design-system": "1.4.0",
"@harness/icons": "1.95.1",
"@harness/icons": "1.101.1",
"@harness/ng-tooltip": ">=1.31.25",
"@harness/telemetry": ">=1.0.42",
"@harness/uicore": "3.95.1",
"@harness/uicore": "3.106.3",
"@harness/use-modal": "1.3.0",
"@popperjs/core": "^2.4.2",
"@uiw/react-markdown-editor": "^5.10.1",

View File

@ -49,6 +49,7 @@ interface ChangesProps extends Pick<GitInfoProps, 'repoMetadata'> {
emptyMessage: string
pullRequestMetadata?: TypesPullReq
className?: string
onCommentUpdate: () => void
}
export const Changes: React.FC<ChangesProps> = ({
@ -59,6 +60,7 @@ export const Changes: React.FC<ChangesProps> = ({
emptyTitle,
emptyMessage,
pullRequestMetadata,
onCommentUpdate,
className
}) => {
const { getString } = useStrings()
@ -72,7 +74,9 @@ export const Changes: React.FC<ChangesProps> = ({
loading,
refetch
} = useGet<string>({
path: `/api/v1/repos/${repoMetadata?.path}/+/compare/${targetBranch}...${sourceBranch}`,
path: `/api/v1/repos/${repoMetadata?.path}/+/${
pullRequestMetadata ? `pullreq/${pullRequestMetadata.number}/diff` : `compare/${targetBranch}...${sourceBranch}`
}`,
lazy: !targetBranch || !sourceBranch
})
const {
@ -214,6 +218,7 @@ export const Changes: React.FC<ChangesProps> = ({
stickyTopPosition={STICKY_TOP_POSITION}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
onCommentUpdate={onCommentUpdate}
/>
))}
</Layout.Vertical>

View File

@ -225,20 +225,20 @@ const CommentsThread = <T = unknown,>({
title={
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
<Text inline icon="code-chat"></Text>
<Avatar name={commentItem.author} size="small" hoverCard={false} />
<Avatar name={commentItem?.author} size="small" hoverCard={false} />
<Text inline>
<strong>{commentItem.author}</strong>
<strong>{commentItem?.author}</strong>
</Text>
<PipeSeparator height={8} />
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
<ReactTimeago date={new Date(commentItem.updated)} />
<ReactTimeago date={new Date(commentItem?.updated)} />
</Text>
<Render when={commentItem.updated !== commentItem.created || !!commentItem.deleted}>
<Render when={commentItem?.updated !== commentItem?.created || !!commentItem?.deleted}>
<>
<PipeSeparator height={8} />
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
{getString(commentItem.deleted ? 'deleted' : 'edited')}
{getString(commentItem?.deleted ? 'deleted' : 'edited')}
</Text>
</>
</Render>
@ -249,7 +249,7 @@ const CommentsThread = <T = unknown,>({
icon="Options"
iconProps={{ size: 14 }}
style={{ padding: '5px' }}
disabled={!!commentItem.deleted}
disabled={!!commentItem?.deleted}
width="100px"
items={[
{
@ -258,7 +258,7 @@ const CommentsThread = <T = unknown,>({
},
{
text: getString('quote'),
onClick: () => onQuote(commentItem.content)
onClick: () => onQuote(commentItem?.content)
},
'-',
{
@ -309,12 +309,12 @@ const CommentsThread = <T = unknown,>({
</Container>
</Truthy>
<Else>
<Match expr={commentItem.deleted}>
<Match expr={commentItem?.deleted}>
<Truthy>
<Text className={css.deleted}>{getString('commentDeleted')}</Text>
</Truthy>
<Else>
<MarkdownEditor.Markdown source={commentItem.content} />
<MarkdownEditor.Markdown source={commentItem?.content} />
</Else>
</Match>
</Else>

View File

@ -6,4 +6,11 @@
.description textarea {
height: 300px !important;
}
.checkbox {
color: var(--grey-600);
font-size: var(--form-input-font-size) !important;
font-weight: 500 !important;
display: inline-flex !important;
}
}

View File

@ -4,5 +4,6 @@ declare const styles: {
readonly main: string
readonly title: string
readonly description: string
readonly checkbox: string
}
export default styles

View File

@ -8,6 +8,7 @@
import React from 'react'
import { Dialog, Intent } from '@blueprintjs/core'
import * as yup from 'yup'
import { Render } from 'react-jsx-match'
import {
Button,
ButtonProps,
@ -35,6 +36,7 @@ import css from './CreatePullRequestModal.module.scss'
interface FormData {
title: string
description: string
draft: boolean
}
interface CreatePullRequestModalProps extends Pick<GitInfoProps, 'repoMetadata'> {
@ -65,7 +67,8 @@ export function useCreatePullRequestModal({
target_branch: targetGitRef,
source_branch: sourceGitRef,
title: title,
description: description
description: description,
is_draft: formData.draft
}
try {
@ -97,7 +100,8 @@ export function useCreatePullRequestModal({
<Formik<FormData>
initialValues={{
title: '',
description: ''
description: '',
draft: false
}}
formName="createPullRequest"
enableReinitialize={true}
@ -129,6 +133,7 @@ export function useCreatePullRequestModal({
className={css.description}
maxLength={1024 * 50}
/>
<FormInput.CheckBox label={getString('pr.createDraftPR')} name="draft" className={css.checkbox} />
<Layout.Horizontal
spacing="small"
@ -143,7 +148,9 @@ export function useCreatePullRequestModal({
<Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="spinner" size={16} />}
<Render when={loading}>
<Icon intent={Intent.PRIMARY} name="spinner" size={16} />
</Render>
</Layout.Horizontal>
</FormikForm>
</Formik>

View File

@ -49,6 +49,7 @@ interface DiffViewerProps extends Pick<GitInfoProps, 'repoMetadata'> {
stickyTopPosition?: number
readOnly?: boolean
pullRequestMetadata?: TypesPullReq
onCommentUpdate: () => void
}
//
@ -62,7 +63,8 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
stickyTopPosition = 0,
readOnly,
repoMetadata,
pullRequestMetadata
pullRequestMetadata,
onCommentUpdate
}) => {
const { getString } = useStrings()
const [viewed, setViewed] = useState(false)
@ -379,6 +381,10 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
}
}
if (result) {
onCommentUpdate()
}
return [result, updatedItem]
}}
/>,

View File

@ -16,7 +16,9 @@ export enum CommentType {
CODE_COMMENT = 'code-comment',
TITLE_CHANGE = 'title-change',
REVIEW_SUBMIT = 'review-submit',
MERGE = 'merge'
MERGE = 'merge',
BRANCH_UPDATE = 'branch-update',
STATE_CHANGE = 'state-change'
}
export const PR_CODE_COMMENT_PAYLOAD_VERSION = '0.1'
@ -194,7 +196,7 @@ export const activityToCommentItem = (activity: TypesPullReqActivity): CommentIt
created: activity.created as number,
updated: activity.edited as number,
deleted: activity.deleted as number,
content: (activity.text || activity.payload?.Message) as string,
content: (activity.text || (activity.payload as Unknown)?.Message) as string,
payload: activity
})

View File

@ -85,9 +85,15 @@
overflow: auto;
:global {
.wmde-markdown .anchor {
.wmde-markdown {
.anchor {
display: none;
}
pre {
position: relative;
}
}
}
}

View File

@ -3,11 +3,11 @@ import { Button, Container, ButtonVariation, NoDataCard, IconName } from '@harne
import { noop } from 'lodash-es'
import { CodeIcon } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import emptyStateImage from 'images/empty-state.svg'
import { Images } from 'images'
import css from './NoResultCard.module.scss'
interface NoResultCardProps {
showWhen: () => boolean
showWhen?: () => boolean
forSearch: boolean
title?: string
message?: string
@ -18,7 +18,7 @@ interface NoResultCardProps {
}
export const NoResultCard: React.FC<NoResultCardProps> = ({
showWhen,
showWhen = () => true,
forSearch,
title,
message,
@ -36,7 +36,7 @@ export const NoResultCard: React.FC<NoResultCardProps> = ({
return (
<Container className={css.main}>
<NoDataCard
image={emptyStateImage}
image={Images.EmptyState}
messageTitle={forSearch ? title || getString('noResultTitle') : undefined}
message={
forSearch ? emptySearchMessage || getString('noResultMessage') : message || getString('noResultMessage')

View File

@ -1,26 +0,0 @@
.state {
--color: var(--green-700) !important;
--bg: var(--green-50) !important;
color: var(--color) !important;
background-color: var(--bg) !important;
font-size: var(--font-size-small) !important;
font-weight: 600 !important;
padding: 4px 8px !important;
border-radius: 4px;
&.merged {
--color: var(--purple-700) !important;
--bg: var(--purple-50) !important;
}
&.closed {
--color: var(--grey-700) !important;
--bg: var(--grey-100) !important;
}
&.rejected {
--color: var(--red-700) !important;
--bg: var(--red-50) !important;
}
}

View File

@ -1,40 +0,0 @@
import React from 'react'
import { Text, Color, StringSubstitute, IconName } from '@harness/uicore'
import cx from 'classnames'
import { CodeIcon, PullRequestState } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import css from './PRStateLabel.module.scss'
export const PRStateLabel: React.FC<{ state: PullRequestState }> = ({ state }) => {
const { getString } = useStrings()
let color = Color.GREEN_700
let icon: IconName = CodeIcon.PullRequest
let clazz: typeof css | string = ''
switch (state) {
case PullRequestState.MERGED:
color = Color.PURPLE_700
icon = CodeIcon.PullRequest
clazz = css.merged
break
case PullRequestState.CLOSED:
color = Color.GREY_600
icon = CodeIcon.PullRequest
clazz = css.closed
break
case PullRequestState.REJECTED:
color = Color.RED_600
icon = CodeIcon.PullRequestRejected
clazz = css.rejected
break
default:
break
}
return (
<Text inline className={cx(css.state, clazz)} icon={icon} iconProps={{ color, size: 9 }}>
<StringSubstitute str={getString('pr.state')} vars={{ state }} />
</Text>
)
}

View File

@ -0,0 +1,58 @@
.prStatus {
--fg: var(--green-500);
--bg: var(--green-50);
display: inline-flex !important;
align-items: center !important;
justify-content: center;
border-radius: 4px;
padding: 5px 10px !important;
font-weight: 600 !important;
font-size: 12px !important;
line-height: 12px !important;
color: var(--fg) !important;
background-color: var(--bg) !important;
> span {
padding-right: 5px !important;
}
&.iconOnly {
padding: 5px !important;
> span {
padding-right: 0 !important;
}
}
svg {
path {
fill: var(--fg) !important;
}
g {
stroke: var(--fg) !important;
}
}
&.open {
--fg: var(--green-700);
--bg: var(--green-50);
}
&.merged {
--fg: var(--blue-800);
--bg: var(--blue-50);
}
&.closed {
--fg: var(--grey-600);
--bg: var(--grey-100);
}
&.draft {
--fg: var(--orange-900);
--bg: var(--orange-100);
}
}

View File

@ -1,9 +1,11 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly state: string
readonly prStatus: string
readonly iconOnly: string
readonly open: string
readonly merged: string
readonly closed: string
readonly rejected: string
readonly draft: string
}
export default styles

View File

@ -0,0 +1,50 @@
import React from 'react'
import { Text, StringSubstitute, IconName } from '@harness/uicore'
import cx from 'classnames'
import { CodeIcon } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import type { TypesPullReq } from 'services/code'
import css from './PullRequestStateLabel.module.scss'
export const PullRequestStateLabel: React.FC<{ data: TypesPullReq; iconSize?: number; iconOnly?: boolean }> = ({
data,
iconSize = 20,
iconOnly = false
}) => {
const { getString } = useStrings()
const maps = {
open: {
icon: CodeIcon.PullRequest,
css: css.open
},
merged: {
icon: CodeIcon.Merged,
css: css.merged
},
closed: {
icon: CodeIcon.Merged,
css: css.closed
},
draft: {
icon: CodeIcon.Draft,
css: css.draft
},
unknown: {
icon: CodeIcon.PullRequest,
css: css.open
}
}
const map = data.is_draft ? maps.draft : maps[data.state || 'unknown']
return (
<Text
tag="span"
className={cx(css.prStatus, map.css, { [css.iconOnly]: iconOnly })}
icon={map.icon as IconName}
iconProps={{ size: iconOnly ? iconSize : 12 }}>
{!iconOnly && (
<StringSubstitute str={getString('pr.state')} vars={{ state: data.is_draft ? 'draft' : data.state }} />
)}
</Text>
)
}

View File

@ -67,9 +67,9 @@ export default function MonacoSourceCodeEditor({
const scrollbar = autoHeight ? 'hidden' : 'auto'
useEffect(() => {
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions?.(diagnosticsOptions)
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions?.(diagnosticsOptions)
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions)
monaco.languages.typescript?.typescriptDefaults?.setDiagnosticsOptions?.(diagnosticsOptions)
monaco.languages.typescript?.javascriptDefaults?.setDiagnosticsOptions?.(diagnosticsOptions)
monaco.languages.typescript?.typescriptDefaults?.setCompilerOptions?.(compilerOptions)
}, [])
return (

View File

@ -0,0 +1,9 @@
.main {
:global {
.wmde-markdown {
pre {
position: relative;
}
}
}
}

View File

@ -0,0 +1,6 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly main: string
}
export default styles

View File

@ -4,6 +4,7 @@ import MarkdownEditor from '@uiw/react-markdown-editor'
import { useStrings } from 'framework/strings'
import { SourceCodeEditor } from 'components/SourceCodeEditor/SourceCodeEditor'
import type { SourceCodeEditorProps } from 'utils/Utils'
import css from './SourceCodeViewer.module.scss'
interface MarkdownViewerProps {
source: string
@ -13,7 +14,7 @@ export function MarkdownViewer({ source }: MarkdownViewerProps) {
const { getString } = useStrings()
return (
<Container>
<Container className={css.main}>
<Suspense fallback={<Text>{getString('loading')}</Text>}>
<MarkdownEditor.Markdown
source={source}

View File

@ -80,6 +80,7 @@ export interface StringsMap {
deployKeys: string
description: string
diff: string
draft: string
edit: string
editFile: string
editNotAllowed: string
@ -149,9 +150,11 @@ export interface StringsMap {
payloadUrl: string
payloadUrlLabel: string
'pr.ableToMerge': string
'pr.authorCommentedPR': string
'pr.branchHasNoConflicts': string
'pr.buttonText': string
'pr.cantMerge': string
'pr.createDraftPR': string
'pr.descriptionPlaceHolder': string
'pr.diffStatsLabel': string
'pr.diffStatus': string
@ -164,12 +167,25 @@ export interface StringsMap {
'pr.failedToUpdateTitle': string
'pr.fileDeleted': string
'pr.fileUnchanged': string
'pr.mergeOptions.close': string
'pr.mergeOptions.closeDesc': string
'pr.mergeOptions.createMergeCommit': string
'pr.mergeOptions.createMergeCommitDesc': string
'pr.mergeOptions.rebaseAndMerge': string
'pr.mergeOptions.rebaseAndMergeDesc': string
'pr.mergeOptions.squashAndMerge': string
'pr.mergeOptions.squashAndMergeDesc': string
'pr.mergePR': string
'pr.metaLine': string
'pr.modalTitle': string
'pr.openForReview': string
'pr.prBranchPushInfo': string
'pr.prCanBeMerged': string
'pr.prMerged': string
'pr.prMergedInfo': string
'pr.prStateChanged': string
'pr.prStateChangedDraft': string
'pr.readyForReview': string
'pr.reviewChanges': string
'pr.reviewSubmitted': string
'pr.showDiff': string
@ -181,6 +197,8 @@ export interface StringsMap {
'pr.titleChangedTable': string
'pr.titlePlaceHolder': string
'pr.unified': string
'prState.draftDesc': string
'prState.draftHeading': string
prefixBase: string
prefixCompare: string
prev: string

View File

@ -156,6 +156,7 @@ enableSSLVerification: 'Enable SSL verification'
createWebhook: Create Webhook
webhook: Webhook
diff: Diff
draft: Draft
conversation: Conversation
pr:
ableToMerge: Able to merge.
@ -164,8 +165,9 @@ pr:
titlePlaceHolder: Enter the pull request title
descriptionPlaceHolder: Leave pull request comment here
modalTitle: Open a pull request
buttonText: Open pull request
metaLine: '{user} wants to merge {number} {number|1:commit,commits} into {target} from {source}'
buttonText: Create pull request
createDraftPR: Create draft pull request
metaLine: '{user} wants to merge {commits} {commitsCount|1:commit,commits} into {target} from {source}'
state: '{state|closed:Closed,merged:Merged,rejected:Rejected,draft:Draft,Open}'
statusLine: '#{number} {state|merged:merged,closed:closed,rejected:rejected,opened} {time} by {user}'
diffStatus: '{status|deleted:Deleted,new:Added,renamed:Renamed,copied:Copied,Changed}'
@ -189,11 +191,29 @@ pr:
prMerged: This Pull Request was merged
reviewSubmitted: Review submitted.
prMergedInfo: '{user} merged branch {source} into {target} {time}.'
prBranchPushInfo: '{user} pushed a new commit {commit}.'
prStateChanged: '{user} changed pull request state from {old} to {new}.'
prStateChangedDraft: '{user} opened pull request for review.'
titleChanged: '{user} changed title from {old} to {new}.'
titleChangedTable: |
### Other title changes in history
| Author | Old Name | New Name | Date |
| ----------- | -------- | -------- | ---- |
readyForReview: Ready for review
openForReview: Open for review
authorCommentedPR: '{author} submitted this pull request {time}'
mergeOptions:
squashAndMerge: Squash and merge
squashAndMergeDesc: All commits from this branch will be combined into one commit in the base branch.
createMergeCommit: Create a merge commit
createMergeCommitDesc: All commits from this branch will be added to the base branch via a merge commit.
rebaseAndMerge: Rebase and merge
rebaseAndMergeDesc: All commits from this branch will be rebased and added to the base branch.
close: Close pull request
closeDesc: Close this pull request. You can still re-open the request after closing.
prState:
draftHeading: This pull request is still a work in progress
draftDesc: Draft pull requests cannot be merged.
webhookListingContent: 'create,delete,deployment ...'
general: 'General'
webhooks: 'Webhooks'

16
web/src/images/index.ts Normal file
View File

@ -0,0 +1,16 @@
import PrOpen from 'images/pull-request-open.svg'
import PrMerged from 'images/pull-request-merged.svg'
import PrClosed from 'images/pull-request-closed.svg'
import PrRejected from 'images/pull-request-rejected.svg'
import PrDraft from 'images/pull-request-draft.svg'
import EmptyState from 'images/empty-state.svg'
export const Images = {
PrOpen,
PrMerged,
PrClosed,
PrRejected,
PrDraft,
EmptyState
}

View File

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

View File

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View File

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 549 B

After

Width:  |  Height:  |  Size: 549 B

View File

@ -1,3 +1,4 @@
import { noop } from 'lodash-es'
import React, { useState } from 'react'
import { Container, PageBody, NoDataCard, Tabs } from '@harness/uicore'
import { useHistory } from 'react-router-dom'
@ -7,7 +8,7 @@ import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { voidFn, getErrorMessage, LIST_FETCHING_LIMIT } from 'utils/Utils'
import emptyStateImage from 'images/empty-state.svg'
import { Images } from 'images'
import { makeDiffRefs } from 'utils/GitUtils'
import { CommitsView } from 'components/CommitsView/CommitsView'
import { Changes } from 'components/Changes/Changes'
@ -81,7 +82,7 @@ export default function Compare() {
{(!targetGitRef || !sourceGitRef) && (
<Container className={css.noDataContainer}>
<NoDataCard image={emptyStateImage} message={getString('selectToViewMore')} />
<NoDataCard image={Images.EmptyState} message={getString('selectToViewMore')} />
</Container>
)}
@ -120,6 +121,7 @@ export default function Compare() {
sourceBranch={sourceGitRef}
emptyTitle={getString('noChanges')}
emptyMessage={getString('noChangesCompare')}
onCommentUpdate={noop} // TODO: Update tab stats
/>
</Container>
)

View File

@ -4,8 +4,15 @@
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16),
0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 5px;
padding: var(--spacing-xlarge) !important;
border-radius: 4px;
padding: var(--spacing-xlarge) var(--spacing-large) !important;
}
/* TODO: This styling per design is too white */
.descBox {
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 4px;
padding: var(--spacing-xlarge) var(--spacing-large) !important;
}
.snapshot {
@ -70,7 +77,7 @@
.newCommentCreated {
box-shadow: 0px 0px 5px rgb(37 41 192);
border-radius: 4px;
transition: box-shadow 1s ease-in-out;
transition: box-shadow 0.5s ease-in-out;
&.clear {
box-shadow: none;

View File

@ -2,6 +2,7 @@
// this is an auto-generated file
declare const styles: {
readonly box: string
readonly descBox: string
readonly snapshot: string
readonly title: string
readonly fname: string

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import {
Avatar,
Color,
@ -20,9 +20,8 @@ import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
import type { TypesPullReqActivity } from 'services/code'
import type { OpenapiUpdatePullReqRequest, TypesPullReqActivity } from 'services/code'
import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from 'components/CommentBox/CommentBox'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
import { useConfirmAct } from 'hooks/useConfirmAction'
@ -33,18 +32,14 @@ import {
PullRequestCodeCommentPayload
} from 'components/DiffViewer/DiffViewerUtils'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
import { PullRequestStatusInfo } from './PullRequestStatusInfo/PullRequestStatusInfo'
import { PullRequestActionsBox } from './PullRequestActionsBox/PullRequestActionsBox'
import css from './Conversation.module.scss'
interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
refreshPullRequestMetadata: () => void
onCommentUpdate: () => void
}
export const Conversation: React.FC<ConversationProps> = ({
repoMetadata,
pullRequestMetadata,
refreshPullRequestMetadata
}) => {
export const Conversation: React.FC<ConversationProps> = ({ repoMetadata, pullRequestMetadata, onCommentUpdate }) => {
const { getString } = useStrings()
const { currentUser } = useAppContext()
const {
@ -62,12 +57,8 @@ export const Conversation: React.FC<ConversationProps> = ({
// contains a parent comment and multiple replied comments
const blocks: CommentItem<TypesPullReqActivity>[][] = []
if (newComments.length) {
blocks.push(orderBy(newComments, 'edited', 'desc').map(activityToCommentItem))
}
// Determine all parent activities
const parentActivities = orderBy(activities?.filter(activity => !activity.parent_id) || [], 'edited', 'desc').map(
const parentActivities = orderBy(activities?.filter(activity => !activity.parent_id) || [], 'edited', 'asc').map(
_comment => [_comment]
)
@ -84,20 +75,26 @@ export const Conversation: React.FC<ConversationProps> = ({
blocks.push(parentActivity.map(activityToCommentItem))
})
// Group title-change events into one single block
const titleChangeItems =
blocks.filter(
_activities => isSystemComment(_activities) && _activities[0].payload?.type === CommentType.TITLE_CHANGE
) || []
titleChangeItems.forEach((value, index) => {
if (index > 0) {
titleChangeItems[0].push(...value)
if (newComments.length) {
blocks.push(orderBy(newComments, 'edited', 'asc').map(activityToCommentItem))
}
})
titleChangeItems.shift()
return blocks.filter(_activities => !titleChangeItems.includes(_activities))
// 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
// ) || []
// titleChangeItems.forEach((value, index) => {
// if (index > 0) {
// titleChangeItems[0].push(...value)
// }
// })
// titleChangeItems.shift()
// return blocks.filter(_activities => !titleChangeItems.includes(_activities))
return blocks
}, [activities, newComments])
const path = useMemo(
() => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/comments`,
@ -108,6 +105,10 @@ export const Conversation: React.FC<ConversationProps> = ({
const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` })
const confirmAct = useConfirmAct()
const [commentCreated, setCommentCreated] = useState(false)
const refreshPR = useCallback(() => {
onCommentUpdate()
refetchActivities()
}, [onCommentUpdate, refetchActivities])
useAnimateNewCommentBox(commentCreated, setCommentCreated)
@ -115,20 +116,17 @@ export const Conversation: React.FC<ConversationProps> = ({
<PullRequestTabContentWrapper loading={loading} error={error} onRetry={refetchActivities}>
<Container>
<Layout.Vertical spacing="xlarge">
<PullRequestStatusInfo
<PullRequestActionsBox
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
onMerge={() => {
refreshPullRequestMetadata()
refetchActivities()
}}
onPRStateChanged={refreshPR}
/>
<Container>
<Layout.Vertical spacing="xlarge">
<DescriptionBox
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
refreshPullRequestMetadata={refreshPullRequestMetadata}
onCommentUpdate={onCommentUpdate}
/>
{activityBlocks?.map((blocks, index) => {
@ -145,7 +143,7 @@ export const Conversation: React.FC<ConversationProps> = ({
<CommentBox
key={threadId}
fluid
className={cx({ [css.newCommentCreated]: commentCreated && !index })}
className={cx({ [css.newCommentCreated]: commentCreated && index === activityBlocks.length - 1 })}
getString={getString}
commentItems={commentItems}
currentUserName={currentUser.display_name}
@ -195,6 +193,10 @@ export const Conversation: React.FC<ConversationProps> = ({
break
}
if (result) {
onCommentUpdate()
}
return [result, updatedItem]
}}
outlets={{
@ -227,6 +229,11 @@ export const Conversation: React.FC<ConversationProps> = ({
result = false
showError(getErrorMessage(exception), 0)
})
if (result) {
onCommentUpdate()
}
return [result, updatedItem]
}}
/>
@ -241,10 +248,10 @@ export const Conversation: React.FC<ConversationProps> = ({
const DescriptionBox: React.FC<ConversationProps> = ({
repoMetadata,
pullRequestMetadata,
refreshPullRequestMetadata
onCommentUpdate: refreshPullRequestMetadata
}) => {
const [edit, setEdit] = useState(false)
const [updated, setUpdated] = useState(pullRequestMetadata.edited as number)
// const [updated, setUpdated] = useState(pullRequestMetadata.edited as number)
const [originalContent, setOriginalContent] = useState(pullRequestMetadata.description as string)
const [content, setContent] = useState(originalContent)
const { getString } = useStrings()
@ -259,15 +266,29 @@ const DescriptionBox: React.FC<ConversationProps> = ({
<Container className={css.box}>
<Layout.Vertical spacing="medium">
<Container>
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
<Layout.Horizontal spacing="xsmall" style={{ alignItems: 'center' }}>
<StringSubstitute
str={getString('pr.authorCommentedPR')}
vars={{
author: (
<>
<Avatar name={name} size="small" hoverCard={false} />
<Text inline>
<Text inline margin={{ right: 'xsmall' }}>
<strong>{name}</strong>
</Text>
<PipeSeparator height={8} />
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
<ReactTimeago date={updated} />
</>
),
time: (
<Text inline>
<ReactTimeago date={pullRequestMetadata.created as number} />
</Text>
)
}}
/>
{/* <PipeSeparator height={8} />
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
</Text> */}
<FlexExpander />
<OptionsMenuButton
isDark={false}
@ -288,15 +309,16 @@ const DescriptionBox: React.FC<ConversationProps> = ({
<MarkdownEditorWithPreview
value={content}
onSave={value => {
mutate({
const payload: OpenapiUpdatePullReqRequest = {
title: pullRequestMetadata.title,
description: value
})
}
mutate(payload)
.then(() => {
setContent(value)
setOriginalContent(value)
setEdit(false)
setUpdated(Date.now())
// setUpdated(Date.now())
refreshPullRequestMetadata()
})
.catch(exception => showError(getErrorMessage(exception), 0, getString('pr.failedToUpdate')))
@ -322,7 +344,7 @@ const DescriptionBox: React.FC<ConversationProps> = ({
}
function isCodeComment(commentItems: CommentItem<TypesPullReqActivity>[]) {
return commentItems[0]?.payload?.payload?.type === CommentType.CODE_COMMENT
return (commentItems[0]?.payload?.payload as Unknown)?.type === CommentType.CODE_COMMENT
}
interface CodeCommentHeaderProps {
@ -400,6 +422,68 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
)
}
case CommentType.BRANCH_UPDATE: {
return (
<Container>
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }} className={css.box}>
<Avatar name={payload?.author?.display_name} size="small" hoverCard={false} />
<Text>
<StringSubstitute
str={getString('pr.prBranchPushInfo')}
vars={{
user: <strong>{payload?.author?.display_name}</strong>,
commit: <strong>{(payload?.payload as Unknown)?.new}</strong>
}}
/>
</Text>
<FlexExpander />
<Text
inline
font={{ variation: FontVariation.SMALL }}
color={Color.GREY_400}
width={100}
style={{ textAlign: 'right' }}>
<ReactTimeago date={payload?.created as number} />
</Text>
</Layout.Horizontal>
</Container>
)
}
case CommentType.STATE_CHANGE: {
const openFromDraft =
(payload?.payload as Unknown)?.old === (payload?.payload as Unknown)?.new &&
(payload?.payload as Unknown)?.new === 'open' &&
(payload?.payload as Unknown)?.is_draft === false
return (
<Container>
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }} className={css.box}>
<Avatar name={payload?.author?.display_name} size="small" hoverCard={false} />
<Text>
<StringSubstitute
str={getString(openFromDraft ? 'pr.prStateChangedDraft' : 'pr.prStateChanged')}
vars={{
user: <strong>{payload?.author?.display_name}</strong>,
old: <strong>{(payload?.payload as Unknown)?.old}</strong>,
new: <strong>{(payload?.payload as Unknown)?.new}</strong>
}}
/>
</Text>
<FlexExpander />
<Text
inline
font={{ variation: FontVariation.SMALL }}
color={Color.GREY_400}
width={100}
style={{ textAlign: 'right' }}>
<ReactTimeago date={payload?.created as number} />
</Text>
</Layout.Horizontal>
</Container>
)
}
case CommentType.TITLE_CHANGE: {
return (
<Container className={css.box}>
@ -412,15 +496,20 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
user: <strong>{payload?.author?.display_name}</strong>,
old: (
<strong>
<s>{payload?.payload?.old}</s>
<s>{(payload?.payload as Unknown)?.old}</s>
</strong>
),
new: <strong>{payload?.payload?.new}</strong>
new: <strong>{(payload?.payload as Unknown)?.new}</strong>
}}
/>
</Text>
<FlexExpander />
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
<Text
inline
font={{ variation: FontVariation.SMALL }}
color={Color.GREY_400}
width={100}
style={{ textAlign: 'right' }}>
<ReactTimeago date={payload?.created as number} />
</Text>
</Layout.Horizontal>
@ -435,8 +524,8 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
.filter((_, index) => index > 0)
.map(
item =>
`|${item.author}|<s>${item.payload?.payload?.old}</s>|${
item.payload?.payload?.new
`|${item.author}|<s>${(item.payload?.payload as Unknown)?.old}</s>|${
(item.payload?.payload as Unknown)?.new
}|${formatDate(item.updated)} ${formatTime(item.updated)}|`
)
)

View File

@ -0,0 +1,70 @@
.main {
--bar-height: 60px;
background-color: var(--green-50) !important; // #f6fff2
margin: -24px -24px 0 !important;
position: sticky;
top: 0;
z-index: 2;
&.merged {
border-color: transparent !important;
background: var(--purple-50) !important;
}
.layout {
height: var(--bar-height);
padding: 0 var(--spacing-xlarge) !important;
.secondaryButton,
[class*='Button--variation-tertiary'] {
--box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.04), 0px 2px 4px rgba(96, 97, 112, 0.16) !important;
}
}
.btn {
background-color: var(--green-800) !important;
color: var(--white) !important;
}
.heading {
font-weight: 600 !important;
font-size: 16px !important;
line-height: 24px !important;
color: var(--grey-700) !important;
}
.sub {
font-weight: 500 !important;
font-size: 13px !important;
line-height: 20px !important;
color: var(--grey-500) !important;
}
}
.popover {
transform: translateY(5px) !important;
.menuItem {
strong {
display: inline-block;
margin-left: 10px;
}
p {
font-size: 12px;
padding-left: 27px;
line-height: 16px;
margin: 5px 0;
max-width: 320px;
}
}
}
.btnWrapper {
button {
--background-color: var(--green-800) !important;
--background-color-hover: var(--green-900) !important;
--background-color-active: var(--green-900) !important;
}
}

View File

@ -3,8 +3,13 @@
declare const styles: {
readonly main: string
readonly merged: string
readonly layout: string
readonly secondaryButton: string
readonly btn: string
readonly heading: string
readonly sub: string
readonly popover: string
readonly menuItem: string
readonly btnWrapper: string
}
export default styles

View File

@ -0,0 +1,244 @@
import React, { useState } from 'react'
import {
Button,
ButtonVariation,
Color,
Container,
FlexExpander,
Icon,
Layout,
SplitButton,
StringSubstitute,
Text,
useToaster
} from '@harness/uicore'
import { useMutate } from 'restful-react'
import { Case, Else, Match, Render, Truthy } from 'react-jsx-match'
import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import type { EnumMergeMethod, OpenapiMergePullReq, OpenapiStatePullReqRequest, TypesPullReq } from 'services/code'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
import { getErrorMessage } from 'utils/Utils'
import css from './PullRequestActionsBox.module.scss'
interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
onPRStateChanged: () => void
}
interface PRMergeOption {
method: EnumMergeMethod | 'close'
title: string
desc: string
}
export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
repoMetadata,
pullRequestMetadata,
onPRStateChanged
}) => {
const { getString } = useStrings()
const { showError } = useToaster()
const { mutate: mergePR, loading } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/merge`
})
const { mutate: updatePRState, loading: loadingState } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/state`
})
const isDraft = pullRequestMetadata.is_draft
const mergeOptions: PRMergeOption[] = [
{
method: 'squash',
title: getString('pr.mergeOptions.squashAndMerge'),
desc: getString('pr.mergeOptions.squashAndMergeDesc')
},
{
method: 'merge',
title: getString('pr.mergeOptions.createMergeCommit'),
desc: getString('pr.mergeOptions.createMergeCommitDesc')
},
{
method: 'rebase',
title: getString('pr.mergeOptions.rebaseAndMerge'),
desc: getString('pr.mergeOptions.rebaseAndMergeDesc')
},
{
method: 'close',
title: getString('pr.mergeOptions.close'),
desc: getString('pr.mergeOptions.closeDesc')
}
]
const [mergeOption, setMergeOption] = useState<PRMergeOption>(mergeOptions[0])
if (pullRequestMetadata.state === PullRequestFilterOption.MERGED) {
return <MergeInfo pullRequestMetadata={pullRequestMetadata} />
}
return (
<Container className={css.main}>
<Layout.Vertical spacing="xlarge">
<Container>
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }} className={css.layout}>
<Icon
name={isDraft ? CodeIcon.Draft : 'tick-circle'}
size={20}
color={isDraft ? Color.ORANGE_900 : Color.GREEN_700}
/>
<Text className={css.sub}>{getString(isDraft ? 'prState.draftHeading' : 'pr.branchHasNoConflicts')}</Text>
<FlexExpander />
<Render when={loading || loadingState}>
<Icon name={CodeIcon.InputSpinner} size={16} margin={{ right: 'xsmall' }} />
</Render>
<Match expr={isDraft}>
<Truthy>
<Button
className={css.secondaryButton}
text={getString('pr.readyForReview')}
variation={ButtonVariation.TERTIARY}
onClick={() => {
const payload: OpenapiStatePullReqRequest = { is_draft: false, state: 'open' }
updatePRState(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
}}
/>
</Truthy>
<Else>
<Container>
<Match expr={pullRequestMetadata.state}>
<Case val={PullRequestState.CLOSED}>
<Button
className={css.secondaryButton}
text={getString('pr.openForReview')}
variation={ButtonVariation.TERTIARY}
onClick={() => {
const payload: OpenapiStatePullReqRequest = { state: 'open' }
updatePRState(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
}}
/>
</Case>
<Case val={PullRequestState.OPEN}>
<Layout.Horizontal
inline
spacing="huge"
className={cx({ [css.btnWrapper]: mergeOption.method !== 'close' })}>
<SplitButton
text={mergeOption.title}
className={cx({ [css.secondaryButton]: mergeOption.method === 'close' })}
variation={
mergeOption.method === 'close' ? ButtonVariation.TERTIARY : ButtonVariation.PRIMARY
}
popoverProps={{
interactionKind: 'click',
usePortal: true,
popoverClassName: css.popover,
position: PopoverPosition.BOTTOM_RIGHT,
transitionDuration: 1000
}}
onClick={() => {
if (mergeOption.method !== 'close') {
const payload: OpenapiMergePullReq = {
method: mergeOption.method,
force: false,
delete_branch: false
}
mergePR(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
} else {
const payload: OpenapiStatePullReqRequest = { state: 'closed' }
updatePRState(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
}
}}
disabled={loading}
dropdownDisabled={loading}>
{/* <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
key={option.method}
className={css.menuItem}
text={
<>
<BIcon icon={mergeOption.method === option.method ? 'tick' : 'blank'} />
<strong>{option.title}</strong>
<p>{option.desc}</p>
</>
}
onClick={() => setMergeOption(option)}
/>
)
})}
</SplitButton>
</Layout.Horizontal>
</Case>
</Match>
</Container>
</Else>
</Match>
</Layout.Horizontal>
</Container>
</Layout.Vertical>
</Container>
)
}
const MergeInfo: React.FC<{ pullRequestMetadata: TypesPullReq }> = ({ pullRequestMetadata }) => {
const { getString } = useStrings()
return (
<Container className={cx(css.main, css.merged)}>
<Layout.Horizontal spacing="medium" flex={{ alignItems: 'center' }} className={css.layout}>
<Icon name={CodeIcon.PullRequest} size={20} color={Color.PURPLE_700} />
<Container>
{/* <Text className={css.heading}>{getString('pr.prMerged')}</Text> */}
<Text className={css.sub}>
<StringSubstitute
str={getString('pr.prMergedInfo')}
vars={{
user: <strong>{pullRequestMetadata.merger?.display_name}</strong>,
source: <strong>{pullRequestMetadata.source_branch}</strong>,
target: <strong>{pullRequestMetadata.target_branch} </strong>,
time: <ReactTimeago date={pullRequestMetadata.merged as number} />
}}
/>
</Text>
</Container>
<FlexExpander />
</Layout.Horizontal>
</Container>
)
}

View File

@ -1,30 +0,0 @@
.main {
border: 2px solid var(--green-700);
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 5px;
background-color: var(--white);
&.merged {
border-color: transparent !important;
background: var(--purple-50) !important;
}
.btn {
background-color: var(--green-800) !important;
color: var(--white) !important;
}
.heading {
font-weight: 600 !important;
font-size: 16px !important;
line-height: 24px !important;
color: var(--grey-700) !important;
}
.sub {
font-weight: 500 !important;
font-size: 13px !important;
line-height: 20px !important;
color: var(--grey-500) !important;
}
}

View File

@ -1,96 +0,0 @@
import React from 'react'
import {
Button,
Color,
Container,
FlexExpander,
Icon,
Layout,
StringSubstitute,
Text,
useToaster
} from '@harness/uicore'
import { useMutate } from 'restful-react'
import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import type { TypesPullReq } from 'services/code'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, PullRequestFilterOption } from 'utils/GitUtils'
import { getErrorMessage } from 'utils/Utils'
import css from './PullRequestStatusInfo.module.scss'
interface PullRequestStatusInfoProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
onMerge: () => void
}
export const PullRequestStatusInfo: React.FC<PullRequestStatusInfoProps> = ({
repoMetadata,
pullRequestMetadata,
onMerge
}) => {
const { getString } = useStrings()
const { showError } = useToaster()
const { mutate: mergePR } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/merge`
})
if (pullRequestMetadata.state === PullRequestFilterOption.MERGED) {
return <MergeInfo pullRequestMetadata={pullRequestMetadata} />
}
return (
<Container className={css.main} padding="xlarge">
<Layout.Vertical spacing="xlarge">
<Container>
<Layout.Horizontal spacing="medium" flex={{ alignItems: 'center' }}>
<Icon name="tick-circle" size={28} color={Color.GREEN_700} />
<Container>
<Text className={css.heading}>{getString('pr.branchHasNoConflicts')}</Text>
<Text className={css.sub}>{getString('pr.prCanBeMerged')}</Text>
</Container>
<FlexExpander />
</Layout.Horizontal>
</Container>
<Container>
<Button
className={css.btn}
text={getString('pr.mergePR')}
onClick={() => {
mergePR({})
.then(onMerge)
.catch(exception => showError(getErrorMessage(exception)))
}}
/>
</Container>
</Layout.Vertical>
</Container>
)
}
const MergeInfo: React.FC<{ pullRequestMetadata: TypesPullReq }> = ({ pullRequestMetadata }) => {
const { getString } = useStrings()
return (
<Container className={cx(css.main, css.merged)} padding="xlarge">
<Layout.Horizontal spacing="medium" flex={{ alignItems: 'center' }}>
<Icon name={CodeIcon.PullRequest} size={28} color={Color.PURPLE_700} />
<Container>
<Text className={css.heading}>{getString('pr.prMerged')}</Text>
<Text className={css.sub}>
<StringSubstitute
str={getString('pr.prMergedInfo')}
vars={{
user: <strong>{pullRequestMetadata.merger?.display_name}</strong>,
source: <strong>{pullRequestMetadata.source_branch}</strong>,
target: <strong>{pullRequestMetadata.target_branch} </strong>,
time: <ReactTimeago date={pullRequestMetadata.merged as number} />
}}
/>
</Text>
</Container>
<FlexExpander />
</Layout.Horizontal>
</Container>
)
}

View File

@ -104,18 +104,30 @@ export default function PullRequest() {
tabList={[
{
id: PullRequestSection.CONVERSATION,
title: <TabTitle icon={CodeIcon.Chat} title={getString('conversation')} count={0} />,
title: (
<TabTitle
icon={CodeIcon.Chat}
title={getString('conversation')}
count={prData?.stats?.conversations || 0}
/>
),
panel: (
<Conversation
repoMetadata={repoMetadata as TypesRepository}
pullRequestMetadata={prData as TypesPullReq}
refreshPullRequestMetadata={voidFn(refetchPullRequest)}
onCommentUpdate={voidFn(refetchPullRequest)}
/>
)
},
{
id: PullRequestSection.COMMITS,
title: <TabTitle icon={CodeIcon.Commit} title={getString('commits')} count={0} />,
title: (
<TabTitle
icon={CodeIcon.Commit}
title={getString('commits')}
count={prData?.stats?.commits || 0}
/>
),
panel: (
<PullRequestCommits
repoMetadata={repoMetadata as TypesRepository}
@ -125,7 +137,13 @@ export default function PullRequest() {
},
{
id: PullRequestSection.FILES_CHANGED,
title: <TabTitle icon={CodeIcon.File} title={getString('filesChanged')} count={0} />,
title: (
<TabTitle
icon={CodeIcon.File}
title={getString('filesChanged')}
count={prData?.stats?.files_changed || 0}
/>
),
panel: (
<Container className={css.changes}>
<Changes
@ -135,6 +153,7 @@ export default function PullRequest() {
sourceBranch={prData?.source_branch}
emptyTitle={getString('noChanges')}
emptyMessage={getString('noChangesPR')}
onCommentUpdate={voidFn(refetchPullRequest)}
/>
</Container>
)

View File

@ -23,7 +23,7 @@ export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'p
refetch,
response
} = useGet<TypesCommit[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestMetadata.number}/commits`,
queryParams: {
limit,
page,

View File

@ -2,11 +2,11 @@ import React from 'react'
import { Container, Text, Layout, StringSubstitute } from '@harness/uicore'
import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import { GitInfoProps, PullRequestState } from 'utils/GitUtils'
import type { GitInfoProps } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { useStrings } from 'framework/strings'
import type { TypesPullReq } from 'services/code'
import { PRStateLabel } from 'components/PRStateLabel/PRStateLabel'
import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequestStateLabel'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
import css from './PullRequestMetaLine.module.scss'
@ -17,14 +17,16 @@ export const PullRequestMetaLine: React.FC<TypesPullReq & Pick<GitInfoProps, 're
source_branch,
author,
edited,
merged,
state
state,
is_draft,
stats
}) => {
const { getString } = useStrings()
const { routes } = useAppContext()
const vars = {
user: <strong>{author?.display_name}</strong>,
number: <strong>5</strong>, // TODO: No data from backend now
commits: <strong>{stats?.commits}</strong>,
commitsCount: stats?.commits,
target: (
<GitRefLink
text={target_branch as string}
@ -42,7 +44,7 @@ export const PullRequestMetaLine: React.FC<TypesPullReq & Pick<GitInfoProps, 're
return (
<Container padding={{ left: 'xlarge' }} className={css.main}>
<Layout.Horizontal spacing="small" className={css.layout}>
<PRStateLabel state={merged ? PullRequestState.MERGED : (state as PullRequestState)} />
<PullRequestStateLabel data={{ is_draft, state }} />
<Text className={css.metaline}>
<StringSubstitute str={getString('pr.metaLine')} vars={vars} />
</Text>

View File

@ -14,31 +14,8 @@
}
.titleRow {
padding-left: var(--spacing-medium);
padding-left: var(--spacing-small);
align-items: center;
}
}
}
.rowImg {
padding: 4px;
width: 28px;
height: 28px;
border-radius: 4px;
&.open {
background-color: var(--green-50);
}
&.merged {
background-color: var(--blue-50);
}
&.closed {
background-color: var(--grey-50);
}
&.draft {
background-color: var(--orange-100);
}
}

View File

@ -6,10 +6,5 @@ declare const styles: {
readonly row: string
readonly title: string
readonly titleRow: string
readonly rowImg: string
readonly open: string
readonly merged: string
readonly closed: string
readonly draft: string
}
export default styles

View File

@ -1,9 +1,9 @@
import React, { useMemo, useState } from 'react'
import { Container, PageBody, Text, Color, TableV2, Layout, StringSubstitute } from '@harness/uicore'
import cx from 'classnames'
import { useHistory } from 'react-router-dom'
import { useGet } from 'restful-react'
import type { CellProps, Column } from 'react-table'
import { Case, Match, Render, Truthy } from 'react-jsx-match'
import ReactTimeago from 'react-timeago'
import { makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
@ -12,15 +12,12 @@ import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { voidFn, getErrorMessage, LIST_FETCHING_LIMIT } from 'utils/Utils'
import { usePageIndex } from 'hooks/usePageIndex'
import type { TypesPullReq } from 'services/code'
import type { TypesPullReq, TypesRepository } from 'services/code'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequestStateLabel'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { PullRequestsContentHeader } from './PullRequestsContentHeader/PullRequestsContentHeader'
import prImgOpen from './pull-request-open.svg'
import prImgMerged from './pull-request-merged.svg'
import prImgClosed from './pull-request-closed.svg'
// import prImgDraft from './pull-request-draft.svg'
import css from './PullRequests.module.scss'
export default function PullRequests() {
@ -56,7 +53,7 @@ export default function PullRequests() {
Cell: ({ row }: CellProps<TypesPullReq>) => {
return (
<Layout.Horizontal className={css.titleRow} spacing="medium">
<img {...stateToImageProps(row.original)} />
<PullRequestStateLabel data={row.original} iconOnly />
<Container padding={{ left: 'small' }}>
<Layout.Vertical spacing="small">
<Text icon="success-tick" color={Color.GREY_800} className={css.title}>
@ -99,11 +96,11 @@ export default function PullRequests() {
<PageBody error={getErrorMessage(error || prError)} retryOnError={voidFn(refetch)}>
<LoadingSpinner visible={loading} />
{repoMetadata && (
<Render when={repoMetadata}>
<Layout.Vertical>
<PullRequestsContentHeader
loading={prLoading}
repoMetadata={repoMetadata}
repoMetadata={repoMetadata as TypesRepository}
onPullRequestFilterChanged={_filter => {
setFilter(_filter)
setPage(1)
@ -114,18 +111,19 @@ export default function PullRequests() {
}}
/>
<Container padding="xlarge">
{!!data?.length && (
<Match expr={data?.length}>
<Truthy>
<>
<TableV2<TypesPullReq>
className={css.table}
hideHeaders
columns={columns}
data={data}
data={data || []}
getRowClassName={() => css.row}
onRowClick={row => {
history.push(
routes.toCODEPullRequest({
repoPath: repoMetadata.path as string,
repoPath: repoMetadata?.path as string,
pullRequestId: String(row.number)
})
)
@ -133,9 +131,9 @@ export default function PullRequests() {
/>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</>
)}
</Truthy>
<Case val={0}>
<NoResultCard
showWhen={() => data?.length === 0}
forSearch={!!searchTerm}
message={getString('pullRequestEmpty')}
buttonText={getString('newPullRequest')}
@ -148,37 +146,12 @@ export default function PullRequests() {
)
}
/>
</Case>
</Match>
</Container>
</Layout.Vertical>
)}
</Render>
</PageBody>
</Container>
)
}
const stateToImageProps = (pr: TypesPullReq) => {
let src = prImgClosed
let clazz = css.open
switch (pr.state) {
case PullRequestFilterOption.OPEN:
src = prImgOpen
clazz = css.open
break
case PullRequestFilterOption.MERGED:
src = prImgMerged
clazz = css.merged
break
case PullRequestFilterOption.CLOSED:
src = prImgClosed
clazz = css.closed
break
// TODO: Not supported yet from backend
// case PullRequestFilterOption.DRAFT:
// src = prImgDraft
// clazz = css.draft
// break
}
return { src, title: pr.state, className: cx(css.rowImg, clazz) }
}

View File

@ -31,7 +31,7 @@ export function PullRequestsContentHeader({
{ label: getString('open'), value: PullRequestFilterOption.OPEN },
{ label: getString('merged'), value: PullRequestFilterOption.MERGED },
{ label: getString('closed'), value: PullRequestFilterOption.CLOSED },
{ label: getString('rejected'), value: PullRequestFilterOption.REJECTED },
{ label: getString('draft'), value: PullRequestFilterOption.DRAFT },
// { label: getString('yours'), value: PullRequestFilterOption.YOURS },
{ label: getString('all'), value: PullRequestFilterOption.ALL }
],

View File

@ -8,7 +8,7 @@ import { useAppContext } from 'AppContext'
import { CodeIcon } from 'utils/GitUtils'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { getErrorMessage } from 'utils/Utils'
import emptyStateImage from 'images/empty-state.svg'
import { Images } from 'images'
import hooks from './mockWebhooks.json'
import { SettingsContent } from './SettingsContent'
import css from './RepositorySettings.module.scss'
@ -74,7 +74,7 @@ export default function RepositorySettings() {
noData={{
when: () => repoMetadata !== null,
message: getString('noWebHooks'),
image: emptyStateImage,
image: Images.EmptyState,
button: NewWebHookButton
}}>
<Container className={css.contentContainer}>

View File

@ -346,6 +346,7 @@ export interface TypesPullReq {
source_branch?: string
source_repo_id?: number
state?: EnumPullReqState
stats?: TypesPullReqStats
target_branch?: string
target_repo_id?: number
title?: string
@ -371,6 +372,12 @@ export interface TypesPullReqActivity {
type?: EnumPullReqActivityType
}
export interface TypesPullReqStats {
commits?: number
conversations?: number
files_changed?: number
}
export type TypesPullRequestActivityPayloadComment = { [key: string]: any } | null
export interface TypesRepository {
@ -384,6 +391,7 @@ export interface TypesRepository {
is_public?: boolean
num_closed_pulls?: number
num_forks?: number
num_merged_pulls?: number
num_open_pulls?: number
num_pulls?: number
parent_id?: number
@ -1599,6 +1607,32 @@ export const useMergePullReqOp = ({ repo_ref, pullreq_number, ...props }: UseMer
{ base: getConfig('code'), pathParams: { repo_ref, pullreq_number }, ...props }
)
export interface PullReqMetaDataPathParams {
repo_ref: string
pullreq_number: number
}
export type PullReqMetaDataProps = Omit<GetProps<void, UsererrorError, void, PullReqMetaDataPathParams>, 'path'> &
PullReqMetaDataPathParams
export const PullReqMetaData = ({ repo_ref, pullreq_number, ...props }: PullReqMetaDataProps) => (
<Get<void, UsererrorError, void, PullReqMetaDataPathParams>
path={`/repos/${repo_ref}/pullreq/${pullreq_number}/metadata`}
base={getConfig('code')}
{...props}
/>
)
export type UsePullReqMetaDataProps = Omit<UseGetProps<void, UsererrorError, void, PullReqMetaDataPathParams>, 'path'> &
PullReqMetaDataPathParams
export const usePullReqMetaData = ({ repo_ref, pullreq_number, ...props }: UsePullReqMetaDataProps) =>
useGet<void, UsererrorError, void, PullReqMetaDataPathParams>(
(paramsInPath: PullReqMetaDataPathParams) =>
`/repos/${paramsInPath.repo_ref}/pullreq/${paramsInPath.pullreq_number}/metadata`,
{ base: getConfig('code'), pathParams: { repo_ref, pullreq_number }, ...props }
)
export interface ReviewerListPullReqPathParams {
repo_ref: string
pullreq_number: number

View File

@ -1624,6 +1624,53 @@ paths:
description: Unprocessable Entity
tags:
- pullreq
/repos/{repo_ref}/pullreq/{pullreq_number}/metadata:
get:
operationId: pullReqMetaData
parameters:
- in: path
name: repo_ref
required: true
schema:
type: string
- in: path
name: pullreq_number
required: true
schema:
type: integer
responses:
'200':
content:
text/plain:
schema:
type: string
description: OK
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Unauthorized
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Forbidden
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Not Found
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Internal Server Error
tags:
- pullreq
/repos/{repo_ref}/pullreq/{pullreq_number}/reviewers:
get:
operationId: reviewerListPullReq
@ -3834,6 +3881,8 @@ components:
type: integer
state:
$ref: '#/components/schemas/EnumPullReqState'
stats:
$ref: '#/components/schemas/TypesPullReqStats'
target_branch:
type: string
target_repo_id:
@ -3882,6 +3931,15 @@ components:
type:
$ref: '#/components/schemas/EnumPullReqActivityType'
type: object
TypesPullReqStats:
properties:
commits:
type: integer
conversations:
type: integer
files_changed:
type: integer
type: object
TypesPullRequestActivityPayloadComment:
additionalProperties: {}
nullable: true
@ -3908,6 +3966,8 @@ components:
type: integer
num_forks:
type: integer
num_merged_pulls:
type: integer
num_open_pulls:
type: integer
num_pulls:

View File

@ -51,13 +51,13 @@ export enum GitCommitAction {
export enum PullRequestState {
OPEN = 'open',
MERGED = 'merged',
CLOSED = 'closed',
REJECTED = 'rejected',
DRAFT = 'draft'
CLOSED = 'closed'
}
export const PullRequestFilterOption = {
...PullRequestState,
// REJECTED: 'rejected',
DRAFT: 'draft',
YOURS: 'yours',
ALL: 'all'
}
@ -65,6 +65,8 @@ export const PullRequestFilterOption = {
export const CodeIcon = {
Logo: 'code' as IconName,
PullRequest: 'git-pull' as IconName,
Merged: 'code-merged' as IconName,
Draft: 'code-draft' as IconName,
PullRequestRejected: 'main-close' as IconName,
Add: 'plus' as IconName,
BranchSmall: 'code-branch-small' as IconName,

View File

@ -661,10 +661,10 @@
resolved "https://npm.pkg.github.com/download/@harness/design-system/1.4.0/b2a77f73696d71a53765c71efd0a5b28039fa1cf#b2a77f73696d71a53765c71efd0a5b28039fa1cf"
integrity sha512-LuzuPEHPkE6xgIuXxn16RCCvPY1NDXF3o1JWlIjxmepoDTkgFuwnV1OhBdQftvAVBawJ5wJP10IIKUL161LdYg==
"@harness/icons@1.95.1":
version "1.95.1"
resolved "https://npm.pkg.github.com/download/@harness/icons/1.95.1/53de6061dfaa8c8b1ef8aa6271f65456a84bfe46#53de6061dfaa8c8b1ef8aa6271f65456a84bfe46"
integrity sha512-RZ9OdWLUcVrKR4FJvrJa2ewgZPsK8vk550ZPYC4F3ZRxkN5M8B1HNtrDmmAgJzgO2EeP6+9Xzj8LcbeOWQPlMA==
"@harness/icons@1.101.1":
version "1.101.1"
resolved "https://npm.pkg.github.com/download/@harness/icons/1.101.1/b15024459bc229e20ca6ba48f86d038a576173c9#b15024459bc229e20ca6ba48f86d038a576173c9"
integrity sha512-IMYSDpWT/Hi8XlkM+XjpquJlaCLAVH7aWzbfxkJl/rraD0btym72/08lVM5xTLRlwLgQt9Y3TAL2f0JvWGU0NA==
"@harness/jarvis@^0.12.0":
version "0.12.0"
@ -695,10 +695,10 @@
resolved "https://npm.pkg.github.com/download/@harness/telemetry/1.0.44/55e75d8caccbcdcb0a11226c813edd631578d9af#55e75d8caccbcdcb0a11226c813edd631578d9af"
integrity sha512-t6N3Ie/F9Nw/tANAmptsunebGYBkC3j865bc75MZVL2ZqFM0CBRxFR1MG8zC+hU6uDpr8Drqsn81NmdlVlBSmA==
"@harness/uicore@3.95.1":
version "3.95.1"
resolved "https://npm.pkg.github.com/download/@harness/uicore/3.95.1/4b9551e0299ed56e6f4291e7e75cb48562aa7dd8#4b9551e0299ed56e6f4291e7e75cb48562aa7dd8"
integrity sha512-NSxVyQ5aGLF+3kysijjEeqFPQ4HHlWdYRq9HRKj8lRoT0YLYyvxSxZbGae/Ceq74SutnDk6h36qEK2L6lu8Cfw==
"@harness/uicore@3.106.3":
version "3.106.3"
resolved "https://npm.pkg.github.com/download/@harness/uicore/3.106.3/64b3c1cfb645a3eaaa2fe5ea481bd96a912de468#64b3c1cfb645a3eaaa2fe5ea481bd96a912de468"
integrity sha512-LHDMbhdHHklJCKPj/n9EZBPNiWBPMHacQoInuTcSEswiXdQDThlozJ/3ccrsUk4Qbak16LV4xpDkDYCwolo5Pg==
"@harness/use-modal@1.3.0":
version "1.3.0"