diff --git a/web/src/components/CommentBox/CommentBox.module.scss b/web/src/components/CommentBox/CommentBox.module.scss new file mode 100644 index 000000000..2be595b63 --- /dev/null +++ b/web/src/components/CommentBox/CommentBox.module.scss @@ -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; +} diff --git a/web/src/components/DiffViewer/CommentBox/CommentBox.module.scss.d.ts b/web/src/components/CommentBox/CommentBox.module.scss.d.ts similarity index 68% rename from web/src/components/DiffViewer/CommentBox/CommentBox.module.scss.d.ts rename to web/src/components/CommentBox/CommentBox.module.scss.d.ts index cb99e6e38..50b1420b8 100644 --- a/web/src/components/DiffViewer/CommentBox/CommentBox.module.scss.d.ts +++ b/web/src/components/CommentBox/CommentBox.module.scss.d.ts @@ -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 diff --git a/web/src/components/CommentBox/CommentBox.tsx b/web/src/components/CommentBox/CommentBox.tsx new file mode 100644 index 000000000..0defb597f --- /dev/null +++ b/web/src/components/CommentBox/CommentBox.tsx @@ -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 = ({ + 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(_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 ( + + + + + + {(showReplyPlaceHolder && ( + + + + + + + )) || ( + + { + setCommentsThread([ + ...commentsThread, + { + id: '0', + author: currentUser.name, + created: Date.now().toString(), + updated: Date.now().toString(), + content: value + } + ]) + setMarkdown('') + setShowReplyPlaceHolder(true) + }} + onCancel={_onCancel} + /> + + )} + + + + ) +} + +interface CommentsThreadProps + extends Pick { + onQuote: (content: string) => void +} + +const CommentsThread: React.FC = ({ + getString, + currentUser, + onQuote, + commentsThread = [], + executeDeleteComent +}) => { + const [editIndexes, setEditIndexes] = useState>({}) + + return commentsThread.length ? ( + + {commentsThread.map((commentEntry, index) => { + const isLastItem = index === commentsThread.length - 1 + + return ( + + + + + {commentEntry.author} + + + + + + + setEditIndexes({ ...editIndexes, ...{ [index]: true } }) + }, + { + text: getString('quote'), + onClick: () => { + onQuote(commentEntry.content) + } + }, + MenuDivider, + { + text: ( + + {getString('delete')} + + ), + onClick: () => + executeDeleteComent({ + commentEntry, + onSuccess: () => { + alert('success') + } + }), + className: css.deleteMenuItem + } + ]} + /> + + } + hideGutter={isLastItem}> + + {editIndexes[index] ? ( + + {}} + onCancel={() => { + delete editIndexes[index] + setEditIndexes({ ...editIndexes }) + }} + /> + + ) : ( + + )} + + + ) + })} + + ) : null +} diff --git a/web/src/components/DiffViewer/CommentBox/CommentBox.module.scss b/web/src/components/DiffViewer/CommentBox/CommentBox.module.scss deleted file mode 100644 index e5b8c0437..000000000 --- a/web/src/components/DiffViewer/CommentBox/CommentBox.module.scss +++ /dev/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; - } - } -} diff --git a/web/src/components/DiffViewer/CommentBox/CommentBox.tsx b/web/src/components/DiffViewer/CommentBox/CommentBox.tsx deleted file mode 100644 index 99bd8d930..000000000 --- a/web/src/components/DiffViewer/CommentBox/CommentBox.tsx +++ /dev/null @@ -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 = ({ - getString, - onHeightChange, - onCancel, - width, - contents: _contents = [], - currentUser -}) => { - const [contents, setContents] = useState(_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 ( - - - - {!!contents.length && ( - - {contents.map((content, index) => { - const isLastItem = index === contents.length - 1 - return ( - - - - - {currentUser.name} - - - - - - } - hideGutter={isLastItem}> - - - - - ) - })} - - )} - - {(showReplyPlaceHolder && ( - - - - - - - )) || ( - - - onHeightChange('auto')} key="horizontal"> - - - { - onHeightChange('auto') - setMarkdown(value) - }} - /> - - } - /> - - - - } - /> - - - -