mirror of
https://github.com/harness/drone.git
synced 2025-05-18 18:09:56 +08:00
Implement components to support PR commenting (#146)
This commit is contained in:
parent
1337f729e7
commit
e4ae2b269b
66
web/src/components/CommentBox/CommentBox.module.scss
Normal file
66
web/src/components/CommentBox/CommentBox.module.scss
Normal file
@ -0,0 +1,66 @@
|
||||
.main {
|
||||
max-width: 900px;
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--white) !important;
|
||||
|
||||
.box {
|
||||
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
||||
border-radius: 4px;
|
||||
|
||||
:global {
|
||||
.cm-editor .cm-line {
|
||||
&,
|
||||
* {
|
||||
font-family: var(--font-family);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.boxLayout {
|
||||
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.viewer {
|
||||
padding-bottom: var(--spacing-xsmall) !important;
|
||||
|
||||
:global {
|
||||
.wmde-markdown .anchor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.replyPlaceHolder {
|
||||
align-items: center;
|
||||
|
||||
> div:last-of-type {
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.deleteMenuItem:hover {
|
||||
background-color: var(--red-500);
|
||||
|
||||
* {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.newCommentContainer {
|
||||
border-top: 1px solid var(--grey-200);
|
||||
padding-top: var(--spacing-xlarge) !important;
|
||||
background: var(--grey-50) !important;
|
||||
}
|
||||
|
||||
.editCommentContainer {
|
||||
background-color: var(--grey-50) !important;
|
||||
padding: var(--spacing-large) !important;
|
||||
border-radius: 5px;
|
||||
}
|
@ -5,9 +5,9 @@ declare const styles: {
|
||||
readonly box: string
|
||||
readonly boxLayout: string
|
||||
readonly viewer: string
|
||||
readonly editor: string
|
||||
readonly markdownEditor: string
|
||||
readonly preview: string
|
||||
readonly replyPlaceHolder: string
|
||||
readonly deleteMenuItem: string
|
||||
readonly newCommentContainer: string
|
||||
readonly editCommentContainer: string
|
||||
}
|
||||
export default styles
|
229
web/src/components/CommentBox/CommentBox.tsx
Normal file
229
web/src/components/CommentBox/CommentBox.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useResizeDetector } from 'react-resize-detector'
|
||||
import { Container, Layout, Avatar, TextInput, Text, Color, FontVariation, FlexExpander } from '@harness/uicore'
|
||||
import MarkdownEditor from '@uiw/react-markdown-editor'
|
||||
import ReactTimeago from 'react-timeago'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import 'diff2html/bundles/css/diff2html.min.css'
|
||||
import type { UseStringsReturn } from 'framework/strings'
|
||||
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import { CodeIcon } from 'utils/GitUtils'
|
||||
import type { CommentThreadEntry, UserProfile } from 'utils/types'
|
||||
import { MenuDivider, OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||
import css from './CommentBox.module.scss'
|
||||
|
||||
interface CommentBoxProps {
|
||||
getString: UseStringsReturn['getString']
|
||||
onHeightChange: (height: number) => void
|
||||
onCancel: () => void
|
||||
width?: string
|
||||
commentsThread: CommentThreadEntry[]
|
||||
currentUser: UserProfile
|
||||
executeDeleteComent: (params: { commentEntry: CommentThreadEntry; onSuccess: () => void }) => void
|
||||
}
|
||||
|
||||
export const CommentBox: React.FC<CommentBoxProps> = ({
|
||||
getString,
|
||||
onHeightChange,
|
||||
onCancel,
|
||||
width,
|
||||
commentsThread: _commentsThread = [],
|
||||
currentUser,
|
||||
executeDeleteComent
|
||||
}) => {
|
||||
// TODO: \r\n for Windows or based on configuration
|
||||
// @see https://www.aleksandrhovhannisyan.com/blog/crlf-vs-lf-normalizing-line-endings-in-git/
|
||||
const CRLF = '\n'
|
||||
const [commentsThread, setCommentsThread] = useState<CommentThreadEntry[]>(_commentsThread)
|
||||
const [showReplyPlaceHolder, setShowReplyPlaceHolder] = useState(!!commentsThread.length)
|
||||
const [markdown, setMarkdown] = useState('')
|
||||
const { ref } = useResizeDetector({
|
||||
refreshMode: 'debounce',
|
||||
handleWidth: false,
|
||||
refreshRate: 50,
|
||||
observerOptions: {
|
||||
box: 'border-box'
|
||||
},
|
||||
onResize: () => {
|
||||
onHeightChange(ref.current?.offsetHeight)
|
||||
}
|
||||
})
|
||||
const _onCancel = useCallback(() => {
|
||||
setMarkdown('')
|
||||
if (!commentsThread.length) {
|
||||
onCancel()
|
||||
} else {
|
||||
setShowReplyPlaceHolder(true)
|
||||
// onHeightChange('auto')
|
||||
}
|
||||
}, [commentsThread, setShowReplyPlaceHolder, onCancel])
|
||||
const hidePlaceHolder = useCallback(() => {
|
||||
setShowReplyPlaceHolder(false)
|
||||
// onHeightChange('auto')
|
||||
}, [setShowReplyPlaceHolder])
|
||||
const onQuote = useCallback((content: string) => {
|
||||
setShowReplyPlaceHolder(false)
|
||||
// onHeightChange('auto')
|
||||
setMarkdown(
|
||||
content
|
||||
.split(CRLF)
|
||||
.map(line => `> ${line}`)
|
||||
.concat([CRLF, CRLF])
|
||||
.join(CRLF)
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container className={css.main} padding="medium" width={width} ref={ref}>
|
||||
<Container className={css.box}>
|
||||
<Layout.Vertical className={css.boxLayout}>
|
||||
<CommentsThread
|
||||
commentsThread={commentsThread}
|
||||
getString={getString}
|
||||
currentUser={currentUser}
|
||||
onQuote={onQuote}
|
||||
executeDeleteComent={executeDeleteComent}
|
||||
/>
|
||||
|
||||
{(showReplyPlaceHolder && (
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small" className={css.replyPlaceHolder} padding="medium">
|
||||
<Avatar name={currentUser.name} size="small" hoverCard={false} />
|
||||
<TextInput placeholder={getString('replyHere')} onFocus={hidePlaceHolder} onClick={hidePlaceHolder} />
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
)) || (
|
||||
<Container padding="xlarge" className={css.newCommentContainer}>
|
||||
<MarkdownEditorWithPreview
|
||||
placeHolder={getString(commentsThread.length ? 'replyHere' : 'leaveAComment')}
|
||||
value={markdown}
|
||||
editTabText={getString('write')}
|
||||
previewTabText={getString('preview')}
|
||||
saveButtonText={getString('addComment')}
|
||||
cancelButtonText={getString('cancel')}
|
||||
onChange={setMarkdown}
|
||||
onSave={value => {
|
||||
setCommentsThread([
|
||||
...commentsThread,
|
||||
{
|
||||
id: '0',
|
||||
author: currentUser.name,
|
||||
created: Date.now().toString(),
|
||||
updated: Date.now().toString(),
|
||||
content: value
|
||||
}
|
||||
])
|
||||
setMarkdown('')
|
||||
setShowReplyPlaceHolder(true)
|
||||
}}
|
||||
onCancel={_onCancel}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface CommentsThreadProps
|
||||
extends Pick<CommentBoxProps, 'commentsThread' | 'getString' | 'currentUser' | 'executeDeleteComent'> {
|
||||
onQuote: (content: string) => void
|
||||
}
|
||||
|
||||
const CommentsThread: React.FC<CommentsThreadProps> = ({
|
||||
getString,
|
||||
currentUser,
|
||||
onQuote,
|
||||
commentsThread = [],
|
||||
executeDeleteComent
|
||||
}) => {
|
||||
const [editIndexes, setEditIndexes] = useState<Record<number, boolean>>({})
|
||||
|
||||
return commentsThread.length ? (
|
||||
<Container className={css.viewer} padding="xlarge">
|
||||
{commentsThread.map((commentEntry, index) => {
|
||||
const isLastItem = index === commentsThread.length - 1
|
||||
|
||||
return (
|
||||
<ThreadSection
|
||||
key={index}
|
||||
title={
|
||||
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
|
||||
<Text inline icon={CodeIcon.Chat}></Text>
|
||||
<Avatar name={commentEntry.author} size="small" hoverCard={false} />
|
||||
<Text inline>
|
||||
<strong>{commentEntry.author}</strong>
|
||||
</Text>
|
||||
<PipeSeparator height={8} />
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||
<ReactTimeago date={new Date(commentEntry.updated)} />
|
||||
</Text>
|
||||
<FlexExpander />
|
||||
<OptionsMenuButton
|
||||
isDark={false}
|
||||
icon="Options"
|
||||
iconProps={{ size: 14 }}
|
||||
style={{ padding: '5px' }}
|
||||
items={[
|
||||
{
|
||||
text: getString('edit'),
|
||||
onClick: () => setEditIndexes({ ...editIndexes, ...{ [index]: true } })
|
||||
},
|
||||
{
|
||||
text: getString('quote'),
|
||||
onClick: () => {
|
||||
onQuote(commentEntry.content)
|
||||
}
|
||||
},
|
||||
MenuDivider,
|
||||
{
|
||||
text: (
|
||||
<Text width={100} color={Color.RED_500}>
|
||||
{getString('delete')}
|
||||
</Text>
|
||||
),
|
||||
onClick: () =>
|
||||
executeDeleteComent({
|
||||
commentEntry,
|
||||
onSuccess: () => {
|
||||
alert('success')
|
||||
}
|
||||
}),
|
||||
className: css.deleteMenuItem
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
}
|
||||
hideGutter={isLastItem}>
|
||||
<Container
|
||||
padding={{ left: editIndexes[index] ? undefined : 'medium', bottom: isLastItem ? undefined : 'xsmall' }}>
|
||||
{editIndexes[index] ? (
|
||||
<Container className={css.editCommentContainer}>
|
||||
<MarkdownEditorWithPreview
|
||||
placeHolder={getString('leaveAComment')}
|
||||
value={commentEntry.content}
|
||||
editTabText={getString('write')}
|
||||
previewTabText={getString('preview')}
|
||||
saveButtonText={getString('save')}
|
||||
cancelButtonText={getString('cancel')}
|
||||
onSave={(value, original) => {}}
|
||||
onCancel={() => {
|
||||
delete editIndexes[index]
|
||||
setEditIndexes({ ...editIndexes })
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
) : (
|
||||
<MarkdownEditor.Markdown key={index} source={commentEntry.content} />
|
||||
)}
|
||||
</Container>
|
||||
</ThreadSection>
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
) : null
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
.main {
|
||||
max-width: 900px;
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--white) !important;
|
||||
|
||||
.box {
|
||||
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
||||
border-radius: 4px;
|
||||
|
||||
:global {
|
||||
.cm-editor .cm-line {
|
||||
&,
|
||||
* {
|
||||
font-family: var(--font-family);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.md-editor-preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-editor,
|
||||
.md-editor-toolbar-warp {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.md-editor-content {
|
||||
padding: var(--spacing-small) var(--spacing-xsmall);
|
||||
background-color: var(--white);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
|
||||
.ͼ1.cm-editor.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
// .d2h-code-line,
|
||||
// .d2h-code-line-ctn {
|
||||
// white-space: pre-wrap;
|
||||
// }
|
||||
}
|
||||
|
||||
.boxLayout {
|
||||
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.viewer {
|
||||
padding-bottom: var(--spacing-xsmall) !important;
|
||||
|
||||
:global {
|
||||
.wmde-markdown .anchor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
background-color: var(--grey-50);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top: 1px solid var(--grey-100);
|
||||
|
||||
> div:first-child {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.markdownEditor {
|
||||
:global {
|
||||
.md-editor {
|
||||
--color-border-shadow: 0 0 0 1px rgb(178 178 178 / 10%), 0 0 0 rgb(223 223 223 / 0%),
|
||||
0 1px 1px rgb(104 109 112 / 20%);
|
||||
|
||||
&:focus-within {
|
||||
--color-border-shadow: 0 0 0 2px #1d86c7, 0 0 0 #059fff, 0 0px 0px rgb(104 109 112 / 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.replyPlaceHolder {
|
||||
align-items: center;
|
||||
|
||||
> div:last-of-type {
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useResizeDetector } from 'react-resize-detector'
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
ButtonVariation,
|
||||
Layout,
|
||||
Avatar,
|
||||
TextInput,
|
||||
Text,
|
||||
Color,
|
||||
FontVariation
|
||||
} from '@harness/uicore'
|
||||
import MarkdownEditor from '@uiw/react-markdown-editor'
|
||||
import { Tab, Tabs } from '@blueprintjs/core'
|
||||
import { indentWithTab } from '@codemirror/commands'
|
||||
import ReactTimeago from 'react-timeago'
|
||||
import { keymap } from '@codemirror/view'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import 'diff2html/bundles/css/diff2html.min.css'
|
||||
import type { CurrentUser } from 'hooks/useCurrentUser'
|
||||
import type { UseStringsReturn } from 'framework/strings'
|
||||
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
||||
import { CodeIcon } from 'utils/GitUtils'
|
||||
import css from './CommentBox.module.scss'
|
||||
|
||||
interface CommentBoxProps {
|
||||
getString: UseStringsReturn['getString']
|
||||
onHeightChange: (height: number | 'auto') => void
|
||||
onCancel: () => void
|
||||
width?: string
|
||||
contents?: string[]
|
||||
currentUser: CurrentUser
|
||||
}
|
||||
|
||||
export const CommentBox: React.FC<CommentBoxProps> = ({
|
||||
getString,
|
||||
onHeightChange,
|
||||
onCancel,
|
||||
width,
|
||||
contents: _contents = [],
|
||||
currentUser
|
||||
}) => {
|
||||
const [contents, setContents] = useState<string[]>(_contents)
|
||||
const [showReplyPlaceHolder, setShowReplyPlaceHolder] = useState(!!contents.length)
|
||||
const [markdown, setMarkdown] = useState('')
|
||||
const { ref } = useResizeDetector({
|
||||
refreshMode: 'debounce',
|
||||
handleWidth: false,
|
||||
refreshRate: 50,
|
||||
observerOptions: {
|
||||
box: 'border-box'
|
||||
},
|
||||
onResize: () => {
|
||||
onHeightChange(ref.current?.offsetHeight)
|
||||
}
|
||||
})
|
||||
// Note: Send 'auto' to avoid render flickering
|
||||
const onCancelBtnClick = useCallback(() => {
|
||||
if (!contents.length) {
|
||||
onCancel()
|
||||
} else {
|
||||
setShowReplyPlaceHolder(true)
|
||||
onHeightChange('auto')
|
||||
}
|
||||
}, [contents, setShowReplyPlaceHolder, onCancel, onHeightChange])
|
||||
const hidePlaceHolder = useCallback(() => {
|
||||
setShowReplyPlaceHolder(false)
|
||||
onHeightChange('auto')
|
||||
}, [setShowReplyPlaceHolder, onHeightChange])
|
||||
|
||||
return (
|
||||
<Container className={css.main} padding="medium" width={width} ref={ref}>
|
||||
<Container className={css.box}>
|
||||
<Layout.Vertical className={css.boxLayout}>
|
||||
{!!contents.length && (
|
||||
<Container className={css.viewer} padding="xlarge">
|
||||
{contents.map((content, index) => {
|
||||
const isLastItem = index === contents.length - 1
|
||||
return (
|
||||
<ThreadSection
|
||||
key={index}
|
||||
title={
|
||||
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
|
||||
<Text inline icon={CodeIcon.Chat}></Text>
|
||||
<Avatar name={currentUser.name} size="small" hoverCard={false} />
|
||||
<Text inline>
|
||||
<strong>{currentUser.name}</strong>
|
||||
</Text>
|
||||
<Text inline font={{ variation: FontVariation.SMALL }} color={Color.GREY_400}>
|
||||
<ReactTimeago date={'2022-12-22'} />
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
}
|
||||
hideGutter={isLastItem}>
|
||||
<Container padding={{ left: 'medium', bottom: isLastItem ? undefined : 'xsmall' }}>
|
||||
<MarkdownEditor.Markdown key={index} source={content} />
|
||||
</Container>
|
||||
</ThreadSection>
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
)}
|
||||
<Container className={css.editor}>
|
||||
{(showReplyPlaceHolder && (
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small" className={css.replyPlaceHolder} padding="medium">
|
||||
<Avatar name={currentUser.name} size="small" hoverCard={false} />
|
||||
<TextInput placeholder={getString('replyHere')} onFocus={hidePlaceHolder} onClick={hidePlaceHolder} />
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
)) || (
|
||||
<Container padding="xlarge">
|
||||
<Layout.Vertical spacing="large">
|
||||
<Tabs animate={true} id="CommentBoxTabs" onChange={() => onHeightChange('auto')} key="horizontal">
|
||||
<Tabs.Expander />
|
||||
<Tab
|
||||
id="write"
|
||||
title="Write"
|
||||
panel={
|
||||
<Container className={css.markdownEditor}>
|
||||
<MarkdownEditor
|
||||
value={markdown}
|
||||
visible={false}
|
||||
placeholder={getString(contents.length ? 'replyHere' : 'leaveAComment')}
|
||||
theme="light"
|
||||
indentWithTab={false}
|
||||
autoFocus
|
||||
toolbars={[
|
||||
// 'header',
|
||||
'bold',
|
||||
'italic',
|
||||
'strike',
|
||||
'quote',
|
||||
'olist',
|
||||
'ulist',
|
||||
'todo',
|
||||
'link',
|
||||
'image',
|
||||
'codeBlock'
|
||||
]}
|
||||
toolbarsMode={[]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
foldGutter: false,
|
||||
highlightActiveLine: false
|
||||
}}
|
||||
extensions={[keymap.of([indentWithTab])]}
|
||||
onChange={(value, _viewUpdate) => {
|
||||
onHeightChange('auto')
|
||||
setMarkdown(value)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
id="preview"
|
||||
disabled={!markdown}
|
||||
title="Preview"
|
||||
panel={
|
||||
<Container padding="large" className={css.preview}>
|
||||
<MarkdownEditor.Markdown source={markdown} />
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small">
|
||||
<Button
|
||||
disabled={!(markdown || '').trim()}
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
onClick={() => {
|
||||
setContents([...contents, markdown])
|
||||
setMarkdown('')
|
||||
setShowReplyPlaceHolder(true)
|
||||
onHeightChange('auto')
|
||||
}}
|
||||
text={getString('addComment')}
|
||||
/>
|
||||
<Button
|
||||
variation={ButtonVariation.TERTIARY}
|
||||
onClick={onCancelBtnClick}
|
||||
text={getString('cancel')}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,16 +1,28 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { Button, Color, Container, FlexExpander, ButtonVariation, Layout, Text, ButtonSize } from '@harness/uicore'
|
||||
import {
|
||||
Button,
|
||||
Color,
|
||||
Container,
|
||||
FlexExpander,
|
||||
ButtonVariation,
|
||||
Layout,
|
||||
Text,
|
||||
ButtonSize,
|
||||
Intent
|
||||
} from '@harness/uicore'
|
||||
import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import 'diff2html/bundles/css/diff2html.min.css'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { CodeIcon } from 'utils/GitUtils'
|
||||
import { useEventListener } from 'hooks/useEventListener'
|
||||
import type { DiffFileEntry } from 'utils/types'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import { useCurrentUser } from 'hooks/useCurrentUser'
|
||||
import { useConfirmAction } from 'hooks/useConfirmAction'
|
||||
import {
|
||||
CommentItem,
|
||||
DIFF2HTML_CONFIG,
|
||||
@ -19,7 +31,7 @@ import {
|
||||
renderCommentOppositePlaceHolder,
|
||||
ViewStyle
|
||||
} from './DiffViewerUtils'
|
||||
import { CommentBox } from './CommentBox/CommentBox'
|
||||
import { CommentBox } from '../CommentBox/CommentBox'
|
||||
import css from './DiffViewer.module.scss'
|
||||
|
||||
interface DiffViewerProps {
|
||||
@ -46,6 +58,17 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
|
||||
const { ref: inViewRef, inView } = useInView({ rootMargin: '100px 0px' })
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const currentUser = useCurrentUser()
|
||||
const executeDeleteComentConfirmation = useConfirmAction({
|
||||
title: getString('delete'),
|
||||
intent: Intent.DANGER,
|
||||
message: <Text>{getString('deleteCommentConfirm')}</Text>,
|
||||
action: async ({ commentEntry, onSuccess = noop }) => {
|
||||
// TODO: Delete comment
|
||||
console.log('Deleting...', commentEntry)
|
||||
onSuccess('Delete ', commentEntry)
|
||||
}
|
||||
})
|
||||
|
||||
const [comments, setComments] = useState<CommentItem[]>(
|
||||
!index
|
||||
? [
|
||||
@ -54,13 +77,19 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
|
||||
right: true,
|
||||
height: 0,
|
||||
lineNumber: 11,
|
||||
contents: [
|
||||
commentsThread: [
|
||||
`Logs will looks similar to\n\n<img width="1494" alt="image" src="https://user-images.githubusercontent.com/98799615/207994246-19ce9eb2-604f-4226-9a3c-6f4125d3b7cc.png">\n\n\ngitrpc logs using the \`ctx\` will have the following annotations:\n- \`grpc.service=rpc.ReferenceService\`\n- \`grpc.method=CreateBranch\`\n- \`grpc.peer=127.0.0.1:49364\`\n- \`grpc.request_id=cedrl6p1eqltblt13mgg\``,
|
||||
// `it seems we don't actually do anything with the explicit error type other than calling .Error(), which technically we could do on the original err object too? unless I'm missing something, could we then use errors.Is instead? (would avoid the extra var definitions at the top)`,
|
||||
//`If error is not converted then it will be detailed error: in BranchDelete: Branch doesn't exists. What we want is human readable error: Branch 'name' doesn't exists.`,
|
||||
// `* GitRPC isolated errors, bcoz this will be probably separate repo in future and we dont want every where to include grpc status codes in our main app\n* Errors are explicit for repsonses based on error passing by types`,
|
||||
`> global ctx in wire will kill all routines, right? is this affect middlewares and interceptors? because requests should finish they work, right?\n\nI've changed the code now to pass the config directly instead of the systemstore and context, to avoid confusion (what we discussed yesterday - I remove systemstore itself another time).`
|
||||
]
|
||||
].map(content => ({
|
||||
id: '0',
|
||||
author: 'Tan Nhu',
|
||||
created: '2022-12-21',
|
||||
updated: '2022-12-21',
|
||||
content
|
||||
}))
|
||||
}
|
||||
]
|
||||
: []
|
||||
@ -168,7 +197,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
|
||||
right: false,
|
||||
lineNumber: 0,
|
||||
height: 0,
|
||||
contents: []
|
||||
commentsThread: []
|
||||
}
|
||||
|
||||
if (targetButton && annotatedLineRow) {
|
||||
@ -244,18 +273,14 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
|
||||
ReactDOM.unmountComponentAtNode(element as HTMLDivElement)
|
||||
ReactDOM.render(
|
||||
<CommentBox
|
||||
contents={comment.contents}
|
||||
commentsThread={comment.commentsThread}
|
||||
getString={getString}
|
||||
width={isSideBySide ? 'calc(100vw / 2 - 163px)' : undefined}
|
||||
onHeightChange={boxHeight => {
|
||||
if (typeof boxHeight === 'string') {
|
||||
element.style.height = boxHeight
|
||||
} else {
|
||||
if (comment.height !== boxHeight) {
|
||||
comment.height = boxHeight
|
||||
element.style.height = `${boxHeight}px`
|
||||
setTimeout(() => setComments([...commentsRef.current]), 0)
|
||||
}
|
||||
if (comment.height !== boxHeight) {
|
||||
comment.height = boxHeight
|
||||
// element.style.height = `${boxHeight}px`
|
||||
setTimeout(() => setComments([...commentsRef.current]), 0)
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
@ -269,6 +294,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
|
||||
setTimeout(() => setComments(commentsRef.current.filter(item => item !== comment)), 0)
|
||||
}}
|
||||
currentUser={currentUser}
|
||||
executeDeleteComent={executeDeleteComentConfirmation}
|
||||
/>,
|
||||
element
|
||||
)
|
||||
@ -278,13 +304,14 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
|
||||
renderCommentOppositePlaceHolder(comment, lineInfo.oppositeRowElement)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Comment no longer has UI relevant anchors to be rendered
|
||||
console.info('Comment is discarded due to no UI relevant anchors', { comment, lineInfo })
|
||||
}
|
||||
// Comment no longer has UI relevant anchors to be rendered
|
||||
// else {
|
||||
// console.info('Comment is discarded due to no UI relevant anchors', { comment, lineInfo })
|
||||
// }
|
||||
})
|
||||
},
|
||||
[comments, viewStyle, getString, currentUser]
|
||||
[comments, viewStyle, getString, currentUser, executeDeleteComentConfirmation]
|
||||
)
|
||||
|
||||
useEffect(function cleanUpCommentBoxRendering() {
|
||||
|
@ -2,6 +2,7 @@ import type * as Diff2Html from 'diff2html'
|
||||
import HoganJsUtils from 'diff2html/lib/hoganjs-utils'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import 'diff2html/bundles/css/diff2html.min.css'
|
||||
import type { CommentThreadEntry } from 'utils/types'
|
||||
|
||||
export enum ViewStyle {
|
||||
SIDE_BY_SIDE = 'side-by-side',
|
||||
@ -17,7 +18,7 @@ export interface CommentItem {
|
||||
right: boolean
|
||||
lineNumber: number
|
||||
height: number
|
||||
contents: string[]
|
||||
commentsThread: CommentThreadEntry[]
|
||||
}
|
||||
|
||||
export const DIFF2HTML_CONFIG = {
|
||||
|
@ -0,0 +1,104 @@
|
||||
.main {
|
||||
--color-border: var(--grey-200);
|
||||
|
||||
background-color: var(--grey-50);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&:focus-within {
|
||||
--color-border: var(--primary-7);
|
||||
}
|
||||
|
||||
&.withPreview {
|
||||
--color-border: var(--grey-200);
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
:global {
|
||||
.bp3-tab-list {
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 25px;
|
||||
z-index: 2;
|
||||
padding: 0;
|
||||
|
||||
.bp3-tab {
|
||||
margin: 0 !important;
|
||||
padding: 6px 10px !important;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&[aria-selected='true'] {
|
||||
background-color: var(--white) !important;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
&:not([aria-selected='true']) {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-tab-indicator-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
background-color: var(--white);
|
||||
margin-top: 28px !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.markdownEditor {
|
||||
:global {
|
||||
.md-editor {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.md-editor-content {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.md-editor-toolbar-warp,
|
||||
.md-editor-toolbar-warp:not(.md-editor-toolbar-bottom) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.md-editor-preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-editor-toolbar {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.md-editor-content {
|
||||
padding: var(--spacing-small);
|
||||
background-color: var(--white);
|
||||
|
||||
.ͼ1.cm-editor.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
min-height: 46px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss.d.ts
vendored
Normal file
10
web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
// this is an auto-generated file
|
||||
declare const styles: {
|
||||
readonly main: string
|
||||
readonly withPreview: string
|
||||
readonly tabs: string
|
||||
readonly preview: string
|
||||
readonly markdownEditor: string
|
||||
}
|
||||
export default styles
|
@ -0,0 +1,110 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Container, ButtonVariation, Layout } from '@harness/uicore'
|
||||
import MarkdownEditor from '@uiw/react-markdown-editor'
|
||||
import { Tab, Tabs } from '@blueprintjs/core'
|
||||
import { indentWithTab } from '@codemirror/commands'
|
||||
import cx from 'classnames'
|
||||
import { keymap, EditorView } from '@codemirror/view'
|
||||
import { noop } from 'lodash-es'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import 'diff2html/bundles/css/diff2html.min.css'
|
||||
import type { IToolBarProps } from '@uiw/react-markdown-editor/cjs/components/ToolBar'
|
||||
import css from './MarkdownEditorWithPreview.module.scss'
|
||||
|
||||
interface MarkdownEditorWithPreviewProps {
|
||||
placeHolder: string
|
||||
value: string
|
||||
editTabText: string
|
||||
previewTabText: string
|
||||
cancelButtonText: string
|
||||
saveButtonText: string
|
||||
onChange?: (value: string, original: string) => void
|
||||
onSave: (value: string, original: string) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function MarkdownEditorWithPreview({
|
||||
placeHolder,
|
||||
value,
|
||||
editTabText,
|
||||
previewTabText,
|
||||
saveButtonText,
|
||||
cancelButtonText,
|
||||
onChange = noop,
|
||||
onSave,
|
||||
onCancel
|
||||
}: MarkdownEditorWithPreviewProps) {
|
||||
const [original] = useState(value)
|
||||
const [selectedTab, setSelectedTab] = useState<MarkdownEditorTab>(MarkdownEditorTab.WRITE)
|
||||
const [val, setVal] = useState(value)
|
||||
|
||||
return (
|
||||
<Container className={cx(css.main, selectedTab === MarkdownEditorTab.PREVIEW ? css.withPreview : '')}>
|
||||
<Layout.Vertical spacing="large">
|
||||
<Tabs
|
||||
className={css.tabs}
|
||||
defaultSelectedTabId={selectedTab}
|
||||
onChange={tabId => setSelectedTab(tabId as MarkdownEditorTab)}>
|
||||
<Tab
|
||||
id={MarkdownEditorTab.WRITE}
|
||||
title={editTabText}
|
||||
panel={
|
||||
<Container className={css.markdownEditor}>
|
||||
<MarkdownEditor
|
||||
value={val}
|
||||
visible={false}
|
||||
placeholder={placeHolder}
|
||||
theme="light"
|
||||
indentWithTab={false}
|
||||
autoFocus
|
||||
// TODO: Customize toolbars to show tooltip.
|
||||
// @see https://github.com/uiwjs/react-markdown-editor#custom-toolbars
|
||||
toolbars={toolbars}
|
||||
toolbarsMode={[]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
foldGutter: false,
|
||||
highlightActiveLine: false
|
||||
}}
|
||||
extensions={[keymap.of([indentWithTab]), EditorView.lineWrapping]}
|
||||
onChange={(_value, _viewUpdate) => {
|
||||
setVal(_value)
|
||||
onChange(_value, original)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
id={MarkdownEditorTab.PREVIEW}
|
||||
disabled={!value}
|
||||
title={previewTabText}
|
||||
panel={
|
||||
<Container padding="large" className={css.preview}>
|
||||
<MarkdownEditor.Markdown source={val} />
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small">
|
||||
<Button
|
||||
disabled={!(val || '').trim() || val === original}
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
onClick={() => onSave(val, original)}
|
||||
text={saveButtonText}
|
||||
/>
|
||||
<Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={cancelButtonText} />
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const toolbars: IToolBarProps['toolbars'] = ['bold', 'strike', 'olist', 'ulist', 'todo', 'link', 'image']
|
||||
|
||||
enum MarkdownEditorTab {
|
||||
WRITE = 'write',
|
||||
PREVIEW = 'preview'
|
||||
}
|
@ -7,14 +7,21 @@ export const MenuDivider = '-' as const
|
||||
|
||||
export interface OptionsMenuButtonProps extends ButtonProps {
|
||||
items: Array<React.ComponentProps<typeof Menu.Item> | '-'>
|
||||
isDark?: boolean
|
||||
icon?: ButtonProps['icon']
|
||||
}
|
||||
|
||||
export const OptionsMenuButton = ({ items, ...props }: OptionsMenuButtonProps): ReactElement => {
|
||||
export const OptionsMenuButton = ({
|
||||
items,
|
||||
icon = 'code-more',
|
||||
isDark = true,
|
||||
...props
|
||||
}: OptionsMenuButtonProps): ReactElement => {
|
||||
return (
|
||||
<Button
|
||||
minimal
|
||||
icon="code-more"
|
||||
tooltipProps={{ isDark: true, interactionKind: 'click', hasBackdrop: true } as PopoverProps}
|
||||
icon={icon}
|
||||
tooltipProps={{ isDark, interactionKind: 'click', hasBackdrop: true } as PopoverProps}
|
||||
tooltip={
|
||||
<Menu style={{ minWidth: 'unset' }}>
|
||||
{items.map(
|
||||
|
@ -67,6 +67,7 @@ export interface StringsMap {
|
||||
delete: string
|
||||
deleteBranch: string
|
||||
deleteBranchConfirm: string
|
||||
deleteCommentConfirm: string
|
||||
deleteFile: string
|
||||
deleteNotAllowed: string
|
||||
deployKeys: string
|
||||
@ -145,12 +146,14 @@ export interface StringsMap {
|
||||
prefixBase: string
|
||||
prefixCompare: string
|
||||
prev: string
|
||||
preview: string
|
||||
private: string
|
||||
public: string
|
||||
pullMustBeMadeFromBranches: string
|
||||
pullRequestEmpty: string
|
||||
pullRequests: string
|
||||
pushEvent: string
|
||||
quote: string
|
||||
rejected: string
|
||||
renameFile: string
|
||||
replyHere: string
|
||||
@ -163,6 +166,7 @@ export interface StringsMap {
|
||||
'repos.updated': string
|
||||
repositories: string
|
||||
samplePayloadUrl: string
|
||||
save: string
|
||||
scanAlerts: string
|
||||
scrollToTop: string
|
||||
search: string
|
||||
@ -191,6 +195,7 @@ export interface StringsMap {
|
||||
webhookEventsLabel: string
|
||||
webhookListingContent: string
|
||||
webhooks: string
|
||||
write: string
|
||||
yourBranches: string
|
||||
yours: string
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { Intent } from '@blueprintjs/core'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useConfirmationDialog } from '@harness/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
|
||||
@ -15,12 +16,13 @@ export interface UseConfirmActionDialogProps {
|
||||
title?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
action: () => void
|
||||
action: (params?: Unknown) => void
|
||||
}
|
||||
|
||||
export const useConfirmAction = (props: UseConfirmActionDialogProps) => {
|
||||
const { title, message, confirmText, cancelText, intent, action } = props
|
||||
const { getString } = useStrings()
|
||||
const [params, setParams] = useState<Unknown>()
|
||||
const { openDialog } = useConfirmationDialog({
|
||||
intent,
|
||||
titleText: title || getString('confirmation'),
|
||||
@ -30,10 +32,17 @@ export const useConfirmAction = (props: UseConfirmActionDialogProps) => {
|
||||
buttonIntent: intent || Intent.DANGER,
|
||||
onCloseDialog: async (isConfirmed: boolean) => {
|
||||
if (isConfirmed) {
|
||||
action()
|
||||
action(params)
|
||||
}
|
||||
}
|
||||
})
|
||||
const confirm = useCallback(
|
||||
(_params?: Unknown) => {
|
||||
setParams(_params)
|
||||
openDialog()
|
||||
},
|
||||
[openDialog]
|
||||
)
|
||||
|
||||
return openDialog
|
||||
return confirm
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
export interface CurrentUser {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
import type { UserProfile } from 'utils/types'
|
||||
|
||||
export function useCurrentUser(): CurrentUser {
|
||||
export function useCurrentUser(): UserProfile {
|
||||
// TODO: Implement this hook to get current user that works
|
||||
// in both standalone and embedded mode
|
||||
return {
|
||||
id: '0',
|
||||
email: 'admin@harness.io',
|
||||
name: 'Admin'
|
||||
}
|
||||
|
@ -7,10 +7,41 @@ export enum UserPreference {
|
||||
|
||||
export function useUserPreference<T = string>(key: UserPreference, defaultValue: T): [T, (val: T) => void] {
|
||||
const prefKey = `CODE_MODULE_USER_PREFERENCE_${key}`
|
||||
const [preference, setPreference] = useState<T>(localStorage[prefKey] || (defaultValue as T))
|
||||
const convert = useCallback(
|
||||
val => {
|
||||
if (val === undefined || val === null) {
|
||||
return val
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
return val === 'true'
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
return Number(val)
|
||||
}
|
||||
|
||||
if (Array.isArray(defaultValue) || typeof defaultValue === 'object') {
|
||||
try {
|
||||
return JSON.parse(val)
|
||||
} catch (exception) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to parse object', val)
|
||||
}
|
||||
}
|
||||
return val
|
||||
},
|
||||
[defaultValue]
|
||||
)
|
||||
const [preference, setPreference] = useState<T>(convert(localStorage[prefKey]) || (defaultValue as T))
|
||||
const savePreference = useCallback(
|
||||
(val: T) => {
|
||||
localStorage[prefKey] = val
|
||||
try {
|
||||
localStorage[prefKey] = JSON.stringify(val)
|
||||
} catch (exception) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to stringify object', val)
|
||||
}
|
||||
setPreference(val)
|
||||
},
|
||||
[prefKey]
|
||||
|
@ -192,3 +192,8 @@ addComment: Add comment
|
||||
replyHere: Reply here...
|
||||
leaveAComment: Leave a comment here...
|
||||
lineBreaks: Line Breaks
|
||||
quote: Quote
|
||||
deleteCommentConfirm: Are you sure you want to delete this comment?
|
||||
write: Write
|
||||
preview: Preview
|
||||
save: Save
|
||||
|
@ -17,16 +17,16 @@ import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import type { DiffFileEntry } from 'utils/types'
|
||||
import { DIFF2HTML_CONFIG, ViewStyle } from 'components/DiffViewer/DiffViewerUtils'
|
||||
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
|
||||
import { FilesChangedDropdown } from './FilesChangedDropdown'
|
||||
import { ChangesDropdown } from './ChangesDropdown'
|
||||
import { DiffViewConfiguration } from './DiffViewConfiguration'
|
||||
import css from './FilesChanged.module.scss'
|
||||
import css from './Changes.module.scss'
|
||||
import diffExample from 'raw-loader!./example.diff'
|
||||
|
||||
const STICKY_TOP_POSITION = 64
|
||||
const STICKY_HEADER_HEIGHT = 150
|
||||
const diffViewerId = (collection: Unknown[]) => collection.filter(Boolean).join('::::')
|
||||
|
||||
export const FilesChanged: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = () => {
|
||||
export const Changes: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = () => {
|
||||
const { getString } = useStrings()
|
||||
const [viewStyle, setViewStyle] = useUserPreference(UserPreference.DIFF_VIEW_STYLE, ViewStyle.SIDE_BY_SIDE)
|
||||
const [lineBreaks, setLineBreaks] = useUserPreference(UserPreference.DIFF_LINE_BREAKS, false)
|
||||
@ -76,7 +76,7 @@ export const FilesChanged: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullReq
|
||||
<StringSubstitute
|
||||
str={getString('pr.diffStatsLabel')}
|
||||
vars={{
|
||||
changedFilesLink: <FilesChangedDropdown diffs={diffs} />,
|
||||
changedFilesLink: <ChangesDropdown diffs={diffs} />,
|
||||
addedLines: formatNumber(diffStats.addedLines),
|
||||
deletedLines: formatNumber(diffStats.deletedLines),
|
||||
configuration: (
|
@ -8,12 +8,12 @@ import { CodeIcon } from 'utils/GitUtils'
|
||||
import { waitUntil } from 'utils/Utils'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import type { DiffFileEntry } from 'utils/types'
|
||||
import css from './FilesChangedDropdown.module.scss'
|
||||
import css from './ChangesDropdown.module.scss'
|
||||
// import { TreeExample } from 'pages/Repository/RepositoryTree/TreeExample'
|
||||
|
||||
const STICKY_TOP_POSITION = 64
|
||||
|
||||
export const FilesChangedDropdown: React.FC<{ diffs: DiffFileEntry[] }> = ({ diffs }) => {
|
||||
export const ChangesDropdown: React.FC<{ diffs: DiffFileEntry[] }> = ({ diffs }) => {
|
||||
const { getString } = useStrings()
|
||||
|
||||
return (
|
@ -8,12 +8,12 @@ import { useStrings } from 'framework/strings'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { CodeIcon } from 'utils/GitUtils'
|
||||
import type { TypesPullReq } from 'services/code'
|
||||
import { PullRequestMetaLine } from './PullRequestMetaLine'
|
||||
import { PullRequestConversation } from './PullRequestConversation/PullRequestConversation'
|
||||
import { FilesChanged } from './FilesChanged/FilesChanged'
|
||||
import { Changes } from './Changes/Changes'
|
||||
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
||||
import css from './PullRequest.module.scss'
|
||||
import type { TypesPullReq } from 'services/code'
|
||||
|
||||
enum PullRequestSection {
|
||||
CONVERSATION = 'conversation',
|
||||
@ -97,7 +97,7 @@ export default function PullRequest() {
|
||||
{
|
||||
id: PullRequestSection.FILES_CHANGED,
|
||||
title: <TabTitle icon={CodeIcon.File} title={getString('filesChanged')} count={20} />,
|
||||
panel: <FilesChanged repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||
panel: <Changes repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
@ -5,10 +5,10 @@ import ReactTimeago from 'react-timeago'
|
||||
import { CodeIcon, GitInfoProps, PullRequestState } from 'utils/GitUtils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { TypesPullReq } from 'services/code'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
|
||||
import css from './PullRequestMetaLine.module.scss'
|
||||
import type { TypesPullReq } from 'services/code'
|
||||
|
||||
export const PullRequestMetaLine: React.FC<TypesPullReq & Pick<GitInfoProps, 'repoMetadata'>> = ({
|
||||
repoMetadata,
|
||||
@ -26,13 +26,13 @@ export const PullRequestMetaLine: React.FC<TypesPullReq & Pick<GitInfoProps, 're
|
||||
number: <strong>5</strong>, // TODO: No data from backend now
|
||||
target: (
|
||||
<GitRefLink
|
||||
text={target_branch!}
|
||||
text={target_branch as string}
|
||||
url={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef: target_branch })}
|
||||
/>
|
||||
),
|
||||
source: (
|
||||
<GitRefLink
|
||||
text={source_branch!}
|
||||
text={source_branch as string}
|
||||
url={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef: source_branch })}
|
||||
/>
|
||||
)
|
||||
@ -47,7 +47,7 @@ export const PullRequestMetaLine: React.FC<TypesPullReq & Pick<GitInfoProps, 're
|
||||
</Text>
|
||||
<PipeSeparator height={9} />
|
||||
<Text inline className={cx(css.metaline, css.time)}>
|
||||
<ReactTimeago date={updated!} />
|
||||
<ReactTimeago date={updated as number} />
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
|
@ -4,3 +4,26 @@ export interface DiffFileEntry extends DiffFile {
|
||||
containerId: string
|
||||
contentId: string
|
||||
}
|
||||
|
||||
// TODO: Use proper type when API supports it
|
||||
export interface CommentThreadEntry {
|
||||
id: string
|
||||
author: string
|
||||
created: string
|
||||
updated: string
|
||||
content: string
|
||||
emoji?: EmojiInfo[]
|
||||
}
|
||||
|
||||
// TODO: Use proper type when API supports it
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
name: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
// TODO: Use proper type when API supports it
|
||||
export interface EmojiInfo {
|
||||
name: string
|
||||
by: UserProfile[]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user