mirror of
https://github.com/harness/drone.git
synced 2025-05-10 00:29:29 +08:00
Resolve Editor slowness by replacing Editor React wrapper lib
This commit is contained in:
parent
abd3110a5b
commit
1873bf57d6
@ -37,6 +37,11 @@
|
||||
"@blueprintjs/core": "3.26.1",
|
||||
"@blueprintjs/datetime": "3.13.0",
|
||||
"@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/icons": "1.110.2",
|
||||
"@harness/ng-tooltip": ">=1.31.25",
|
||||
@ -47,7 +52,7 @@
|
||||
"@uiw/codemirror-extensions-color": "^4.19.9",
|
||||
"@uiw/codemirror-extensions-hyper-link": "^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",
|
||||
"classnames": "^2.2.6",
|
||||
"clipboard-copy": "^3.1.0",
|
||||
|
@ -5,11 +5,6 @@
|
||||
*/
|
||||
|
||||
.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 {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
|
@ -63,8 +63,7 @@
|
||||
}
|
||||
|
||||
.editCommentContainer {
|
||||
background-color: var(--grey-50) !important;
|
||||
padding: var(--spacing-large) !important;
|
||||
padding: var(--spacing-small) !important;
|
||||
border-radius: var(--box-radius);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useResizeDetector } from 'react-resize-detector'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
||||
import { Container, Layout, Avatar, TextInput, Text, Color, FontVariation, FlexExpander } from '@harness/uicore'
|
||||
import cx from 'classnames'
|
||||
@ -10,10 +11,7 @@ import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||
import {
|
||||
MarkdownEditorWithPreview,
|
||||
MarkdownEditorWithPreviewResetProps
|
||||
} from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import css from './CommentBox.module.scss'
|
||||
|
||||
@ -106,7 +104,7 @@ export const CommentBox = <T = unknown,>({
|
||||
.join(CRLF)
|
||||
)
|
||||
}, [])
|
||||
const editorRef = useRef<MarkdownEditorWithPreviewResetProps>()
|
||||
const viewRef = useRef<EditorView>()
|
||||
|
||||
return (
|
||||
<Container
|
||||
@ -144,11 +142,10 @@ export const CommentBox = <T = unknown,>({
|
||||
</Container>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<Container
|
||||
padding="xlarge"
|
||||
className={cx(css.newCommentContainer, { [css.hasThread]: !!comments.length })}>
|
||||
<Container className={cx(css.newCommentContainer, { [css.hasThread]: !!comments.length })}>
|
||||
<MarkdownEditorWithPreview
|
||||
editorRef={editorRef as React.MutableRefObject<MarkdownEditorWithPreviewResetProps>}
|
||||
viewRef={viewRef}
|
||||
noBorder
|
||||
i18n={{
|
||||
placeHolder: getString(comments.length ? 'replyHere' : 'leaveAComment'),
|
||||
tabEdit: getString('write'),
|
||||
@ -170,7 +167,13 @@ export const CommentBox = <T = unknown,>({
|
||||
setMarkdown('')
|
||||
|
||||
if (resetOnSave) {
|
||||
editorRef.current?.resetEditor?.()
|
||||
viewRef.current?.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: viewRef.current.state.doc.length,
|
||||
insert: ''
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setComments([...comments, updatedItem as CommentItem<T>])
|
||||
setShowReplyPlaceHolder(true)
|
||||
|
@ -177,7 +177,7 @@ export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
|
||||
disabled={disableBranchCreation}
|
||||
label=""
|
||||
onChange={e => {
|
||||
setTargetBranchOption(get(e.target, 'defaultValue'))
|
||||
setTargetBranchOption(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption)
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
|
30
web/src/components/Editor/Editor.module.scss
Normal file
30
web/src/components/Editor/Editor.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
web/src/components/Editor/Editor.module.scss.d.ts
vendored
Normal file
6
web/src/components/Editor/Editor.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
// this is an auto-generated file
|
||||
declare const styles: {
|
||||
readonly editor: string
|
||||
}
|
||||
export default styles
|
@ -2,41 +2,64 @@ import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import { Container } from '@harness/uicore'
|
||||
import { LanguageDescription } from '@codemirror/language'
|
||||
import { indentWithTab } from '@codemirror/commands'
|
||||
import cx from 'classnames'
|
||||
import type { ViewUpdate } from '@codemirror/view'
|
||||
import type { Text } from '@codemirror/state'
|
||||
import { languages } from '@codemirror/language-data'
|
||||
import { EditorView, keymap } from '@codemirror/view'
|
||||
import { noop } from 'lodash-es'
|
||||
import { markdown } from '@codemirror/lang-markdown'
|
||||
import { EditorView, keymap, placeholder as placeholderExtension } from '@codemirror/view'
|
||||
import { Compartment, EditorState, Extension } from '@codemirror/state'
|
||||
import { color } from '@uiw/codemirror-extensions-color'
|
||||
import { hyperLink } from '@uiw/codemirror-extensions-hyper-link'
|
||||
import { githubLight as theme } from '@uiw/codemirror-themes-all'
|
||||
import css from './Editor.module.scss'
|
||||
|
||||
interface EditorProps {
|
||||
filename: string
|
||||
source: string
|
||||
onViewUpdate?: (update: ViewUpdate) => void
|
||||
export interface EditorProps {
|
||||
content: string
|
||||
filename?: string
|
||||
forMarkdown?: boolean
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
autoFocus?: boolean
|
||||
className?: string
|
||||
extensions?: Extension
|
||||
maxHeight?: string | number
|
||||
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({
|
||||
source,
|
||||
content,
|
||||
filename,
|
||||
onViewUpdate = noop,
|
||||
forMarkdown,
|
||||
placeholder,
|
||||
readonly = false,
|
||||
autoFocus,
|
||||
className,
|
||||
extensions = new Compartment().of([]),
|
||||
viewRef
|
||||
maxHeight,
|
||||
viewRef,
|
||||
setDirty,
|
||||
onChange,
|
||||
onViewUpdate
|
||||
}: EditorProps) {
|
||||
const view = useRef<EditorView>()
|
||||
const ref = useRef<HTMLDivElement>()
|
||||
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(() => {
|
||||
const editorView = new EditorView({
|
||||
doc: source,
|
||||
doc: content,
|
||||
extensions: [
|
||||
extensions,
|
||||
|
||||
@ -45,11 +68,21 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||
theme,
|
||||
|
||||
EditorView.lineWrapping,
|
||||
|
||||
...(placeholder ? [placeholderExtension(placeholder)] : []),
|
||||
|
||||
keymap.of([indentWithTab]),
|
||||
|
||||
...(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)
|
||||
@ -67,25 +100,35 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||
viewRef.current = editorView
|
||||
}
|
||||
|
||||
const cleanDoc = editorView.state.doc
|
||||
|
||||
if (autoFocus) {
|
||||
editorView.focus()
|
||||
}
|
||||
|
||||
return () => {
|
||||
editorView.destroy()
|
||||
}
|
||||
}, []) // 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(() => {
|
||||
if (filename) {
|
||||
languageDescriptionFrom(filename)
|
||||
if (forMarkdown) {
|
||||
view.current?.dispatch({ effects: languageConfig.reconfigure(markdownLanguageSupport) })
|
||||
} else if (filename) {
|
||||
LanguageDescription.matchFilename(languages, filename)
|
||||
?.load()
|
||||
.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)
|
||||
}
|
||||
|
@ -1,20 +1,24 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ButtonGroup, ButtonVariation, Button, Container, Dialog, Carousel } from '@harness/uicore'
|
||||
import { ZOOM_INC_DEC_LEVEL } from 'utils/Utils'
|
||||
import type { UseStringsReturn } from 'framework/strings'
|
||||
import css from './ImageCarousel.module.scss'
|
||||
|
||||
interface ImageCarouselProps {
|
||||
isOpen: boolean
|
||||
setIsOpen: (value: boolean) => void
|
||||
setZoomLevel: (value: number) => void
|
||||
zoomLevel: number
|
||||
imgEvent: string[]
|
||||
getString: UseStringsReturn['getString']
|
||||
i18n: {
|
||||
zoomIn: string
|
||||
zoomOut: string
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
portalClassName={css.portalContainer}
|
||||
@ -55,7 +59,7 @@ const ImageCarousel = (props: ImageCarouselProps) => {
|
||||
variation={ButtonVariation.TERTIARY}
|
||||
icon="zoom-in"
|
||||
data-testid="zoomInButton"
|
||||
tooltip={getString('zoomIn')}
|
||||
tooltip={i18n.zoomIn}
|
||||
onClick={() => {
|
||||
Number(zoomLevel.toFixed(1)) < 2 && setZoomLevel(zoomLevel + ZOOM_INC_DEC_LEVEL)
|
||||
}}
|
||||
@ -71,7 +75,7 @@ const ImageCarousel = (props: ImageCarouselProps) => {
|
||||
variation={ButtonVariation.TERTIARY}
|
||||
icon="zoom-out"
|
||||
data-testid="zoomOutButton"
|
||||
tooltip={getString('zoomOut')}
|
||||
tooltip={i18n.zoomOut}
|
||||
onClick={() => {
|
||||
Number(zoomLevel.toFixed(1)) > 0.3 && setZoomLevel(zoomLevel - ZOOM_INC_DEC_LEVEL)
|
||||
}}
|
||||
@ -83,3 +87,6 @@ const ImageCarousel = (props: ImageCarouselProps) => {
|
||||
}
|
||||
|
||||
export default ImageCarousel
|
||||
|
||||
// TODO: Dialog does not have i18n context when mounted inside CommentBox/different React root
|
||||
// Hence getString can't get proper translations
|
||||
|
@ -101,12 +101,12 @@
|
||||
|
||||
.markdownEditor {
|
||||
:global {
|
||||
.cm-editor .cm-line {
|
||||
&,
|
||||
* {
|
||||
@include mono-font;
|
||||
}
|
||||
}
|
||||
// .cm-editor .cm-line {
|
||||
// &,
|
||||
// * {
|
||||
// @include mono-font;
|
||||
// }
|
||||
// }
|
||||
|
||||
.md-editor {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,11 @@ declare const styles: {
|
||||
readonly tabs: string
|
||||
readonly preview: 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
|
||||
|
@ -1,22 +1,45 @@
|
||||
import React, { useEffect, 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 React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Button, Container, ButtonVariation, Layout, Color, ButtonSize, IconName } from '@harness/uicore'
|
||||
import cx from 'classnames'
|
||||
import { keymap, EditorView } from '@codemirror/view'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { IToolBarProps } from '@uiw/react-markdown-editor/cjs/components/ToolBar'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import { Editor } from 'components/Editor/Editor'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import css from './MarkdownEditorWithPreview.module.scss'
|
||||
|
||||
export interface MarkdownEditorWithPreviewResetProps {
|
||||
resetEditor: () => void
|
||||
enum MarkdownEditorTab {
|
||||
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 {
|
||||
value: string
|
||||
onChange?: (value: string, original: string) => void
|
||||
onSave?: (value: string, original: string) => void
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
onSave?: (value: string) => void
|
||||
onCancel?: () => void
|
||||
i18n: {
|
||||
placeHolder: string
|
||||
@ -28,114 +51,220 @@ interface MarkdownEditorWithPreviewProps {
|
||||
hideButtons?: boolean
|
||||
hideCancel?: boolean
|
||||
editorHeight?: string
|
||||
maxEditorHeight?: string
|
||||
editorRef?: React.MutableRefObject<MarkdownEditorWithPreviewResetProps>
|
||||
noBorder?: boolean
|
||||
viewRef?: React.MutableRefObject<EditorView | undefined>
|
||||
}
|
||||
|
||||
export function MarkdownEditorWithPreview({
|
||||
value,
|
||||
onChange = noop,
|
||||
onSave = noop,
|
||||
onCancel = noop,
|
||||
value = '',
|
||||
onChange,
|
||||
onSave,
|
||||
onCancel,
|
||||
i18n,
|
||||
hideButtons,
|
||||
hideCancel,
|
||||
editorHeight,
|
||||
maxEditorHeight,
|
||||
editorRef
|
||||
noBorder,
|
||||
viewRef: viewRefProp
|
||||
}: MarkdownEditorWithPreviewProps) {
|
||||
const [original, setOriginal] = useState(value)
|
||||
const [selectedTab, setSelectedTab] = useState<MarkdownEditorTab>(MarkdownEditorTab.WRITE)
|
||||
const [val, setVal] = useState(value)
|
||||
const [selectedTab, setSelectedTab] = useState(MarkdownEditorTab.WRITE)
|
||||
const viewRef = useRef<EditorView>()
|
||||
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(() => {
|
||||
if (editorRef) {
|
||||
editorRef.current = {
|
||||
resetEditor: () => {
|
||||
setOriginal(value)
|
||||
setVal(value)
|
||||
}
|
||||
}
|
||||
editorRef.current.resetEditor()
|
||||
if (viewRefProp) {
|
||||
viewRefProp.current = viewRef.current
|
||||
}
|
||||
}, [editorRef]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [viewRefProp, viewRef.current]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
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={i18n.tabEdit}
|
||||
panel={
|
||||
<Container
|
||||
className={css.markdownEditor}
|
||||
style={
|
||||
{
|
||||
'--editor-height': editorHeight,
|
||||
'--max-editor-height': maxEditorHeight
|
||||
} as React.CSSProperties
|
||||
}>
|
||||
<MarkdownEditor
|
||||
value={val}
|
||||
visible={false}
|
||||
placeholder={i18n.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>
|
||||
<Container className={cx(css.container, { [css.noBorder]: noBorder })}>
|
||||
<ul className={css.tabs}>
|
||||
<li>
|
||||
<a
|
||||
role="tab"
|
||||
tabIndex={0}
|
||||
aria-selected={selectedTab === MarkdownEditorTab.WRITE}
|
||||
onClick={() => setSelectedTab(MarkdownEditorTab.WRITE)}>
|
||||
Write
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
role="tab"
|
||||
tabIndex={0}
|
||||
aria-selected={selectedTab === MarkdownEditorTab.PREVIEW}
|
||||
onClick={() => setSelectedTab(MarkdownEditorTab.PREVIEW)}>
|
||||
Preview
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<Container className={css.toolbar}>
|
||||
{toolbar.map((item, index) => {
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
size={ButtonSize.SMALL}
|
||||
variation={ButtonVariation.ICON}
|
||||
icon={item.icon}
|
||||
withoutCurrentColor
|
||||
iconProps={{ color: Color.PRIMARY_10, size: 14 }}
|
||||
onClick={() => onToolbarAction(item.action)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
<Container className={css.tabContent}>
|
||||
<Editor
|
||||
forMarkdown
|
||||
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}
|
||||
disabled={!val}
|
||||
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>
|
||||
}}
|
||||
/>
|
||||
{selectedTab === MarkdownEditorTab.PREVIEW && (
|
||||
<MarkdownViewer source={viewRef.current?.state.doc.toString() || ''} getString={getString} maxHeight={800} />
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
const toolbars: IToolBarProps['toolbars'] = ['bold', 'strike', 'olist', 'ulist', 'todo', 'link', 'image']
|
||||
|
||||
enum MarkdownEditorTab {
|
||||
WRITE = 'write',
|
||||
PREVIEW = 'preview'
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
.main {
|
||||
overflow: auto;
|
||||
|
||||
:global {
|
||||
.wmde-markdown {
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
.code-line {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
}
|
||||
|
||||
// Customize https://wangchujiang.com/rehype-video/
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
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 rehypeExternalLinks from 'rehype-external-links'
|
||||
import { INITIAL_ZOOM_LEVEL } from 'utils/Utils'
|
||||
@ -12,9 +13,11 @@ import css from './MarkdownViewer.module.scss'
|
||||
interface MarkdownViewerProps {
|
||||
source: string
|
||||
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 history = useHistory()
|
||||
const [zoomLevel, setZoomLevel] = useState(INITIAL_ZOOM_LEVEL)
|
||||
@ -57,8 +60,11 @@ export function MarkdownViewer({ source, getString }: MarkdownViewerProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className={css.main} onClick={interceptClickEventOnViewerContainer}>
|
||||
<MarkdownEditor.Markdown
|
||||
<Container
|
||||
className={cx(css.main, className)}
|
||||
onClick={interceptClickEventOnViewerContainer}
|
||||
style={{ maxHeight: maxHeight }}>
|
||||
<MarkdownPreview
|
||||
source={source}
|
||||
skipHtml={true}
|
||||
warpperElement={{ 'data-color-mode': 'light' }}
|
||||
@ -80,7 +86,10 @@ export function MarkdownViewer({ source, getString }: MarkdownViewerProps) {
|
||||
setZoomLevel={setZoomLevel}
|
||||
zoomLevel={zoomLevel}
|
||||
imgEvent={imgEvent}
|
||||
getString={getString}
|
||||
i18n={{
|
||||
zoomIn: getString('zoomIn'),
|
||||
zoomOut: getString('zoomOut')
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
@ -50,7 +50,12 @@ export const OptionsMenuButton = ({
|
||||
[css.danger]: (item as OptionsMenuItem).isDanger,
|
||||
[css.isDark]: isDark
|
||||
})}
|
||||
{...omit(item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>, 'isDanger')}
|
||||
{...omit(
|
||||
item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
'isDanger',
|
||||
'hasIcon',
|
||||
'iconName'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
@ -28,8 +28,8 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
||||
})
|
||||
|
||||
return (
|
||||
<Container className={cx(css.box, css.desc)}>
|
||||
<Container padding={{ left: 'small', bottom: 'small' }}>
|
||||
<Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
|
||||
<Container padding={!edit ? { left: 'small', bottom: 'small' } : undefined}>
|
||||
{(edit && (
|
||||
<MarkdownEditorWithPreview
|
||||
value={content}
|
||||
@ -59,7 +59,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
||||
save: getString('save'),
|
||||
cancel: getString('cancel')
|
||||
}}
|
||||
maxEditorHeight="400px"
|
||||
editorHeight="400px"
|
||||
/>
|
||||
)) || (
|
||||
<Container className={css.mdWrapper}>
|
||||
|
@ -27,6 +27,8 @@
|
||||
}
|
||||
|
||||
.gitBlame {
|
||||
--code-editor-border-color: var(--grey-200);
|
||||
|
||||
padding: 0 var(--spacing-xlarge) 0 var(--spacing-small) !important;
|
||||
|
||||
:global {
|
||||
|
@ -235,7 +235,7 @@ const GitBlameRenderer = React.memo(function GitBlameSourceViewer({
|
||||
<Editor
|
||||
viewRef={viewRef}
|
||||
filename={filename}
|
||||
source={source}
|
||||
content={source}
|
||||
readonly={true}
|
||||
className={css.main}
|
||||
onViewUpdate={onViewUpdate}
|
||||
|
@ -73,7 +73,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
|
||||
showSuccess(getString('repoDeleted', { repo: repoMetadata?.uid }), 5000)
|
||||
history.push(routes.toCODERepositories({ space }))
|
||||
})
|
||||
.catch((error: any) => {
|
||||
.catch((error: Unknown) => {
|
||||
showError(getErrorMessage(error), 0, 'failedToDeleteBranch')
|
||||
})
|
||||
}
|
||||
@ -172,7 +172,6 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
|
||||
setEditDesc(ACCESS_MODES.EDIT)
|
||||
}}
|
||||
{...permissionProps(permEditResult, standalone)}
|
||||
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
@ -191,8 +190,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
|
||||
}}
|
||||
variation={ButtonVariation.SECONDARY}
|
||||
text={getString('delete')}
|
||||
{...permissionProps(permDeleteResult, standalone)}
|
||||
></Button>
|
||||
{...permissionProps(permDeleteResult, standalone)}></Button>
|
||||
</Container>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
|
@ -1,4 +1,15 @@
|
||||
$code-editor-font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
||||
monospace;
|
||||
|
||||
@mixin mono-font {
|
||||
font-family: var(--code-editor-font-family) !important;
|
||||
font-size: var(--code-editor-font-size) !important;
|
||||
font-family: var(--font-family-mono) !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;
|
||||
}
|
||||
|
3232
web/yarn.lock
3232
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user