mirror of
https://github.com/harness/drone.git
synced 2025-05-10 07:30:33 +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/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",
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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={[
|
||||||
{
|
{
|
||||||
|
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 { 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)
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}, [viewRefProp, viewRef.current]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
editorRef.current.resetEditor()
|
|
||||||
}
|
|
||||||
}, [editorRef]) // 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>
|
||||||
|
</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}
|
placeholder={i18n.placeHolder}
|
||||||
theme="light"
|
|
||||||
indentWithTab={false}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
// TODO: Customize toolbars to show tooltip.
|
viewRef={viewRef}
|
||||||
// @see https://github.com/uiwjs/react-markdown-editor#custom-toolbars
|
setDirty={setDirty}
|
||||||
toolbars={toolbars}
|
maxHeight={editorHeight}
|
||||||
toolbarsMode={[]}
|
className={selectedTab === MarkdownEditorTab.PREVIEW ? css.hidden : undefined}
|
||||||
basicSetup={{
|
onChange={doc => {
|
||||||
lineNumbers: false,
|
if (dirty) {
|
||||||
foldGutter: false,
|
onChange?.(doc.toString())
|
||||||
highlightActiveLine: false
|
}
|
||||||
}}
|
|
||||||
extensions={[keymap.of([indentWithTab]), EditorView.lineWrapping]}
|
|
||||||
onChange={(_value, _viewUpdate) => {
|
|
||||||
setVal(_value)
|
|
||||||
onChange(_value, original)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{selectedTab === MarkdownEditorTab.PREVIEW && (
|
||||||
|
<MarkdownViewer source={viewRef.current?.state.doc.toString() || ''} getString={getString} maxHeight={800} />
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Tab
|
|
||||||
id={MarkdownEditorTab.PREVIEW}
|
|
||||||
disabled={!val}
|
|
||||||
title={i18n.tabPreview}
|
|
||||||
panel={
|
|
||||||
<Container padding="large" className={css.preview}>
|
|
||||||
<MarkdownEditor.Markdown source={val} />
|
|
||||||
</Container>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
{!hideButtons && (
|
{!hideButtons && (
|
||||||
<Container>
|
<Container className={css.buttonsBar}>
|
||||||
<Layout.Horizontal spacing="small">
|
<Layout.Horizontal spacing="small">
|
||||||
<Button
|
<Button
|
||||||
disabled={!(val || '').trim() || val === original}
|
disabled={!dirty}
|
||||||
variation={ButtonVariation.PRIMARY}
|
variation={ButtonVariation.PRIMARY}
|
||||||
onClick={() => onSave(val, original)}
|
onClick={() => onSave?.(viewRef.current?.state.doc.toString() || '')}
|
||||||
text={i18n.save}
|
text={i18n.save}
|
||||||
/>
|
/>
|
||||||
{!hideCancel && <Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={i18n.cancel} />}
|
{!hideCancel && <Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={i18n.cancel} />}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
</Layout.Vertical>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbars: IToolBarProps['toolbars'] = ['bold', 'strike', 'olist', 'ulist', 'todo', 'link', 'image']
|
|
||||||
|
|
||||||
enum MarkdownEditorTab {
|
|
||||||
WRITE = 'write',
|
|
||||||
PREVIEW = 'preview'
|
|
||||||
}
|
|
||||||
|
@ -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/
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -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}>
|
||||||
|
@ -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 {
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
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