diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 349bdec69..ef2a4d64b 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -879,10 +879,13 @@ export interface StringsMap { 'pr.statusLine': string 'pr.suggestedChange': string 'pr.suggestionApplied': string + 'pr.targetBranchUpdated': string + 'pr.titleAndBranchUpdated': string 'pr.titleChanged': string 'pr.titleChangedTable': string 'pr.titleIsRequired': string 'pr.titlePlaceHolder': string + 'pr.titleUpdated': string 'pr.toggleComments': string 'pr.unified': string 'pr.updatedLine': string @@ -908,6 +911,7 @@ export interface StringsMap { 'prReview.requested': string 'prReview.selfAssigned': string 'prReview.selfRemoved': string + 'prReview.targetBranchChange': string prSourceAndTargetMustBeDifferent: string 'prState.draftDesc': string 'prState.draftHeading': string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index 97cc60e3f..5e2a79411 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -368,6 +368,9 @@ pr: reviewRequested: Review Requested changesRequested: Changes Requested myPRs: My Pull Requests + titleUpdated: Pull request title updated + targetBranchUpdated: Target branch updated to {{branch}} + titleAndBranchUpdated: 'Pull request updated: Title and target branch ({{branch}})' prState: draftHeading: This pull request is still a work in progress draftDesc: Draft pull requests cannot be merged. @@ -391,6 +394,7 @@ prReview: codeowners: '{author} requested review from {codeowners} as {count|1:code owner,code owners}' defaultReviewers: '{author} requested review from {reviewers} as {count|1:default reviewer,default reviewers}' labelsAssigned: '{count|0:label,labels}' + targetBranchChange: '{user} changed the target branch from {old} to {new}' webhookListingContent: 'create,delete,deployment ...' general: 'General' webhooks: 'Webhooks' diff --git a/web/src/pages/PullRequest/Conversation/SystemComment.tsx b/web/src/pages/PullRequest/Conversation/SystemComment.tsx index 5c07b7d90..153aed897 100644 --- a/web/src/pages/PullRequest/Conversation/SystemComment.tsx +++ b/web/src/pages/PullRequest/Conversation/SystemComment.tsx @@ -31,6 +31,7 @@ import { CommitActions } from 'components/CommitActions/CommitActions' import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator' import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal' import { Label } from 'components/Label/Label' +import { GitRefLink } from 'components/GitRefLink/GitRefLink' import { ActivityLabel, CommentType } from '../PullRequestUtils' import css from './Conversation.module.scss' @@ -612,6 +613,50 @@ export const SystemComment: React.FC = ({ pullReqMetadata, c ) } + case CommentType.TARGET_BRANCH_CHANGE: { + const vars = { + user: {payload?.author?.display_name}, + old: ( + + ), + new: ( + + ) + } + return ( + + + + + + + + + + + ) + } + default: { // eslint-disable-next-line no-console console.warn('Unable to render system type activity', commentItems) diff --git a/web/src/pages/PullRequest/PullRequest.tsx b/web/src/pages/PullRequest/PullRequest.tsx index 0a25893b3..455bec911 100644 --- a/web/src/pages/PullRequest/PullRequest.tsx +++ b/web/src/pages/PullRequest/PullRequest.tsx @@ -62,6 +62,8 @@ export default function PullRequest() { refetchPullReq, retryOnErrorFunc } = useGetPullRequestInfo() + const [edit, setEdit] = useState(false) + const [currentRef, setCurrentRef] = useState('') const onAddDescriptionClick = useCallback(() => { setShowEditDescription(true) @@ -98,6 +100,9 @@ export default function PullRequest() { repoMetadata={repoMetadata} {...pullReqMetadata} onAddDescriptionClick={onAddDescriptionClick} + currentRef={currentRef as string} + edit={edit} + setEdit={setEdit} /> ) : ( '' @@ -118,7 +123,13 @@ export default function PullRequest() { <> - + > = ({ +interface PullRequestMetaLineProps extends TypesPullReq, Pick { + edit: boolean + currentRef: string + setCurrentRef: React.Dispatch> +} + +export const PullRequestMetaLine: React.FC = ({ repoMetadata, target_branch, source_branch, @@ -38,8 +45,14 @@ export const PullRequestMetaLine: React.FC { + useEffect(() => { + setCurrentRef(target_branch as string) + }, [target_branch, edit]) const { getString } = useStrings() const { routes } = useAppContext() const vars = { @@ -47,12 +60,23 @@ export const PullRequestMetaLine: React.FC{author?.display_name || author?.email || ''}, commits: {stats?.commits}, commitsCount: stats?.commits, - target: ( + target: !edit ? ( + ) : ( + { + setCurrentRef(ref) + }} + /> ), source: ( { + edit: boolean + currentRef: string + setEdit: React.Dispatch> onSaveDone?: (newTitle: string) => Promise onAddDescriptionClick: () => void } @@ -38,28 +41,78 @@ export const PullRequestTitle: React.FC = ({ title, number, description, + currentRef, + target_branch, + edit, + setEdit, 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({ + const { showError, showSuccess } = useToaster() + const { mutate: updatePRTitle } = useMutate({ verb: 'PATCH', - path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${number}` + path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${number}` + }) + const { mutate: updateTargetBranch } = useMutate({ + verb: 'PUT', + path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${number}/branch` }) const submitChange = useCallback(() => { - mutate({ - title: val, - description - }) - .then(() => { - setEdit(false) + const titleChanged = title !== val + const targetBranchChanged = target_branch !== currentRef + + if (!titleChanged && !targetBranchChanged) { + return + } + + const promises = [] + + if (titleChanged) { + promises.push( + updatePRTitle({ + title: val, + description + }) + .then(() => ({ success: true, type: 'title' })) + .catch(exception => { + showError(getErrorMessage(exception), 1000) + return { success: false, type: 'title' } + }) + ) + } + + if (targetBranchChanged) { + promises.push( + updateTargetBranch({ branch_name: currentRef }) + .then(() => ({ success: true, type: 'branch' })) + .catch(exception => { + showError(getErrorMessage(exception), 1000) + return { success: false, type: 'branch' } + }) + ) + } + + Promise.all(promises).then(results => { + setEdit(false) + if (titleChanged && results.some(r => r.type === 'title' && r.success)) { setOriginal(val) - }) - .catch(exception => showError(getErrorMessage(exception), 0)) - }, [description, val, mutate, showError]) + } + + const successful = results.filter(result => result.success) + const titleSuccess = successful.some(r => r.type === 'title') + const branchSuccess = successful.some(r => r.type === 'branch') + + if (titleSuccess && branchSuccess) { + showSuccess(getString('pr.titleAndBranchUpdated', { branch: currentRef }), 3000) + } else if (titleSuccess) { + showSuccess(getString('pr.titleUpdated'), 3000) + } else if (branchSuccess) { + showSuccess(getString('pr.targetBranchUpdated', { branch: currentRef }), 3000) + } + }) + }, [description, val, title, target_branch, currentRef, updatePRTitle, updateTargetBranch, showError, showSuccess]) useEffect(() => { setOriginal(title) @@ -99,7 +152,7 @@ export const PullRequestTitle: React.FC = ({ variation={ButtonVariation.PRIMARY} text={getString('save')} size={ButtonSize.MEDIUM} - disabled={(val || '').trim().length === 0 || title === val} + disabled={isEmpty(val?.trim()) || !(title !== val || target_branch !== currentRef)} onClick={submitChange} />