mirror of
https://github.com/harness/drone.git
synced 2025-05-20 10:59:56 +08:00
Merge branch 'ui/editor-dirty-check' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#48)
This commit is contained in:
commit
db0fcbcde4
@ -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 }}
|
||||||
/>
|
// />
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
@ -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'),
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
.main {
|
||||||
|
padding-top: var(--spacing-xxlarge) !important;
|
||||||
|
|
||||||
|
p {
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
}
|
6
web/src/components/NavigationCheck/NavigationCheck.module.scss.d.ts
vendored
Normal file
6
web/src/components/NavigationCheck/NavigationCheck.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// this is an auto-generated file
|
||||||
|
declare const styles: {
|
||||||
|
readonly main: string
|
||||||
|
}
|
||||||
|
export default styles
|
56
web/src/components/NavigationCheck/NavigationCheck.tsx
Normal file
56
web/src/components/NavigationCheck/NavigationCheck.tsx
Normal 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} />
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user