mirror of
https://github.com/harness/drone.git
synced 2025-05-17 01:20:13 +08:00
feat: Allow empty PR description
This commit is contained in:
parent
674dcc11d2
commit
404631a22d
@ -27,7 +27,7 @@ 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'> {
|
||||||
commits: TypesCommit[]
|
commits: TypesCommit[] | null
|
||||||
emptyTitle: string
|
emptyTitle: string
|
||||||
emptyMessage: string
|
emptyMessage: string
|
||||||
prHasChanged?: boolean
|
prHasChanged?: boolean
|
||||||
@ -116,7 +116,7 @@ export function CommitsView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
{!!commits.length &&
|
{!!commits?.length &&
|
||||||
Object.entries(commitsGroupedByDate).map(([date, commitsByDate]) => {
|
Object.entries(commitsGroupedByDate).map(([date, commitsByDate]) => {
|
||||||
return (
|
return (
|
||||||
<ThreadSection
|
<ThreadSection
|
||||||
|
@ -8,7 +8,11 @@ export enum UserPreference {
|
|||||||
PULL_REQUEST_CREATION_OPTION = 'PULL_REQUEST_CREATION_OPTION'
|
PULL_REQUEST_CREATION_OPTION = 'PULL_REQUEST_CREATION_OPTION'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserPreference<T = string>(key: UserPreference, defaultValue: T): [T, (val: T) => void] {
|
export function useUserPreference<T = string>(
|
||||||
|
key: UserPreference,
|
||||||
|
defaultValue: T,
|
||||||
|
filter: (val: T) => boolean = () => true
|
||||||
|
): [T, (val: T) => void] {
|
||||||
const prefKey = `CODE_MOD_USER_PREF__${key}`
|
const prefKey = `CODE_MOD_USER_PREF__${key}`
|
||||||
const convert = useCallback(
|
const convert = useCallback(
|
||||||
val => {
|
val => {
|
||||||
@ -40,14 +44,16 @@ export function useUserPreference<T = string>(key: UserPreference, defaultValue:
|
|||||||
const savePreference = useCallback(
|
const savePreference = useCallback(
|
||||||
(val: T) => {
|
(val: T) => {
|
||||||
try {
|
try {
|
||||||
|
if (filter(val)) {
|
||||||
localStorage[prefKey] = Array.isArray(val) || typeof val === 'object' ? JSON.stringify(val) : val
|
localStorage[prefKey] = Array.isArray(val) || typeof val === 'object' ? JSON.stringify(val) : val
|
||||||
|
}
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('useUserPreference: Failed to stringify object', val)
|
console.error('useUserPreference: Failed to stringify object', val)
|
||||||
}
|
}
|
||||||
setPreference(val)
|
setPreference(val)
|
||||||
},
|
},
|
||||||
[prefKey]
|
[prefKey, filter]
|
||||||
)
|
)
|
||||||
|
|
||||||
return [preference, savePreference]
|
return [preference, savePreference]
|
||||||
|
@ -85,16 +85,12 @@ export default function Compare() {
|
|||||||
return showToaster(getString('pr.titleIsRequired'))
|
return showToaster(getString('pr.titleIsRequired'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description) {
|
|
||||||
return showToaster(getString('pr.descIsRequired'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const pullReqUrl = window.location.href.split('compare')?.[0]
|
const pullReqUrl = window.location.href.split('compare')?.[0]
|
||||||
const payload: OpenapiCreatePullReqRequest = {
|
const payload: OpenapiCreatePullReqRequest = {
|
||||||
target_branch: targetGitRef,
|
target_branch: targetGitRef,
|
||||||
source_branch: sourceGitRef,
|
source_branch: sourceGitRef,
|
||||||
title: title,
|
title: title,
|
||||||
description: description,
|
description: description || '',
|
||||||
is_draft: creationType === PRCreationType.DRAFT
|
is_draft: creationType === PRCreationType.DRAFT
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +239,7 @@ export default function Compare() {
|
|||||||
</Container>
|
</Container>
|
||||||
<Container className={css.markdownContainer}>
|
<Container className={css.markdownContainer}>
|
||||||
<Layout.Vertical spacing="small">
|
<Layout.Vertical spacing="small">
|
||||||
<Text font={{ variation: FontVariation.SMALL_BOLD }}>{getString('description')} *</Text>
|
<Text font={{ variation: FontVariation.SMALL_BOLD }}>{getString('description')}</Text>
|
||||||
<MarkdownEditorWithPreview
|
<MarkdownEditorWithPreview
|
||||||
value={description}
|
value={description}
|
||||||
onChange={setDescription}
|
onChange={setDescription}
|
||||||
|
@ -27,13 +27,17 @@ import css from './Conversation.module.scss'
|
|||||||
export interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
export interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
||||||
onCommentUpdate: () => void
|
onCommentUpdate: () => void
|
||||||
prHasChanged?: boolean
|
prHasChanged?: boolean
|
||||||
|
showEditDescription?: boolean
|
||||||
|
onCancelEditDescription: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Conversation: React.FC<ConversationProps> = ({
|
export const Conversation: React.FC<ConversationProps> = ({
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
pullRequestMetadata,
|
pullRequestMetadata,
|
||||||
onCommentUpdate,
|
onCommentUpdate,
|
||||||
prHasChanged
|
prHasChanged,
|
||||||
|
showEditDescription,
|
||||||
|
onCancelEditDescription
|
||||||
}) => {
|
}) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { currentUser } = useAppContext()
|
const { currentUser } = useAppContext()
|
||||||
@ -116,6 +120,10 @@ export const Conversation: React.FC<ConversationProps> = ({
|
|||||||
onCommentUpdate()
|
onCommentUpdate()
|
||||||
refetchActivities()
|
refetchActivities()
|
||||||
}, [onCommentUpdate, refetchActivities])
|
}, [onCommentUpdate, refetchActivities])
|
||||||
|
const hasDescription = useMemo(
|
||||||
|
() => !!pullRequestMetadata?.description?.length,
|
||||||
|
[pullRequestMetadata?.description?.length]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prHasChanged) {
|
if (prHasChanged) {
|
||||||
@ -136,12 +144,17 @@ export const Conversation: React.FC<ConversationProps> = ({
|
|||||||
<Layout.Horizontal>
|
<Layout.Horizontal>
|
||||||
<Container width={`70%`}>
|
<Container width={`70%`}>
|
||||||
<Layout.Vertical spacing="xlarge">
|
<Layout.Vertical spacing="xlarge">
|
||||||
|
{(hasDescription || showEditDescription) && (
|
||||||
<DescriptionBox
|
<DescriptionBox
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
pullRequestMetadata={pullRequestMetadata}
|
pullRequestMetadata={pullRequestMetadata}
|
||||||
onCommentUpdate={onCommentUpdate}
|
onCommentUpdate={onCommentUpdate}
|
||||||
|
onCancelEditDescription={onCancelEditDescription}
|
||||||
/>
|
/>
|
||||||
<Layout.Horizontal className={css.sortContainer} padding={{ top: 'xxlarge', bottom: 'medium' }}>
|
)}
|
||||||
|
<Layout.Horizontal
|
||||||
|
className={css.sortContainer}
|
||||||
|
padding={{ top: hasDescription || showEditDescription ? 'xxlarge' : undefined, bottom: 'medium' }}>
|
||||||
<Container>
|
<Container>
|
||||||
<Select
|
<Select
|
||||||
items={activityFilters}
|
items={activityFilters}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Container, useToaster } from '@harness/uicore'
|
import { Container, useToaster } from '@harness/uicore'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { useMutate } from 'restful-react'
|
import { useMutate } from 'restful-react'
|
||||||
@ -12,10 +12,15 @@ import { getErrorMessage } from 'utils/Utils'
|
|||||||
import type { ConversationProps } from './Conversation'
|
import type { ConversationProps } from './Conversation'
|
||||||
import css from './Conversation.module.scss'
|
import css from './Conversation.module.scss'
|
||||||
|
|
||||||
export const DescriptionBox: React.FC<ConversationProps> = ({
|
interface DescriptionBoxProps extends Omit<ConversationProps, 'onCancelEditDescription'> {
|
||||||
|
onCancelEditDescription: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
pullRequestMetadata,
|
pullRequestMetadata,
|
||||||
onCommentUpdate: refreshPullRequestMetadata
|
onCommentUpdate: refreshPullRequestMetadata,
|
||||||
|
onCancelEditDescription
|
||||||
}) => {
|
}) => {
|
||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
const [dirty, setDirty] = useState(false)
|
const [dirty, setDirty] = useState(false)
|
||||||
@ -28,6 +33,10 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
|||||||
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}`
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEdit(!pullRequestMetadata?.description?.length)
|
||||||
|
}, [pullRequestMetadata?.description?.length])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
|
<Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
|
||||||
<Container padding={!edit ? { left: 'small', bottom: 'small' } : undefined}>
|
<Container padding={!edit ? { left: 'small', bottom: 'small' } : undefined}>
|
||||||
@ -37,14 +46,13 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
|||||||
onSave={value => {
|
onSave={value => {
|
||||||
const payload: OpenapiUpdatePullReqRequest = {
|
const payload: OpenapiUpdatePullReqRequest = {
|
||||||
title: pullRequestMetadata.title,
|
title: pullRequestMetadata.title,
|
||||||
description: value
|
description: value || ''
|
||||||
}
|
}
|
||||||
mutate(payload)
|
mutate(payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setContent(value)
|
setContent(value)
|
||||||
setOriginalContent(value)
|
setOriginalContent(value)
|
||||||
setEdit(false)
|
setEdit(false)
|
||||||
// setUpdated(Date.now())
|
|
||||||
refreshPullRequestMetadata()
|
refreshPullRequestMetadata()
|
||||||
})
|
})
|
||||||
.catch(exception => showError(getErrorMessage(exception), 0, getString('pr.failedToUpdate')))
|
.catch(exception => showError(getErrorMessage(exception), 0, getString('pr.failedToUpdate')))
|
||||||
@ -52,6 +60,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setContent(originalContent)
|
setContent(originalContent)
|
||||||
setEdit(false)
|
setEdit(false)
|
||||||
|
onCancelEditDescription()
|
||||||
}}
|
}}
|
||||||
setDirty={setDirty}
|
setDirty={setDirty}
|
||||||
i18n={{
|
i18n={{
|
||||||
@ -62,6 +71,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
|||||||
cancel: getString('cancel')
|
cancel: getString('cancel')
|
||||||
}}
|
}}
|
||||||
editorHeight="400px"
|
editorHeight="400px"
|
||||||
|
autoFocusAndPositioning
|
||||||
/>
|
/>
|
||||||
)) || (
|
)) || (
|
||||||
<Container className={css.mdWrapper}>
|
<Container className={css.mdWrapper}>
|
||||||
|
@ -133,7 +133,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
|||||||
|
|
||||||
const [mergeOption, setMergeOption] = useUserPreference<PRMergeOption>(
|
const [mergeOption, setMergeOption] = useUserPreference<PRMergeOption>(
|
||||||
UserPreference.PULL_REQUEST_MERGE_STRATEGY,
|
UserPreference.PULL_REQUEST_MERGE_STRATEGY,
|
||||||
mergeOptions[1]
|
mergeOptions[1],
|
||||||
|
option => option.method !== 'close'
|
||||||
)
|
)
|
||||||
const [draftOption, setDraftOption] = useState<PRDraftOption>(draftOptions[0])
|
const [draftOption, setDraftOption] = useState<PRDraftOption>(draftOptions[0])
|
||||||
const permPushResult = hooks?.usePermissionTranslate?.(
|
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||||
|
@ -1,26 +1,14 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import { Container, PageBody, Tabs } from '@harness/uicore'
|
||||||
Container,
|
import { useGet } from 'restful-react'
|
||||||
PageBody,
|
import { Render } from 'react-jsx-match'
|
||||||
Text,
|
|
||||||
FontVariation,
|
|
||||||
Tabs,
|
|
||||||
Layout,
|
|
||||||
Button,
|
|
||||||
ButtonVariation,
|
|
||||||
ButtonSize,
|
|
||||||
TextInput,
|
|
||||||
useToaster
|
|
||||||
} from '@harness/uicore'
|
|
||||||
import { useGet, useMutate } from 'restful-react'
|
|
||||||
import { Render, Match, Truthy, Else } from 'react-jsx-match'
|
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
import { useAppContext } from 'AppContext'
|
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 } from 'utils/Utils'
|
import { voidFn, getErrorMessage } from 'utils/Utils'
|
||||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
import { CodeIcon } 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'
|
||||||
import { TabTitleWithCount, tabContainerCSS } from 'components/TabTitleWithCount/TabTitleWithCount'
|
import { TabTitleWithCount, tabContainerCSS } from 'components/TabTitleWithCount/TabTitleWithCount'
|
||||||
@ -29,6 +17,7 @@ import { Conversation } from './Conversation/Conversation'
|
|||||||
import { Checks } from './Checks/Checks'
|
import { Checks } from './Checks/Checks'
|
||||||
import { Changes } from '../../components/Changes/Changes'
|
import { Changes } from '../../components/Changes/Changes'
|
||||||
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
||||||
|
import { PullRequestTitle } from './PullRequestTitle'
|
||||||
import css from './PullRequest.module.scss'
|
import css from './PullRequest.module.scss'
|
||||||
|
|
||||||
export default function PullRequest() {
|
export default function PullRequest() {
|
||||||
@ -61,6 +50,7 @@ export default function PullRequest() {
|
|||||||
return loading || (prLoading && !prData)
|
return loading || (prLoading && !prData)
|
||||||
}, [loading, prLoading, prData])
|
}, [loading, prLoading, prData])
|
||||||
const [stats, setStats] = useState<TypesPullReqStats>()
|
const [stats, setStats] = useState<TypesPullReqStats>()
|
||||||
|
const [showEditDescription, setShowEditDescription] = useState(false)
|
||||||
const prHasChanged = useMemo(() => {
|
const prHasChanged = useMemo(() => {
|
||||||
if (stats && prData?.stats) {
|
if (stats && prData?.stats) {
|
||||||
if (
|
if (
|
||||||
@ -74,6 +64,9 @@ export default function PullRequest() {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, [prData?.stats, stats])
|
}, [prData?.stats, stats])
|
||||||
|
const onAddDescriptionClick = useCallback(() => {
|
||||||
|
setShowEditDescription(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function setStatsIfNotSet() {
|
function setStatsIfNotSet() {
|
||||||
@ -120,7 +113,13 @@ export default function PullRequest() {
|
|||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<RepositoryPageHeader
|
<RepositoryPageHeader
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
title={repoMetadata && prData ? <PullRequestTitle repoMetadata={repoMetadata} {...prData} /> : ''}
|
title={
|
||||||
|
repoMetadata && prData ? (
|
||||||
|
<PullRequestTitle repoMetadata={repoMetadata} {...prData} onAddDescriptionClick={onAddDescriptionClick} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
dataTooltipId="repositoryPullRequests"
|
dataTooltipId="repositoryPullRequests"
|
||||||
extraBreadcrumbLinks={
|
extraBreadcrumbLinks={
|
||||||
repoMetadata && [
|
repoMetadata && [
|
||||||
@ -165,8 +164,13 @@ export default function PullRequest() {
|
|||||||
<Conversation
|
<Conversation
|
||||||
repoMetadata={repoMetadata as TypesRepository}
|
repoMetadata={repoMetadata as TypesRepository}
|
||||||
pullRequestMetadata={prData as TypesPullReq}
|
pullRequestMetadata={prData as TypesPullReq}
|
||||||
onCommentUpdate={voidFn(refetchPullRequest)}
|
onCommentUpdate={() => {
|
||||||
|
setShowEditDescription(false)
|
||||||
|
refetchPullRequest()
|
||||||
|
}}
|
||||||
prHasChanged={prHasChanged}
|
prHasChanged={prHasChanged}
|
||||||
|
showEditDescription={showEditDescription}
|
||||||
|
onCancelEditDescription={() => setShowEditDescription(false)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -237,92 +241,6 @@ export default function PullRequest() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PullRequestTitleProps extends TypesPullReq, Pick<GitInfoProps, 'repoMetadata'> {
|
|
||||||
onSaveDone?: (newTitle: string) => Promise<boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
const PullRequestTitle: React.FC<PullRequestTitleProps> = ({ repoMetadata, title, number, description }) => {
|
|
||||||
const [original, setOriginal] = useState(title)
|
|
||||||
const [val, setVal] = useState(title)
|
|
||||||
const [edit, setEdit] = useState(false)
|
|
||||||
const { getString } = useStrings()
|
|
||||||
const { showError } = useToaster()
|
|
||||||
const { mutate } = useMutate({
|
|
||||||
verb: 'PATCH',
|
|
||||||
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${number}`
|
|
||||||
})
|
|
||||||
const submitChange = useCallback(() => {
|
|
||||||
mutate({
|
|
||||||
title: val,
|
|
||||||
description
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setEdit(false)
|
|
||||||
setOriginal(val)
|
|
||||||
})
|
|
||||||
.catch(exception => showError(getErrorMessage(exception), 0))
|
|
||||||
}, [description, val, mutate, showError])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout.Horizontal spacing="xsmall" className={css.prTitle}>
|
|
||||||
<Match expr={edit}>
|
|
||||||
<Truthy>
|
|
||||||
<Container>
|
|
||||||
<Layout.Horizontal spacing="small">
|
|
||||||
<TextInput
|
|
||||||
wrapperClassName={css.input}
|
|
||||||
value={val}
|
|
||||||
onFocus={event => event.target.select()}
|
|
||||||
onInput={event => setVal(event.currentTarget.value)}
|
|
||||||
autoFocus
|
|
||||||
onKeyDown={event => {
|
|
||||||
switch (event.key) {
|
|
||||||
case 'Enter':
|
|
||||||
submitChange()
|
|
||||||
break
|
|
||||||
case 'Escape': // does not work, maybe TextInput cancels ESC?
|
|
||||||
setEdit(false)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variation={ButtonVariation.PRIMARY}
|
|
||||||
text={getString('save')}
|
|
||||||
size={ButtonSize.MEDIUM}
|
|
||||||
disabled={(val || '').trim().length === 0 || title === val}
|
|
||||||
onClick={submitChange}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variation={ButtonVariation.TERTIARY}
|
|
||||||
text={getString('cancel')}
|
|
||||||
size={ButtonSize.MEDIUM}
|
|
||||||
onClick={() => setEdit(false)}
|
|
||||||
/>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
</Container>
|
|
||||||
</Truthy>
|
|
||||||
<Else>
|
|
||||||
<>
|
|
||||||
<Text tag="h1" font={{ variation: FontVariation.H4 }}>
|
|
||||||
{original} <span className={css.prNumber}>#{number}</span>
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
variation={ButtonVariation.ICON}
|
|
||||||
tooltip={getString('edit')}
|
|
||||||
tooltipProps={{ isDark: true, position: 'right' }}
|
|
||||||
size={ButtonSize.SMALL}
|
|
||||||
icon="code-edit"
|
|
||||||
className={css.btn}
|
|
||||||
onClick={() => setEdit(true)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</Else>
|
|
||||||
</Match>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PullRequestSection {
|
enum PullRequestSection {
|
||||||
CONVERSATION = 'conversation',
|
CONVERSATION = 'conversation',
|
||||||
COMMITS = 'commits',
|
COMMITS = 'commits',
|
||||||
|
@ -43,7 +43,7 @@ export const PullRequestCommits: React.FC<CommitProps> = ({
|
|||||||
return (
|
return (
|
||||||
<PullRequestTabContentWrapper loading={loading} error={error} onRetry={voidFn(refetch)}>
|
<PullRequestTabContentWrapper loading={loading} error={error} onRetry={voidFn(refetch)}>
|
||||||
<CommitsView
|
<CommitsView
|
||||||
commits={commits || []}
|
commits={commits}
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
emptyTitle={getString('noCommits')}
|
emptyTitle={getString('noCommits')}
|
||||||
emptyMessage={getString('noCommitsPR')}
|
emptyMessage={getString('noCommitsPR')}
|
||||||
|
121
web/src/pages/PullRequest/PullRequestTitle.tsx
Normal file
121
web/src/pages/PullRequest/PullRequestTitle.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Text,
|
||||||
|
FontVariation,
|
||||||
|
Layout,
|
||||||
|
Button,
|
||||||
|
ButtonVariation,
|
||||||
|
ButtonSize,
|
||||||
|
TextInput,
|
||||||
|
useToaster
|
||||||
|
} from '@harness/uicore'
|
||||||
|
import { useMutate } from 'restful-react'
|
||||||
|
import { Match, Truthy, Else } from 'react-jsx-match'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { ButtonRoleProps, getErrorMessage } from 'utils/Utils'
|
||||||
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
|
import type { TypesPullReq } from 'services/code'
|
||||||
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
|
import css from './PullRequest.module.scss'
|
||||||
|
|
||||||
|
interface PullRequestTitleProps extends TypesPullReq, Pick<GitInfoProps, 'repoMetadata'> {
|
||||||
|
onSaveDone?: (newTitle: string) => Promise<boolean>
|
||||||
|
onAddDescriptionClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PullRequestTitle: React.FC<PullRequestTitleProps> = ({
|
||||||
|
repoMetadata,
|
||||||
|
title,
|
||||||
|
number,
|
||||||
|
description,
|
||||||
|
onAddDescriptionClick
|
||||||
|
}) => {
|
||||||
|
const [original, setOriginal] = useState(title)
|
||||||
|
const [val, setVal] = useState(title)
|
||||||
|
const [edit, setEdit] = useState(false)
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { showError } = useToaster()
|
||||||
|
const { mutate } = useMutate({
|
||||||
|
verb: 'PATCH',
|
||||||
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${number}`
|
||||||
|
})
|
||||||
|
const submitChange = useCallback(() => {
|
||||||
|
mutate({
|
||||||
|
title: val,
|
||||||
|
description
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setEdit(false)
|
||||||
|
setOriginal(val)
|
||||||
|
})
|
||||||
|
.catch(exception => showError(getErrorMessage(exception), 0))
|
||||||
|
}, [description, val, mutate, showError])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Horizontal spacing="small" className={css.prTitle}>
|
||||||
|
<Match expr={edit}>
|
||||||
|
<Truthy>
|
||||||
|
<Container>
|
||||||
|
<Layout.Horizontal spacing="small">
|
||||||
|
<TextInput
|
||||||
|
wrapperClassName={css.input}
|
||||||
|
value={val}
|
||||||
|
onFocus={event => event.target.select()}
|
||||||
|
onInput={event => setVal(event.currentTarget.value)}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={event => {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
submitChange()
|
||||||
|
break
|
||||||
|
case 'Escape': // does not work, maybe TextInput cancels ESC?
|
||||||
|
setEdit(false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.PRIMARY}
|
||||||
|
text={getString('save')}
|
||||||
|
size={ButtonSize.MEDIUM}
|
||||||
|
disabled={(val || '').trim().length === 0 || title === val}
|
||||||
|
onClick={submitChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.TERTIARY}
|
||||||
|
text={getString('cancel')}
|
||||||
|
size={ButtonSize.MEDIUM}
|
||||||
|
onClick={() => setEdit(false)}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
</Truthy>
|
||||||
|
<Else>
|
||||||
|
<>
|
||||||
|
<Text tag="h1" font={{ variation: FontVariation.H4 }}>
|
||||||
|
{original} <span className={css.prNumber}>#{number}</span>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.ICON}
|
||||||
|
tooltip={getString('edit')}
|
||||||
|
tooltipProps={{ isDark: true, position: 'right' }}
|
||||||
|
size={ButtonSize.SMALL}
|
||||||
|
icon="code-edit"
|
||||||
|
className={css.btn}
|
||||||
|
onClick={() => setEdit(true)}
|
||||||
|
/>
|
||||||
|
{!(description || '').trim().length && (
|
||||||
|
<>
|
||||||
|
<PipeSeparator height={10} />
|
||||||
|
<a {...ButtonRoleProps} onClick={onAddDescriptionClick}>
|
||||||
|
Add Description
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Else>
|
||||||
|
</Match>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
)
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.convoIcon {
|
.convoIcon {
|
||||||
padding-top: 1px !important;
|
padding-top: 1px !important;
|
||||||
}
|
}
|
||||||
@ -20,7 +21,7 @@
|
|||||||
|
|
||||||
.titleRow {
|
.titleRow {
|
||||||
padding-left: var(--spacing-small);
|
padding-left: var(--spacing-small);
|
||||||
align-items: start;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ import {
|
|||||||
StringSubstitute,
|
StringSubstitute,
|
||||||
Icon,
|
Icon,
|
||||||
FontVariation,
|
FontVariation,
|
||||||
FlexExpander
|
FlexExpander,
|
||||||
|
Utils
|
||||||
} from '@harness/uicore'
|
} from '@harness/uicore'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
@ -30,6 +31,8 @@ import type { TypesPullReq, TypesRepository } from 'services/code'
|
|||||||
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
|
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
|
||||||
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
||||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||||
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
|
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
|
||||||
import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequestStateLabel'
|
import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequestStateLabel'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
import { ExecutionStatusLabel } from 'components/ExecutionStatusLabel/ExecutionStatusLabel'
|
import { ExecutionStatusLabel } from 'components/ExecutionStatusLabel/ExecutionStatusLabel'
|
||||||
@ -97,7 +100,7 @@ export default function PullRequests() {
|
|||||||
<Layout.Horizontal className={css.titleRow} spacing="medium">
|
<Layout.Horizontal className={css.titleRow} spacing="medium">
|
||||||
<PullRequestStateLabel iconSize={22} data={row.original} iconOnly />
|
<PullRequestStateLabel iconSize={22} data={row.original} iconOnly />
|
||||||
<Container padding={{ left: 'small' }}>
|
<Container padding={{ left: 'small' }}>
|
||||||
<Layout.Vertical spacing="small">
|
<Layout.Vertical spacing="xsmall">
|
||||||
<Text color={Color.GREY_800} className={css.title}>
|
<Text color={Color.GREY_800} className={css.title}>
|
||||||
{row.original.title}
|
{row.original.title}
|
||||||
<Icon
|
<Icon
|
||||||
@ -110,6 +113,8 @@ export default function PullRequests() {
|
|||||||
{row.original.stats?.conversations}
|
{row.original.stats?.conversations}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
<Container>
|
||||||
|
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
|
||||||
<Text color={Color.GREY_500} font={{ size: 'small' }}>
|
<Text color={Color.GREY_500} font={{ size: 'small' }}>
|
||||||
<StringSubstitute
|
<StringSubstitute
|
||||||
str={getString('pr.statusLine')}
|
str={getString('pr.statusLine')}
|
||||||
@ -120,7 +125,9 @@ export default function PullRequests() {
|
|||||||
<strong>
|
<strong>
|
||||||
<ReactTimeago
|
<ReactTimeago
|
||||||
date={
|
date={
|
||||||
(row.original.state == 'merged' ? row.original.merged : row.original.created) as number
|
(row.original.state == 'merged'
|
||||||
|
? row.original.merged
|
||||||
|
: row.original.created) as number
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</strong>
|
</strong>
|
||||||
@ -129,10 +136,32 @@ export default function PullRequests() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
|
<PipeSeparator height={10} />
|
||||||
|
<Container>
|
||||||
|
<Layout.Horizontal spacing="xsmall" style={{ alignItems: 'center' }} onClick={Utils.stopEvent}>
|
||||||
|
<GitRefLink
|
||||||
|
text={row.original.target_branch as string}
|
||||||
|
url={routes.toCODERepository({
|
||||||
|
repoPath: repoMetadata?.path as string,
|
||||||
|
gitRef: row.original.target_branch
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Text color={Color.GREY_500}>←</Text>
|
||||||
|
<GitRefLink
|
||||||
|
text={row.original.source_branch as string}
|
||||||
|
url={routes.toCODERepository({
|
||||||
|
repoPath: repoMetadata?.path as string,
|
||||||
|
gitRef: row.original.source_branch
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
</Container>
|
</Container>
|
||||||
<FlexExpander />
|
<FlexExpander />
|
||||||
{/* fix state when api is fully implemented */}
|
{/* TODO: Pass proper state when check api is fully implemented */}
|
||||||
<ExecutionStatusLabel data={{ state: 'success' }} />
|
<ExecutionStatusLabel data={{ state: 'success' }} />
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
)
|
)
|
||||||
|
@ -115,7 +115,7 @@ export function formatNumber(num: number | bigint): string {
|
|||||||
* support (hit Enter/Space will trigger click event)
|
* support (hit Enter/Space will trigger click event)
|
||||||
*/
|
*/
|
||||||
export const ButtonRoleProps = {
|
export const ButtonRoleProps = {
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar' || e.which === 13 || e.which === 32) {
|
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar' || e.which === 13 || e.which === 32) {
|
||||||
;(e.target as unknown as { click: () => void })?.click?.()
|
;(e.target as unknown as { click: () => void })?.click?.()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user