Resolve Editor slowness by replacing Editor React wrapper lib

This commit is contained in:
“tan-nhu” 2023-04-24 12:41:42 -07:00
parent abd3110a5b
commit 1873bf57d6
21 changed files with 2211 additions and 1717 deletions

View File

@ -37,6 +37,11 @@
"@blueprintjs/core": "3.26.1", "@blueprintjs/core": "3.26.1",
"@blueprintjs/datetime": "3.13.0", "@blueprintjs/datetime": "3.13.0",
"@blueprintjs/select": "3.12.3", "@blueprintjs/select": "3.12.3",
"@codemirror/commands": "^6.2.3",
"@codemirror/lang-markdown": "^6.1.1",
"@codemirror/language-data": "^6.3.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.9.6",
"@harness/design-system": "1.4.0", "@harness/design-system": "1.4.0",
"@harness/icons": "1.110.2", "@harness/icons": "1.110.2",
"@harness/ng-tooltip": ">=1.31.25", "@harness/ng-tooltip": ">=1.31.25",
@ -47,7 +52,7 @@
"@uiw/codemirror-extensions-color": "^4.19.9", "@uiw/codemirror-extensions-color": "^4.19.9",
"@uiw/codemirror-extensions-hyper-link": "^4.19.9", "@uiw/codemirror-extensions-hyper-link": "^4.19.9",
"@uiw/codemirror-themes-all": "^4.19.9", "@uiw/codemirror-themes-all": "^4.19.9",
"@uiw/react-markdown-editor": "^5.10.1", "@uiw/react-markdown-preview": "^4.1.12",
"anser": "2.0.1", "anser": "2.0.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"clipboard-copy": "^3.1.0", "clipboard-copy": "^3.1.0",

View File

@ -5,11 +5,6 @@
*/ */
.main { .main {
--code-editor-font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
--code-editor-font-size: 13px;
--code-editor-border-color: var(--grey-200);
:global { :global {
.Resizer { .Resizer {
background-color: var(--grey-300); background-color: var(--grey-300);

View File

@ -63,8 +63,7 @@
} }
.editCommentContainer { .editCommentContainer {
background-color: var(--grey-50) !important; padding: var(--spacing-small) !important;
padding: var(--spacing-large) !important;
border-radius: var(--box-radius); border-radius: var(--box-radius);
} }

View File

@ -1,5 +1,6 @@
import React, { useCallback, useRef, useState } from 'react' import React, { useCallback, useRef, useState } from 'react'
import { useResizeDetector } from 'react-resize-detector' import { useResizeDetector } from 'react-resize-detector'
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'
import { Container, Layout, Avatar, TextInput, Text, Color, FontVariation, FlexExpander } from '@harness/uicore' import { Container, Layout, Avatar, TextInput, Text, Color, FontVariation, FlexExpander } from '@harness/uicore'
import cx from 'classnames' import cx from 'classnames'
@ -10,10 +11,7 @@ import { ThreadSection } from 'components/ThreadSection/ThreadSection'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator' import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
MarkdownEditorWithPreview,
MarkdownEditorWithPreviewResetProps
} from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer' import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
import css from './CommentBox.module.scss' import css from './CommentBox.module.scss'
@ -106,7 +104,7 @@ export const CommentBox = <T = unknown,>({
.join(CRLF) .join(CRLF)
) )
}, []) }, [])
const editorRef = useRef<MarkdownEditorWithPreviewResetProps>() const viewRef = useRef<EditorView>()
return ( return (
<Container <Container
@ -144,11 +142,10 @@ export const CommentBox = <T = unknown,>({
</Container> </Container>
</Truthy> </Truthy>
<Falsy> <Falsy>
<Container <Container className={cx(css.newCommentContainer, { [css.hasThread]: !!comments.length })}>
padding="xlarge"
className={cx(css.newCommentContainer, { [css.hasThread]: !!comments.length })}>
<MarkdownEditorWithPreview <MarkdownEditorWithPreview
editorRef={editorRef as React.MutableRefObject<MarkdownEditorWithPreviewResetProps>} viewRef={viewRef}
noBorder
i18n={{ i18n={{
placeHolder: getString(comments.length ? 'replyHere' : 'leaveAComment'), placeHolder: getString(comments.length ? 'replyHere' : 'leaveAComment'),
tabEdit: getString('write'), tabEdit: getString('write'),
@ -170,7 +167,13 @@ export const CommentBox = <T = unknown,>({
setMarkdown('') setMarkdown('')
if (resetOnSave) { if (resetOnSave) {
editorRef.current?.resetEditor?.() viewRef.current?.dispatch({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: ''
}
})
} else { } else {
setComments([...comments, updatedItem as CommentItem<T>]) setComments([...comments, updatedItem as CommentItem<T>])
setShowReplyPlaceHolder(true) setShowReplyPlaceHolder(true)

View File

@ -177,7 +177,7 @@ export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
disabled={disableBranchCreation} disabled={disableBranchCreation}
label="" label=""
onChange={e => { onChange={e => {
setTargetBranchOption(get(e.target, 'defaultValue')) setTargetBranchOption(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption)
}} }}
items={[ items={[
{ {

View File

@ -0,0 +1,30 @@
@import 'src/utils/utils';
.editor {
:global {
.cm-editor {
outline: none;
border: 1px solid var(--grey-200);
border-radius: 4px;
min-height: var(--editor-min-height, 60px);
max-height: var(--editor-max-height, 600px);
&.cm-focused {
border-color: var(--primary-7);
outline: none;
}
.cm-scroller {
overflow: auto;
padding: var(--spacing-small);
.cm-line {
&,
* {
@include markdown-font;
}
}
}
}
}
}

View File

@ -0,0 +1,6 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly editor: string
}
export default styles

View File

@ -2,41 +2,64 @@ import React, { useEffect, useMemo, useRef } from 'react'
import { Container } from '@harness/uicore' import { Container } from '@harness/uicore'
import { LanguageDescription } from '@codemirror/language' import { LanguageDescription } from '@codemirror/language'
import { indentWithTab } from '@codemirror/commands' import { indentWithTab } from '@codemirror/commands'
import cx from 'classnames'
import type { ViewUpdate } from '@codemirror/view' import type { ViewUpdate } from '@codemirror/view'
import type { Text } from '@codemirror/state'
import { languages } from '@codemirror/language-data' import { languages } from '@codemirror/language-data'
import { EditorView, keymap } from '@codemirror/view' import { markdown } from '@codemirror/lang-markdown'
import { noop } from 'lodash-es' import { EditorView, keymap, placeholder as placeholderExtension } from '@codemirror/view'
import { Compartment, EditorState, Extension } from '@codemirror/state' import { Compartment, EditorState, Extension } from '@codemirror/state'
import { color } from '@uiw/codemirror-extensions-color' import { color } from '@uiw/codemirror-extensions-color'
import { hyperLink } from '@uiw/codemirror-extensions-hyper-link' import { hyperLink } from '@uiw/codemirror-extensions-hyper-link'
import { githubLight as theme } from '@uiw/codemirror-themes-all' import { githubLight as theme } from '@uiw/codemirror-themes-all'
import css from './Editor.module.scss'
interface EditorProps { export interface EditorProps {
filename: string content: string
source: string filename?: string
onViewUpdate?: (update: ViewUpdate) => void forMarkdown?: boolean
placeholder?: string
readonly?: boolean readonly?: boolean
autoFocus?: boolean
className?: string className?: string
extensions?: Extension extensions?: Extension
maxHeight?: string | number
viewRef?: React.MutableRefObject<EditorView | undefined> viewRef?: React.MutableRefObject<EditorView | undefined>
setDirty?: React.Dispatch<React.SetStateAction<boolean>>
onChange?: (doc: Text, viewUpdate: ViewUpdate) => void
onViewUpdate?: (viewUpdate: ViewUpdate) => void
} }
export const Editor = React.memo(function CodeMirrorReactEditor({ export const Editor = React.memo(function CodeMirrorReactEditor({
source, content,
filename, filename,
onViewUpdate = noop, forMarkdown,
placeholder,
readonly = false, readonly = false,
autoFocus,
className, className,
extensions = new Compartment().of([]), extensions = new Compartment().of([]),
viewRef maxHeight,
viewRef,
setDirty,
onChange,
onViewUpdate
}: EditorProps) { }: EditorProps) {
const view = useRef<EditorView>() const view = useRef<EditorView>()
const ref = useRef<HTMLDivElement>() const ref = useRef<HTMLDivElement>()
const languageConfig = useMemo(() => new Compartment(), []) const languageConfig = useMemo(() => new Compartment(), [])
const markdownLanguageSupport = useMemo(() => markdown({ codeLanguages: languages }), [])
const style = useMemo(() => {
if (maxHeight) {
return {
'--editor-max-height': Number.isInteger(maxHeight) ? `${maxHeight}px` : maxHeight
} as React.CSSProperties
}
}, [maxHeight])
useEffect(() => { useEffect(() => {
const editorView = new EditorView({ const editorView = new EditorView({
doc: source, doc: content,
extensions: [ extensions: [
extensions, extensions,
@ -45,11 +68,21 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
theme, theme,
EditorView.lineWrapping, EditorView.lineWrapping,
...(placeholder ? [placeholderExtension(placeholder)] : []),
keymap.of([indentWithTab]), keymap.of([indentWithTab]),
...(readonly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []), ...(readonly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
EditorView.updateListener.of(onViewUpdate), EditorView.updateListener.of(viewUpdate => {
setDirty?.(!cleanDoc.eq(viewUpdate.state.doc))
onViewUpdate?.(viewUpdate)
if (viewUpdate.docChanged) {
onChange?.(viewUpdate.state.doc, viewUpdate)
}
}),
/** /**
languageConfig is a compartment that defaults to an empty array (no language support) languageConfig is a compartment that defaults to an empty array (no language support)
@ -67,25 +100,35 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
viewRef.current = editorView viewRef.current = editorView
} }
const cleanDoc = editorView.state.doc
if (autoFocus) {
editorView.focus()
}
return () => { return () => {
editorView.destroy() editorView.destroy()
} }
}, []) // eslint-disable-line react-hooks/exhaustive-deps }, []) // eslint-disable-line react-hooks/exhaustive-deps
// Dynamically load language support based on filename // Dynamically load language support based on filename. Note that
// we need to configure languageSupport for Markdown separately to
// enable syntax highlighting for all code blocks (multi-lang).
useEffect(() => { useEffect(() => {
if (filename) { if (forMarkdown) {
languageDescriptionFrom(filename) view.current?.dispatch({ effects: languageConfig.reconfigure(markdownLanguageSupport) })
} else if (filename) {
LanguageDescription.matchFilename(languages, filename)
?.load() ?.load()
.then(languageSupport => { .then(languageSupport => {
view.current?.dispatch({ effects: languageConfig.reconfigure(languageSupport) }) view.current?.dispatch({
effects: languageConfig.reconfigure(
languageSupport.language.name === 'markdown' ? markdownLanguageSupport : languageSupport
)
})
}) })
} }
}, [filename, view, languageConfig]) }, [filename, forMarkdown, view, languageConfig, markdownLanguageSupport])
return <Container ref={ref} className={className} /> return <Container ref={ref} className={cx(css.editor, className)} style={style} />
}) })
function languageDescriptionFrom(filename: string) {
return LanguageDescription.matchFilename(languages, filename)
}

View File

@ -1,20 +1,24 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { ButtonGroup, ButtonVariation, Button, Container, Dialog, Carousel } from '@harness/uicore' import { ButtonGroup, ButtonVariation, Button, Container, Dialog, Carousel } from '@harness/uicore'
import { ZOOM_INC_DEC_LEVEL } from 'utils/Utils' import { ZOOM_INC_DEC_LEVEL } from 'utils/Utils'
import type { UseStringsReturn } from 'framework/strings'
import css from './ImageCarousel.module.scss' import css from './ImageCarousel.module.scss'
interface ImageCarouselProps { interface ImageCarouselProps {
isOpen: boolean isOpen: boolean
setIsOpen: (value: boolean) => void setIsOpen: (value: boolean) => void
setZoomLevel: (value: number) => void setZoomLevel: (value: number) => void
zoomLevel: number zoomLevel: number
imgEvent: string[] imgEvent: string[]
getString: UseStringsReturn['getString'] i18n: {
zoomIn: string
zoomOut: string
}
} }
const ImageCarousel = (props: ImageCarouselProps) => { const ImageCarousel = (props: ImageCarouselProps) => {
const { getString, isOpen, setIsOpen, setZoomLevel, zoomLevel, imgEvent } = props const { isOpen, setIsOpen, setZoomLevel, zoomLevel, imgEvent, i18n } = props
const [imgTitle, setImageTitle] = useState(imgEvent[0]) const [imgTitle, setImageTitle] = useState(imgEvent[0])
return ( return (
<Dialog <Dialog
portalClassName={css.portalContainer} portalClassName={css.portalContainer}
@ -55,7 +59,7 @@ const ImageCarousel = (props: ImageCarouselProps) => {
variation={ButtonVariation.TERTIARY} variation={ButtonVariation.TERTIARY}
icon="zoom-in" icon="zoom-in"
data-testid="zoomInButton" data-testid="zoomInButton"
tooltip={getString('zoomIn')} tooltip={i18n.zoomIn}
onClick={() => { onClick={() => {
Number(zoomLevel.toFixed(1)) < 2 && setZoomLevel(zoomLevel + ZOOM_INC_DEC_LEVEL) Number(zoomLevel.toFixed(1)) < 2 && setZoomLevel(zoomLevel + ZOOM_INC_DEC_LEVEL)
}} }}
@ -71,7 +75,7 @@ const ImageCarousel = (props: ImageCarouselProps) => {
variation={ButtonVariation.TERTIARY} variation={ButtonVariation.TERTIARY}
icon="zoom-out" icon="zoom-out"
data-testid="zoomOutButton" data-testid="zoomOutButton"
tooltip={getString('zoomOut')} tooltip={i18n.zoomOut}
onClick={() => { onClick={() => {
Number(zoomLevel.toFixed(1)) > 0.3 && setZoomLevel(zoomLevel - ZOOM_INC_DEC_LEVEL) Number(zoomLevel.toFixed(1)) > 0.3 && setZoomLevel(zoomLevel - ZOOM_INC_DEC_LEVEL)
}} }}
@ -83,3 +87,6 @@ const ImageCarousel = (props: ImageCarouselProps) => {
} }
export default ImageCarousel export default ImageCarousel
// TODO: Dialog does not have i18n context when mounted inside CommentBox/different React root
// Hence getString can't get proper translations

View File

@ -101,12 +101,12 @@
.markdownEditor { .markdownEditor {
:global { :global {
.cm-editor .cm-line { // .cm-editor .cm-line {
&, // &,
* { // * {
@include mono-font; // @include mono-font;
} // }
} // }
.md-editor { .md-editor {
background-color: transparent !important; background-color: transparent !important;
@ -153,3 +153,99 @@
} }
} }
} }
.container {
--tab-border-primary: rgba(176, 177, 195, 0.5);
--radius: 5px;
box-shadow: var(--elevation-2);
border-radius: var(--radius);
border: 1px solid var(--tab-border-primary);
border-bottom: none;
background-color: var(--white);
&.noBorder {
border: none;
box-shadow: none;
}
position: relative;
.tabs {
list-style: none;
margin: 0;
padding: 0;
display: flex;
background-color: var(--grey-100);
padding: 15px 15px 0;
background: var(--grey-50) !important;
border-bottom: 1px solid var(--tab-border-primary);
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
li {
a {
padding: 6px 15px;
display: block;
font-style: normal;
font-weight: 500;
font-size: 12px;
line-height: 18px;
text-decoration: none;
color: var(--grey-900);
position: relative;
border: 1px solid transparent;
border-bottom: none;
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
&::after {
content: '';
right: 0;
left: 0;
bottom: -1px;
height: 1px;
width: 100%;
position: absolute;
background-color: transparent;
}
&[aria-selected='true'] {
background-color: var(--white);
border-color: var(--tab-border-primary);
&::after {
background-color: var(--white);
}
}
}
}
}
.toolbar {
position: absolute;
top: 12px;
right: 16px;
margin: 0;
padding: 0;
display: flex;
}
.tabContent {
padding: var(--spacing-large);
background-color: var(--white);
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
.buttonsBar {
padding: 0 var(--spacing-large) var(--spacing-large);
background-color: var(--white);
border-radius: var(--radius);
}
.hidden {
display: none;
}
}

View File

@ -6,5 +6,11 @@ declare const styles: {
readonly tabs: string readonly tabs: string
readonly preview: string readonly preview: string
readonly markdownEditor: string readonly markdownEditor: string
readonly container: string
readonly noBorder: string
readonly toolbar: string
readonly tabContent: string
readonly buttonsBar: string
readonly hidden: string
} }
export default styles export default styles

View File

@ -1,22 +1,45 @@
import React, { useEffect, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Button, Container, ButtonVariation, Layout } from '@harness/uicore' import { Button, Container, ButtonVariation, Layout, Color, ButtonSize, IconName } 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 cx from 'classnames'
import { keymap, EditorView } from '@codemirror/view' import type { EditorView } from '@codemirror/view'
import { noop } from 'lodash-es' import { EditorSelection } from '@codemirror/state'
import type { IToolBarProps } from '@uiw/react-markdown-editor/cjs/components/ToolBar' import { Editor } from 'components/Editor/Editor'
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
import { useStrings } from 'framework/strings'
import css from './MarkdownEditorWithPreview.module.scss' import css from './MarkdownEditorWithPreview.module.scss'
export interface MarkdownEditorWithPreviewResetProps { enum MarkdownEditorTab {
resetEditor: () => void WRITE = 'write',
PREVIEW = 'preview'
} }
enum ToolbarAction {
HEADER = 'HEADER',
BOLD = 'BOLD',
ITALIC = 'ITALIC',
UNORDER_LIST = 'UNORDER_LIST',
CHECK_LIST = 'CHECK_LIST',
CODE_BLOCK = 'CODE_BLOCK'
}
interface ToolbarItem {
icon: IconName
action: ToolbarAction
}
const toolbar: ToolbarItem[] = [
{ icon: 'header', action: ToolbarAction.HEADER },
{ icon: 'bold', action: ToolbarAction.BOLD },
{ icon: 'italic', action: ToolbarAction.ITALIC },
{ icon: 'properties', action: ToolbarAction.UNORDER_LIST },
{ icon: 'form', action: ToolbarAction.CHECK_LIST },
{ icon: 'main-code-yaml', action: ToolbarAction.CODE_BLOCK }
]
interface MarkdownEditorWithPreviewProps { interface MarkdownEditorWithPreviewProps {
value: string value?: string
onChange?: (value: string, original: string) => void onChange?: (value: string) => void
onSave?: (value: string, original: string) => void onSave?: (value: string) => void
onCancel?: () => void onCancel?: () => void
i18n: { i18n: {
placeHolder: string placeHolder: string
@ -28,114 +51,220 @@ interface MarkdownEditorWithPreviewProps {
hideButtons?: boolean hideButtons?: boolean
hideCancel?: boolean hideCancel?: boolean
editorHeight?: string editorHeight?: string
maxEditorHeight?: string noBorder?: boolean
editorRef?: React.MutableRefObject<MarkdownEditorWithPreviewResetProps> viewRef?: React.MutableRefObject<EditorView | undefined>
} }
export function MarkdownEditorWithPreview({ export function MarkdownEditorWithPreview({
value, value = '',
onChange = noop, onChange,
onSave = noop, onSave,
onCancel = noop, onCancel,
i18n, i18n,
hideButtons, hideButtons,
hideCancel, hideCancel,
editorHeight, editorHeight,
maxEditorHeight, noBorder,
editorRef viewRef: viewRefProp
}: MarkdownEditorWithPreviewProps) { }: MarkdownEditorWithPreviewProps) {
const [original, setOriginal] = useState(value) const [selectedTab, setSelectedTab] = useState(MarkdownEditorTab.WRITE)
const [selectedTab, setSelectedTab] = useState<MarkdownEditorTab>(MarkdownEditorTab.WRITE) const viewRef = useRef<EditorView>()
const [val, setVal] = useState(value) const [dirty, setDirty] = useState(false)
const { getString } = useStrings()
const onToolbarAction = useCallback((action: ToolbarAction) => {
const view = viewRef.current
if (!view?.state) {
return
}
// Note: Part of this code is copied from @uiwjs/react-markdown-editor
// MIT License, Copyright (c) 2020 uiw
// @see https://github.dev/uiwjs/react-markdown-editor/blob/2d3f45079c79616b867ef03681a8ba9799169921/src/commands/header.tsx
switch (action) {
case ToolbarAction.HEADER: {
const lineInfo = view.state.doc.lineAt(view.state.selection.main.from)
let mark = '#'
const matchMark = lineInfo.text.match(/^#+/)
if (matchMark && matchMark[0]) {
const txt = matchMark[0]
if (txt.length < 6) {
mark = txt + '#'
}
}
if (mark.length > 6) {
mark = '#'
}
const title = lineInfo.text.replace(/^#+/, '')
view.dispatch({
changes: {
from: lineInfo.from,
to: lineInfo.to,
insert: `${mark} ${title}`
},
// selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to),
selection: { anchor: lineInfo.from + mark.length + 1 }
})
break
}
case ToolbarAction.BOLD: {
view.dispatch(
view.state.changeByRange(range => ({
changes: [
{ from: range.from, insert: '**' },
{ from: range.to, insert: '**' }
],
range: EditorSelection.range(range.from + 2, range.to + 2)
}))
)
break
}
case ToolbarAction.ITALIC: {
view.dispatch(
view.state.changeByRange(range => ({
changes: [
{ from: range.from, insert: '*' },
{ from: range.to, insert: '*' }
],
range: EditorSelection.range(range.from + 1, range.to + 1)
}))
)
break
}
case ToolbarAction.UNORDER_LIST: {
const lineInfo = view.state.doc.lineAt(view.state.selection.main.from)
let mark = '- '
const matchMark = lineInfo.text.match(/^-/)
if (matchMark && matchMark[0]) {
mark = ''
}
view.dispatch({
changes: {
from: lineInfo.from,
to: lineInfo.to,
insert: `${mark}${lineInfo.text}`
},
// selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to),
selection: { anchor: view.state.selection.main.from + mark.length }
})
break
}
case ToolbarAction.CHECK_LIST: {
const lineInfo = view.state.doc.lineAt(view.state.selection.main.from)
let mark = '- [ ] '
const matchMark = lineInfo.text.match(/^-\s\[\s\]\s/)
if (matchMark && matchMark[0]) {
mark = ''
}
view.dispatch({
changes: {
from: lineInfo.from,
to: lineInfo.to,
insert: `${mark}${lineInfo.text}`
},
// selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to),
selection: { anchor: view.state.selection.main.from + mark.length }
})
break
}
case ToolbarAction.CODE_BLOCK: {
const main = view.state.selection.main
const txt = view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to)
view.dispatch({
changes: {
from: main.from,
to: main.to,
insert: `\`\`\`tsx\n${txt}\n\`\`\``
},
selection: EditorSelection.range(main.from + 3, main.from + 6)
})
break
}
}
}, [])
useEffect(() => { useEffect(() => {
if (editorRef) { if (viewRefProp) {
editorRef.current = { viewRefProp.current = viewRef.current
resetEditor: () => {
setOriginal(value)
setVal(value)
}
}
editorRef.current.resetEditor()
} }
}, [editorRef]) // eslint-disable-line react-hooks/exhaustive-deps }, [viewRefProp, viewRef.current]) // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<Container className={cx(css.main, selectedTab === MarkdownEditorTab.PREVIEW ? css.withPreview : '')}> <Container className={cx(css.container, { [css.noBorder]: noBorder })}>
<Layout.Vertical spacing="large"> <ul className={css.tabs}>
<Tabs <li>
className={css.tabs} <a
defaultSelectedTabId={selectedTab} role="tab"
onChange={tabId => setSelectedTab(tabId as MarkdownEditorTab)}> tabIndex={0}
<Tab aria-selected={selectedTab === MarkdownEditorTab.WRITE}
id={MarkdownEditorTab.WRITE} onClick={() => setSelectedTab(MarkdownEditorTab.WRITE)}>
title={i18n.tabEdit} Write
panel={ </a>
<Container </li>
className={css.markdownEditor}
style={ <li>
{ <a
'--editor-height': editorHeight, role="tab"
'--max-editor-height': maxEditorHeight tabIndex={0}
} as React.CSSProperties aria-selected={selectedTab === MarkdownEditorTab.PREVIEW}
}> onClick={() => setSelectedTab(MarkdownEditorTab.PREVIEW)}>
<MarkdownEditor Preview
value={val} </a>
visible={false} </li>
placeholder={i18n.placeHolder} </ul>
theme="light" <Container className={css.toolbar}>
indentWithTab={false} {toolbar.map((item, index) => {
autoFocus return (
// TODO: Customize toolbars to show tooltip. <Button
// @see https://github.com/uiwjs/react-markdown-editor#custom-toolbars key={index}
toolbars={toolbars} size={ButtonSize.SMALL}
toolbarsMode={[]} variation={ButtonVariation.ICON}
basicSetup={{ icon={item.icon}
lineNumbers: false, withoutCurrentColor
foldGutter: false, iconProps={{ color: Color.PRIMARY_10, size: 14 }}
highlightActiveLine: false onClick={() => onToolbarAction(item.action)}
}} />
extensions={[keymap.of([indentWithTab]), EditorView.lineWrapping]} )
onChange={(_value, _viewUpdate) => { })}
setVal(_value) </Container>
onChange(_value, original) <Container className={css.tabContent}>
}} <Editor
/> forMarkdown
</Container> content={value || ''}
placeholder={i18n.placeHolder}
autoFocus
viewRef={viewRef}
setDirty={setDirty}
maxHeight={editorHeight}
className={selectedTab === MarkdownEditorTab.PREVIEW ? css.hidden : undefined}
onChange={doc => {
if (dirty) {
onChange?.(doc.toString())
} }
/> }}
<Tab />
id={MarkdownEditorTab.PREVIEW} {selectedTab === MarkdownEditorTab.PREVIEW && (
disabled={!val} <MarkdownViewer source={viewRef.current?.state.doc.toString() || ''} getString={getString} maxHeight={800} />
title={i18n.tabPreview}
panel={
<Container padding="large" className={css.preview}>
<MarkdownEditor.Markdown source={val} />
</Container>
}
/>
</Tabs>
{!hideButtons && (
<Container>
<Layout.Horizontal spacing="small">
<Button
disabled={!(val || '').trim() || val === original}
variation={ButtonVariation.PRIMARY}
onClick={() => onSave(val, original)}
text={i18n.save}
/>
{!hideCancel && <Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={i18n.cancel} />}
</Layout.Horizontal>
</Container>
)} )}
</Layout.Vertical> </Container>
{!hideButtons && (
<Container className={css.buttonsBar}>
<Layout.Horizontal spacing="small">
<Button
disabled={!dirty}
variation={ButtonVariation.PRIMARY}
onClick={() => onSave?.(viewRef.current?.state.doc.toString() || '')}
text={i18n.save}
/>
{!hideCancel && <Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={i18n.cancel} />}
</Layout.Horizontal>
</Container>
)}
</Container> </Container>
) )
} }
const toolbars: IToolBarProps['toolbars'] = ['bold', 'strike', 'olist', 'ulist', 'todo', 'link', 'image']
enum MarkdownEditorTab {
WRITE = 'write',
PREVIEW = 'preview'
}

View File

@ -1,8 +1,14 @@
.main { .main {
overflow: auto;
:global { :global {
.wmde-markdown { .wmde-markdown {
pre { pre {
position: relative; position: relative;
.code-line {
white-space: break-spaces;
}
} }
// Customize https://wangchujiang.com/rehype-video/ // Customize https://wangchujiang.com/rehype-video/

View File

@ -1,7 +1,8 @@
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { Container } from '@harness/uicore' import { Container } from '@harness/uicore'
import MarkdownEditor from '@uiw/react-markdown-editor' import cx from 'classnames'
import MarkdownPreview from '@uiw/react-markdown-preview'
import rehypeVideo from 'rehype-video' import rehypeVideo from 'rehype-video'
import rehypeExternalLinks from 'rehype-external-links' import rehypeExternalLinks from 'rehype-external-links'
import { INITIAL_ZOOM_LEVEL } from 'utils/Utils' import { INITIAL_ZOOM_LEVEL } from 'utils/Utils'
@ -12,9 +13,11 @@ import css from './MarkdownViewer.module.scss'
interface MarkdownViewerProps { interface MarkdownViewerProps {
source: string source: string
getString: UseStringsReturn['getString'] getString: UseStringsReturn['getString']
className?: string
maxHeight?: string | number
} }
export function MarkdownViewer({ source, getString }: MarkdownViewerProps) { export function MarkdownViewer({ source, getString, className, maxHeight }: MarkdownViewerProps) {
const [isOpen, setIsOpen] = useState<boolean>(false) const [isOpen, setIsOpen] = useState<boolean>(false)
const history = useHistory() const history = useHistory()
const [zoomLevel, setZoomLevel] = useState(INITIAL_ZOOM_LEVEL) const [zoomLevel, setZoomLevel] = useState(INITIAL_ZOOM_LEVEL)
@ -57,8 +60,11 @@ export function MarkdownViewer({ source, getString }: MarkdownViewerProps) {
) )
return ( return (
<Container className={css.main} onClick={interceptClickEventOnViewerContainer}> <Container
<MarkdownEditor.Markdown className={cx(css.main, className)}
onClick={interceptClickEventOnViewerContainer}
style={{ maxHeight: maxHeight }}>
<MarkdownPreview
source={source} source={source}
skipHtml={true} skipHtml={true}
warpperElement={{ 'data-color-mode': 'light' }} warpperElement={{ 'data-color-mode': 'light' }}
@ -80,7 +86,10 @@ export function MarkdownViewer({ source, getString }: MarkdownViewerProps) {
setZoomLevel={setZoomLevel} setZoomLevel={setZoomLevel}
zoomLevel={zoomLevel} zoomLevel={zoomLevel}
imgEvent={imgEvent} imgEvent={imgEvent}
getString={getString} i18n={{
zoomIn: getString('zoomIn'),
zoomOut: getString('zoomOut')
}}
/> />
</Container> </Container>
) )

View File

@ -50,7 +50,12 @@ export const OptionsMenuButton = ({
[css.danger]: (item as OptionsMenuItem).isDanger, [css.danger]: (item as OptionsMenuItem).isDanger,
[css.isDark]: isDark [css.isDark]: isDark
})} })}
{...omit(item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>, 'isDanger')} {...omit(
item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>,
'isDanger',
'hasIcon',
'iconName'
)}
/> />
) )
)} )}

View File

@ -28,8 +28,8 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
}) })
return ( return (
<Container className={cx(css.box, css.desc)}> <Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
<Container padding={{ left: 'small', bottom: 'small' }}> <Container padding={!edit ? { left: 'small', bottom: 'small' } : undefined}>
{(edit && ( {(edit && (
<MarkdownEditorWithPreview <MarkdownEditorWithPreview
value={content} value={content}
@ -59,7 +59,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
save: getString('save'), save: getString('save'),
cancel: getString('cancel') cancel: getString('cancel')
}} }}
maxEditorHeight="400px" editorHeight="400px"
/> />
)) || ( )) || (
<Container className={css.mdWrapper}> <Container className={css.mdWrapper}>

View File

@ -27,6 +27,8 @@
} }
.gitBlame { .gitBlame {
--code-editor-border-color: var(--grey-200);
padding: 0 var(--spacing-xlarge) 0 var(--spacing-small) !important; padding: 0 var(--spacing-xlarge) 0 var(--spacing-small) !important;
:global { :global {

View File

@ -235,7 +235,7 @@ const GitBlameRenderer = React.memo(function GitBlameSourceViewer({
<Editor <Editor
viewRef={viewRef} viewRef={viewRef}
filename={filename} filename={filename}
source={source} content={source}
readonly={true} readonly={true}
className={css.main} className={css.main}
onViewUpdate={onViewUpdate} onViewUpdate={onViewUpdate}

View File

@ -73,7 +73,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
showSuccess(getString('repoDeleted', { repo: repoMetadata?.uid }), 5000) showSuccess(getString('repoDeleted', { repo: repoMetadata?.uid }), 5000)
history.push(routes.toCODERepositories({ space })) history.push(routes.toCODERepositories({ space }))
}) })
.catch((error: any) => { .catch((error: Unknown) => {
showError(getErrorMessage(error), 0, 'failedToDeleteBranch') showError(getErrorMessage(error), 0, 'failedToDeleteBranch')
}) })
} }
@ -172,7 +172,6 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
setEditDesc(ACCESS_MODES.EDIT) setEditDesc(ACCESS_MODES.EDIT)
}} }}
{...permissionProps(permEditResult, standalone)} {...permissionProps(permEditResult, standalone)}
/> />
</Text> </Text>
)} )}
@ -191,8 +190,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
}} }}
variation={ButtonVariation.SECONDARY} variation={ButtonVariation.SECONDARY}
text={getString('delete')} text={getString('delete')}
{...permissionProps(permDeleteResult, standalone)} {...permissionProps(permDeleteResult, standalone)}></Button>
></Button>
</Container> </Container>
</Container> </Container>
</Layout.Vertical> </Layout.Vertical>

View File

@ -1,4 +1,15 @@
$code-editor-font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
@mixin mono-font { @mixin mono-font {
font-family: var(--code-editor-font-family) !important; font-family: var(--font-family-mono) !important;
font-size: var(--code-editor-font-size) !important; font-size: 12px !important;
font-feature-settings: 'liga' 0, 'calt' 0;
line-height: 18px;
letter-spacing: 0px;
}
@mixin markdown-font {
font-family: var(--font-family) !important;
font-size: 12px !important;
} }

File diff suppressed because it is too large Load Diff