Merge branch 'ui/editor-dirty-check' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#48)

This commit is contained in:
Tan Nhu 2023-05-01 23:32:14 +00:00 committed by Harness
commit db0fcbcde4
12 changed files with 256 additions and 122 deletions

View File

@ -1,124 +1,127 @@
import React, { useCallback, useEffect, useState } from 'react' // import React, { useCallback, useEffect, useState } from 'react'
import { Radio, RadioGroup, ButtonVariation, Button, Container, Layout, ButtonSize, useToaster } from '@harness/uicore' // import { Radio, RadioGroup, ButtonVariation, Button, Container, Layout, ButtonSize, useToaster } from '@harness/uicore'
import { Render } from 'react-jsx-match' // import { Render } from 'react-jsx-match'
import { useMutate } from 'restful-react' // import { useMutate } from 'restful-react'
import cx from 'classnames' // import cx from 'classnames'
import { useStrings } from 'framework/strings' // import { useStrings } from 'framework/strings'
import type { GitInfoProps } from 'utils/GitUtils' // import type { GitInfoProps } from 'utils/GitUtils'
import type { TypesPullReq } from 'services/code' // import type { TypesPullReq } from 'services/code'
import { getErrorMessage } from 'utils/Utils' // import { getErrorMessage } from 'utils/Utils'
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview' // import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
import css from './ReviewDecisionButton.module.scss' // import css from './ReviewDecisionButton.module.scss'
enum PullReqReviewDecision { // enum PullReqReviewDecision {
REVIEWED = 'reviewed', // REVIEWED = 'reviewed',
APPROVED = 'approved', // APPROVED = 'approved',
CHANGEREQ = 'changereq' // CHANGEREQ = 'changereq'
} // }
interface ReviewDecisionButtonProps extends Pick<GitInfoProps, 'repoMetadata'> { // interface ReviewDecisionButtonProps extends Pick<GitInfoProps, 'repoMetadata'> {
shouldHide: boolean // shouldHide: boolean
pullRequestMetadata?: TypesPullReq // pullRequestMetadata?: TypesPullReq
} // }
export const ReviewDecisionButton: React.FC<ReviewDecisionButtonProps> = ({ // /**
repoMetadata, // * @deprecated
pullRequestMetadata, // */
shouldHide // export const ReviewDecisionButton: React.FC<ReviewDecisionButtonProps> = ({
}) => { // repoMetadata,
const { getString } = useStrings() // pullRequestMetadata,
const { showError, showSuccess } = useToaster() // shouldHide
const [comment, setComment] = useState('') // }) => {
const [reset, setReset] = useState(false) // const { getString } = useStrings()
const [decision, setDecision] = useState<PullReqReviewDecision>(PullReqReviewDecision.REVIEWED) // const { showError, showSuccess } = useToaster()
const { mutate, loading } = useMutate({ // const [comment, setComment] = useState('')
verb: 'POST', // const [reset, setReset] = useState(false)
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviews` // const [decision, setDecision] = useState<PullReqReviewDecision>(PullReqReviewDecision.REVIEWED)
}) // const { mutate, loading } = useMutate({
const submitReview = useCallback(() => { // verb: 'POST',
mutate({ decision, message: comment }) // path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviews`
.then(() => { // })
setReset(true) // const submitReview = useCallback(() => {
showSuccess(getString('pr.reviewSubmitted')) // mutate({ decision, message: comment })
}) // .then(() => {
.catch(exception => showError(getErrorMessage(exception))) // setReset(true)
}, [comment, decision, mutate, showError, showSuccess, getString]) // showSuccess(getString('pr.reviewSubmitted'))
// })
// .catch(exception => showError(getErrorMessage(exception)))
// }, [comment, decision, mutate, showError, showSuccess, getString])
useEffect(() => { // useEffect(() => {
let timeoutId = 0 // let timeoutId = 0
if (reset) { // if (reset) {
timeoutId = window.setTimeout(() => setReset(false)) // timeoutId = window.setTimeout(() => setReset(false))
} // }
return () => window.clearTimeout(timeoutId) // return () => window.clearTimeout(timeoutId)
}, [reset]) // }, [reset])
return ( // return (
<Button // <Button
text={getString('pr.reviewChanges')} // text={getString('pr.reviewChanges')}
variation={ButtonVariation.PRIMARY} // variation={ButtonVariation.PRIMARY}
intent="success" // intent="success"
rightIcon="chevron-down" // rightIcon="chevron-down"
className={cx(css.btn, { [css.hide]: shouldHide })} // className={cx(css.btn, { [css.hide]: shouldHide })}
style={{ '--background-color': 'var(--green-800)' } as React.CSSProperties} // style={{ '--background-color': 'var(--green-800)' } as React.CSSProperties}
tooltip={ // tooltip={
<Render when={!reset}> // <Render when={!reset}>
<Container padding="large" className={css.popup}> // <Container padding="large" className={css.popup}>
<Layout.Vertical spacing="medium"> // <Layout.Vertical spacing="medium">
<Container className={css.markdown} padding="medium"> // <Container className={css.markdown} padding="medium">
<MarkdownEditorWithPreview // <MarkdownEditorWithPreview
value={comment} // value={comment}
onChange={setComment} // onChange={setComment}
hideButtons // hideButtons
i18n={{ // i18n={{
placeHolder: getString('leaveAComment'), // placeHolder: getString('leaveAComment'),
tabEdit: getString('write'), // tabEdit: getString('write'),
tabPreview: getString('preview'), // tabPreview: getString('preview'),
save: getString('save'), // save: getString('save'),
cancel: getString('cancel') // cancel: getString('cancel')
}} // }}
editorHeight="100px" // editorHeight="100px"
/> // />
</Container> // </Container>
<Container padding={{ left: 'xxxlarge' }}> // <Container padding={{ left: 'xxxlarge' }}>
<RadioGroup> // <RadioGroup>
<Radio // <Radio
name="decision" // name="decision"
defaultChecked={decision === PullReqReviewDecision.REVIEWED} // defaultChecked={decision === PullReqReviewDecision.REVIEWED}
label={getString('comment')} // label={getString('comment')}
value={PullReqReviewDecision.REVIEWED} // value={PullReqReviewDecision.REVIEWED}
onChange={() => setDecision(PullReqReviewDecision.REVIEWED)} // onChange={() => setDecision(PullReqReviewDecision.REVIEWED)}
/> // />
<Radio // <Radio
name="decision" // name="decision"
defaultChecked={decision === PullReqReviewDecision.APPROVED} // defaultChecked={decision === PullReqReviewDecision.APPROVED}
label={getString('approve')} // label={getString('approve')}
value={PullReqReviewDecision.APPROVED} // value={PullReqReviewDecision.APPROVED}
onChange={() => setDecision(PullReqReviewDecision.APPROVED)} // onChange={() => setDecision(PullReqReviewDecision.APPROVED)}
/> // />
<Radio // <Radio
name="decision" // name="decision"
defaultChecked={decision === PullReqReviewDecision.CHANGEREQ} // defaultChecked={decision === PullReqReviewDecision.CHANGEREQ}
label={getString('requestChanges')} // label={getString('requestChanges')}
value={PullReqReviewDecision.CHANGEREQ} // value={PullReqReviewDecision.CHANGEREQ}
onChange={() => setDecision(PullReqReviewDecision.CHANGEREQ)} // onChange={() => setDecision(PullReqReviewDecision.CHANGEREQ)}
/> // />
</RadioGroup> // </RadioGroup>
</Container> // </Container>
<Container> // <Container>
<Button // <Button
variation={ButtonVariation.PRIMARY} // variation={ButtonVariation.PRIMARY}
text={getString('submitReview')} // text={getString('submitReview')}
size={ButtonSize.MEDIUM} // size={ButtonSize.MEDIUM}
onClick={submitReview} // onClick={submitReview}
disabled={!(comment || '').trim().length && decision != PullReqReviewDecision.APPROVED} // disabled={!(comment || '').trim().length && decision != PullReqReviewDecision.APPROVED}
loading={loading} // loading={loading}
/> // />
</Container> // </Container>
</Layout.Vertical> // </Layout.Vertical>
</Container> // </Container>
</Render> // </Render>
} // }
tooltipProps={{ interactionKind: 'click', position: 'bottom-right', hasBackdrop: true }} // tooltipProps={{ interactionKind: 'click', position: 'bottom-right', hasBackdrop: true }}
/> // />
) // )
} // }

View File

@ -1,4 +1,4 @@
import React, { useCallback, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useResizeDetector } from 'react-resize-detector' import { useResizeDetector } from 'react-resize-detector'
import type { EditorView } from '@codemirror/view' import type { EditorView } from '@codemirror/view'
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match' import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
@ -68,6 +68,7 @@ interface CommentBoxProps<T> {
atCommentItem?: CommentItem<T> atCommentItem?: CommentItem<T>
) => Promise<[boolean, CommentItem<T> | undefined]> ) => Promise<[boolean, CommentItem<T> | undefined]>
onCancel?: () => void onCancel?: () => void
setDirty: (dirty: boolean) => void
outlets?: Partial<Record<CommentBoxOutletPosition, React.ReactNode>> outlets?: Partial<Record<CommentBoxOutletPosition, React.ReactNode>>
} }
@ -83,12 +84,14 @@ export const CommentBox = <T = unknown,>({
onCancel = noop, onCancel = noop,
hideCancel, hideCancel,
resetOnSave, resetOnSave,
setDirty: setDirtyProp,
outlets = {} outlets = {}
}: CommentBoxProps<T>) => { }: CommentBoxProps<T>) => {
const { getString } = useStrings() const { getString } = useStrings()
const [comments, setComments] = useState<CommentItem<T>[]>(commentItems) const [comments, setComments] = useState<CommentItem<T>[]>(commentItems)
const [showReplyPlaceHolder, setShowReplyPlaceHolder] = useState(!!comments.length) const [showReplyPlaceHolder, setShowReplyPlaceHolder] = useState(!!comments.length)
const [markdown, setMarkdown] = useState(initialContent) const [markdown, setMarkdown] = useState(initialContent)
const [dirties, setDirties] = useState<Record<string, boolean>>({})
const { ref } = useResizeDetector<HTMLDivElement>({ const { ref } = useResizeDetector<HTMLDivElement>({
refreshMode: 'debounce', refreshMode: 'debounce',
handleWidth: false, handleWidth: false,
@ -117,6 +120,13 @@ export const CommentBox = <T = unknown,>({
}, []) }, [])
const viewRef = useRef<EditorView>() const viewRef = useRef<EditorView>()
useEffect(() => {
setDirtyProp(Object.values(dirties).some(dirty => dirty))
return () => {
setDirtyProp(false)
}
}, [dirties]) // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<Container <Container
className={cx(css.main, { [css.fluid]: fluid }, className)} className={cx(css.main, { [css.fluid]: fluid }, className)}
@ -140,6 +150,9 @@ export const CommentBox = <T = unknown,>({
return [result, updatedItem] return [result, updatedItem]
}} }}
setDirty={(index, dirty) => {
setDirties({ ...dirties, [index]: dirty })
}}
outlets={outlets} outlets={outlets}
/> />
<Match expr={showReplyPlaceHolder}> <Match expr={showReplyPlaceHolder}>
@ -199,6 +212,9 @@ export const CommentBox = <T = unknown,>({
}} }}
onCancel={_onCancel} onCancel={_onCancel}
hideCancel={hideCancel} hideCancel={hideCancel}
setDirty={_dirty => {
setDirties({ ...dirties, ['new']: _dirty })
}}
/> />
</Container> </Container>
</Falsy> </Falsy>
@ -211,12 +227,14 @@ export const CommentBox = <T = unknown,>({
interface CommentsThreadProps<T> extends Pick<CommentBoxProps<T>, 'commentItems' | 'handleAction' | 'outlets'> { interface CommentsThreadProps<T> extends Pick<CommentBoxProps<T>, 'commentItems' | 'handleAction' | 'outlets'> {
onQuote: (content: string) => void onQuote: (content: string) => void
setDirty: (index: number, dirty: boolean) => void
} }
const CommentsThread = <T = unknown,>({ const CommentsThread = <T = unknown,>({
onQuote, onQuote,
commentItems = [], commentItems = [],
handleAction, handleAction,
setDirty,
outlets = {} outlets = {}
}: CommentsThreadProps<T>) => { }: CommentsThreadProps<T>) => {
const { getString } = useStrings() const { getString } = useStrings()
@ -331,6 +349,9 @@ const CommentsThread = <T = unknown,>({
} }
}} }}
onCancel={() => resetStateAtIndex(index)} onCancel={() => resetStateAtIndex(index)}
setDirty={_dirty => {
setDirty(index, _dirty)
}}
i18n={{ i18n={{
placeHolder: getString('leaveAComment'), placeHolder: getString('leaveAComment'),
tabEdit: getString('write'), tabEdit: getString('write'),

View File

@ -28,6 +28,7 @@ import type { OpenapiCommentCreatePullReqRequest, TypesPullReq, TypesPullReqActi
import { getErrorMessage } from 'utils/Utils' import { getErrorMessage } from 'utils/Utils'
import { CopyButton } from 'components/CopyButton/CopyButton' import { CopyButton } from 'components/CopyButton/CopyButton'
import { AppWrapper } from 'App' import { AppWrapper } from 'App'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import { import {
activitiesToDiffCommentItems, activitiesToDiffCommentItems,
activityToCommentItem, activityToCommentItem,
@ -91,6 +92,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
const { mutate: updateComment } = useMutate({ verb: 'PATCH', path: ({ id }) => `${path}/${id}` }) const { mutate: updateComment } = useMutate({ verb: 'PATCH', path: ({ id }) => `${path}/${id}` })
const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` }) const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` })
const [comments, setComments] = useState<DiffCommentItem<TypesPullReqActivity>[]>(activitiesToDiffCommentItems(diff)) const [comments, setComments] = useState<DiffCommentItem<TypesPullReqActivity>[]>(activitiesToDiffCommentItems(diff))
const [dirty, setDirty] = useState(false)
const commentsRef = useRef<DiffCommentItem<TypesPullReqActivity>[]>(comments) const commentsRef = useRef<DiffCommentItem<TypesPullReqActivity>[]>(comments)
const setContainerRef = useCallback( const setContainerRef = useCallback(
node => { node => {
@ -298,6 +300,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
delete lineInfo.rowElement.dataset.annotated delete lineInfo.rowElement.dataset.annotated
setTimeout(() => setComments(commentsRef.current.filter(item => item !== comment)), 0) setTimeout(() => setComments(commentsRef.current.filter(item => item !== comment)), 0)
}} }}
setDirty={setDirty}
currentUserName={currentUser.display_name} currentUserName={currentUser.display_name}
handleAction={async (action, value, commentItem) => { handleAction={async (action, value, commentItem) => {
let result = true let result = true
@ -511,6 +514,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
</Render> </Render>
</Container> </Container>
</Layout.Vertical> </Layout.Vertical>
<NavigationCheck when={dirty} />
</Container> </Container>
) )
} }

View File

@ -40,6 +40,7 @@ interface MarkdownEditorWithPreviewProps {
onChange?: (value: string) => void onChange?: (value: string) => void
onSave?: (value: string) => void onSave?: (value: string) => void
onCancel?: () => void onCancel?: () => void
setDirty: (dirty: boolean) => void
i18n: { i18n: {
placeHolder: string placeHolder: string
tabEdit: string tabEdit: string
@ -59,6 +60,7 @@ export function MarkdownEditorWithPreview({
onChange, onChange,
onSave, onSave,
onCancel, onCancel,
setDirty: setDirtyProp,
i18n, i18n,
hideButtons, hideButtons,
hideCancel, hideCancel,
@ -186,6 +188,14 @@ export function MarkdownEditorWithPreview({
} }
}, []) }, [])
useEffect(() => {
setDirtyProp?.(dirty)
return () => {
setDirtyProp?.(false)
}
}, [dirty]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { useEffect(() => {
if (viewRefProp) { if (viewRefProp) {
viewRefProp.current = viewRef.current viewRefProp.current = viewRef.current

View File

@ -0,0 +1,7 @@
.main {
padding-top: var(--spacing-xxlarge) !important;
p {
word-break: normal;
}
}

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

@ -0,0 +1,56 @@
import React, { useState, useEffect, useCallback } from 'react'
import { Prompt, useHistory } from 'react-router-dom'
import { useConfirmationDialog } from '@harness/uicore'
import { Intent } from '@harness/design-system'
import type * as History from 'history'
import { useStrings } from 'framework/strings'
import css from './NavigationCheck.module.scss'
interface NavigationCheckProps {
when?: boolean
i18n?: {
message?: string
title?: string
confirmText?: string
cancelText?: string
}
replace?: boolean
}
export const NavigationCheck: React.FC<NavigationCheckProps> = ({ when, i18n, replace }) => {
const history = useHistory()
const [lastLocation, setLastLocation] = useState<History.Location | null>(null)
const [confirmed, setConfirmed] = useState(false)
const { getString } = useStrings()
const { openDialog } = useConfirmationDialog({
cancelButtonText: i18n?.cancelText || getString('unsavedChanges.stay'),
showCloseButton: false,
contentText: i18n?.message || getString('unsavedChanges.message'),
titleText: i18n?.title || getString('unsavedChanges.title'),
confirmButtonText: i18n?.confirmText || getString('unsavedChanges.leave'),
intent: Intent.WARNING,
onCloseDialog: setConfirmed,
className: css.main
})
const prompt = useCallback(
(nextLocation: History.Location): string | boolean => {
if (!confirmed) {
openDialog()
setLastLocation(nextLocation)
return false
}
return true
},
[confirmed, openDialog]
)
useEffect(() => {
if (confirmed && lastLocation) {
history[replace ? 'replace' : 'push'](lastLocation.pathname + lastLocation.search)
}
confirmed && setConfirmed(false)
}, [confirmed, lastLocation, history, replace])
return <Prompt when={when} message={prompt} />
}

View File

@ -302,6 +302,10 @@ export interface StringsMap {
tags: string tags: string
title: string title: string
tooltipRepoEdit: string tooltipRepoEdit: string
'unsavedChanges.leave': string
'unsavedChanges.message': string
'unsavedChanges.stay': string
'unsavedChanges.title': string
updateFile: string updateFile: string
updateWebhook: string updateWebhook: string
updated: string updated: string

View File

@ -377,3 +377,8 @@ viewRaw: View Raw
download: Download download: Download
changes: Changes changes: Changes
contents: Contents contents: Contents
unsavedChanges:
title: Close without saving?
message: 'You have unsaved changes. Are you sure you want to leave this page without saving?'
leave: Leave this Page
stay: Stay on this Page

View File

@ -33,6 +33,7 @@ import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from
import { useConfirmAct } from 'hooks/useConfirmAction' import { useConfirmAct } from 'hooks/useConfirmAction'
import { commentState, formatDate, formatTime, getErrorMessage, orderSortDate, dayAgoInMS } from 'utils/Utils' import { commentState, formatDate, formatTime, getErrorMessage, orderSortDate, dayAgoInMS } from 'utils/Utils'
import { activityToCommentItem, CommentType, DIFF2HTML_CONFIG, ViewStyle } from 'components/DiffViewer/DiffViewerUtils' import { activityToCommentItem, CommentType, DIFF2HTML_CONFIG, ViewStyle } from 'components/DiffViewer/DiffViewerUtils'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import { ThreadSection } from 'components/ThreadSection/ThreadSection' import { ThreadSection } from 'components/ThreadSection/ThreadSection'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper' import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
import { DescriptionBox } from './DescriptionBox' import { DescriptionBox } from './DescriptionBox'
@ -175,6 +176,8 @@ export const Conversation: React.FC<ConversationProps> = ({
const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` }) const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` })
const confirmAct = useConfirmAct() const confirmAct = useConfirmAct()
const [commentCreated, setCommentCreated] = useState(false) const [commentCreated, setCommentCreated] = useState(false)
const [dirtyNewComment, setDirtyNewComment] = useState(false)
const [dirtyCurrentComments, setDirtyCurrentComments] = useState(false)
const refreshPR = useCallback(() => { const refreshPR = useCallback(() => {
onCommentUpdate() onCommentUpdate()
@ -316,6 +319,7 @@ export const Conversation: React.FC<ConversationProps> = ({
})} })}
commentItems={commentItems} commentItems={commentItems}
currentUserName={currentUser.display_name} currentUserName={currentUser.display_name}
setDirty={setDirtyCurrentComments}
handleAction={async (action, value, commentItem) => { handleAction={async (action, value, commentItem) => {
let result = true let result = true
let updatedItem: CommentItem<TypesPullReqActivity> | undefined = undefined let updatedItem: CommentItem<TypesPullReqActivity> | undefined = undefined
@ -411,6 +415,7 @@ export const Conversation: React.FC<ConversationProps> = ({
currentUserName={currentUser.display_name} currentUserName={currentUser.display_name}
resetOnSave resetOnSave
hideCancel hideCancel
setDirty={setDirtyNewComment}
handleAction={async (_action, value) => { handleAction={async (_action, value) => {
let result = true let result = true
let updatedItem: CommentItem<TypesPullReqActivity> | undefined = undefined let updatedItem: CommentItem<TypesPullReqActivity> | undefined = undefined
@ -440,6 +445,7 @@ export const Conversation: React.FC<ConversationProps> = ({
</Container> </Container>
</Layout.Vertical> </Layout.Vertical>
</Container> </Container>
<NavigationCheck when={dirtyCurrentComments || dirtyNewComment} />
</PullRequestTabContentWrapper> </PullRequestTabContentWrapper>
) )
} }

View File

@ -7,6 +7,7 @@ import { useStrings } from 'framework/strings'
import type { OpenapiUpdatePullReqRequest } from 'services/code' import type { OpenapiUpdatePullReqRequest } from 'services/code'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview' import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import { getErrorMessage } from 'utils/Utils' 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'
@ -17,7 +18,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
onCommentUpdate: refreshPullRequestMetadata onCommentUpdate: refreshPullRequestMetadata
}) => { }) => {
const [edit, setEdit] = useState(false) const [edit, setEdit] = useState(false)
// const [updated, setUpdated] = useState(pullRequestMetadata.edited as number) const [dirty, setDirty] = useState(false)
const [originalContent, setOriginalContent] = useState(pullRequestMetadata.description as string) const [originalContent, setOriginalContent] = useState(pullRequestMetadata.description as string)
const [content, setContent] = useState(originalContent) const [content, setContent] = useState(originalContent)
const { getString } = useStrings() const { getString } = useStrings()
@ -52,6 +53,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
setContent(originalContent) setContent(originalContent)
setEdit(false) setEdit(false)
}} }}
setDirty={setDirty}
i18n={{ i18n={{
placeHolder: getString('pr.enterDesc'), placeHolder: getString('pr.enterDesc'),
tabEdit: getString('write'), tabEdit: getString('write'),
@ -84,6 +86,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
</Container> </Container>
)} )}
</Container> </Container>
<NavigationCheck when={dirty} />
</Container> </Container>
) )
} }

View File

@ -24,6 +24,7 @@ import { filenameToLanguage, FILE_SEPERATOR } from 'utils/Utils'
import { useGetResourceContent } from 'hooks/useGetResourceContent' import { useGetResourceContent } from 'hooks/useGetResourceContent'
import { CommitModalButton } from 'components/CommitModalButton/CommitModalButton' import { CommitModalButton } from 'components/CommitModalButton/CommitModalButton'
import { DiffEditor } from 'components/SourceCodeEditor/MonacoSourceCodeEditor' import { DiffEditor } from 'components/SourceCodeEditor/MonacoSourceCodeEditor'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import css from './FileEditor.module.scss' import css from './FileEditor.module.scss'
interface EditorProps extends Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath'> { interface EditorProps extends Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath'> {
@ -97,6 +98,11 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
verifyFolder().then(() => setStartVerifyFolder(true)) verifyFolder().then(() => setStartVerifyFolder(true))
}, [fileName, parentPath, language, content, verifyFolder]) }, [fileName, parentPath, language, content, verifyFolder])
const [selectedView, setSelectedView] = useState(VisualYamlSelectedView.VISUAL) const [selectedView, setSelectedView] = useState(VisualYamlSelectedView.VISUAL)
const [dirty, setDirty] = useState(false)
useEffect(() => {
setDirty(!(!fileName || (isUpdate && content === originalContent)))
}, [fileName, isUpdate, content, originalContent])
// Calculate file name input field width based on number of characters inside // Calculate file name input field width based on number of characters inside
useEffect(() => { useEffect(() => {
@ -194,7 +200,7 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
<CommitModalButton <CommitModalButton
text={getString('commitChanges')} text={getString('commitChanges')}
variation={ButtonVariation.PRIMARY} variation={ButtonVariation.PRIMARY}
disabled={!fileName || (isUpdate && content === originalContent)} disabled={!dirty}
repoMetadata={repoMetadata} repoMetadata={repoMetadata}
commitAction={commitAction} commitAction={commitAction}
commitTitlePlaceHolder={getString(isNew ? 'createFile' : isUpdate ? 'updateFile' : 'renameFile') commitTitlePlaceHolder={getString(isNew ? 'createFile' : isUpdate ? 'updateFile' : 'renameFile')
@ -206,6 +212,8 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
payload={content} payload={content}
sha={resourceContent?.sha} sha={resourceContent?.sha}
onSuccess={(_data, newBranch) => { onSuccess={(_data, newBranch) => {
setDirty(false)
if (newBranch) { if (newBranch) {
history.replace( history.replace(
routes.toCODECompare({ routes.toCODECompare({
@ -259,6 +267,7 @@ function Editor({ resourceContent, repoMetadata, gitRef, resourcePath, isReposit
<DiffEditor language={language} original={originalContent} source={content} onChange={setContent} /> <DiffEditor language={language} original={originalContent} source={content} onChange={setContent} />
)} )}
</Container> </Container>
<NavigationCheck when={dirty} />
</Container> </Container>
) )
} }