mirror of
https://github.com/harness/drone.git
synced 2025-05-09 18:00:05 +08:00
Merge branch 'pr-loading' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#6)
This commit is contained in:
commit
7344959cbb
@ -14,7 +14,7 @@ import {
|
||||
MarkdownEditorWithPreview,
|
||||
MarkdownEditorWithPreviewResetProps
|
||||
} from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import css from './CommentBox.module.scss'
|
||||
|
||||
export interface CommentItem<T = unknown> {
|
||||
@ -331,7 +331,7 @@ const CommentsThread = <T = unknown,>({
|
||||
<Text className={css.deleted}>{getString('commentDeleted')}</Text>
|
||||
</Truthy>
|
||||
<Else>
|
||||
<MarkdownViewer source={commentItem?.content} />
|
||||
<MarkdownViewer source={commentItem?.content} getString={getString} />
|
||||
</Else>
|
||||
</Match>
|
||||
</Else>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ButtonGroup, ButtonVariation, Button, Container, Dialog, Carousel } from '@harness/uicore'
|
||||
import { ZOOM_INC_DEC_LEVEL } from 'utils/Utils'
|
||||
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { UseStringsReturn } from 'framework/strings'
|
||||
import css from './ImageCarousel.module.scss'
|
||||
interface ImageCarouselProps {
|
||||
isOpen: boolean
|
||||
@ -10,12 +9,11 @@ interface ImageCarouselProps {
|
||||
setZoomLevel: (value: number) => void
|
||||
zoomLevel: number
|
||||
imgEvent: string[]
|
||||
getString: UseStringsReturn['getString']
|
||||
}
|
||||
|
||||
const ImageCarousel = (props: ImageCarouselProps) => {
|
||||
const { getString } = useStrings()
|
||||
|
||||
const { isOpen, setIsOpen, setZoomLevel, zoomLevel, imgEvent } = props
|
||||
const { getString, isOpen, setIsOpen, setZoomLevel, zoomLevel, imgEvent } = props
|
||||
const [imgTitle, setImageTitle] = useState(imgEvent[0])
|
||||
return (
|
||||
<Dialog
|
||||
|
22
web/src/components/MarkdownViewer/MarkdownViewer.module.scss
Normal file
22
web/src/components/MarkdownViewer/MarkdownViewer.module.scss
Normal file
@ -0,0 +1,22 @@
|
||||
.main {
|
||||
:global {
|
||||
.wmde-markdown {
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Customize https://wangchujiang.com/rehype-video/
|
||||
details.octicon.octicon-video {
|
||||
display: block;
|
||||
|
||||
> summary {
|
||||
padding-bottom: var(--spacing-xsmall);
|
||||
|
||||
> svg {
|
||||
margin: 0 var(--spacing-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
web/src/components/MarkdownViewer/MarkdownViewer.module.scss.d.ts
vendored
Normal file
6
web/src/components/MarkdownViewer/MarkdownViewer.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
// this is an auto-generated file
|
||||
declare const styles: {
|
||||
readonly main: string
|
||||
}
|
||||
export default styles
|
87
web/src/components/MarkdownViewer/MarkdownViewer.tsx
Normal file
87
web/src/components/MarkdownViewer/MarkdownViewer.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
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 rehypeVideo from 'rehype-video'
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
import { INITIAL_ZOOM_LEVEL } from 'utils/Utils'
|
||||
import type { UseStringsReturn } from 'framework/strings'
|
||||
import ImageCarousel from 'components/ImageCarousel/ImageCarousel'
|
||||
import css from './MarkdownViewer.module.scss'
|
||||
|
||||
interface MarkdownViewerProps {
|
||||
source: string
|
||||
getString: UseStringsReturn['getString']
|
||||
}
|
||||
|
||||
export function MarkdownViewer({ source, getString }: MarkdownViewerProps) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const history = useHistory()
|
||||
const [zoomLevel, setZoomLevel] = useState(INITIAL_ZOOM_LEVEL)
|
||||
|
||||
const [imgEvent, setImageEvent] = useState<string[]>([])
|
||||
|
||||
const interceptClickEventOnViewerContainer = useCallback(
|
||||
event => {
|
||||
const { target } = event
|
||||
|
||||
const imageArray = source.split('\n').filter(string => string.includes('![image]'))
|
||||
const imageStringArray = imageArray.map(string => {
|
||||
const imageSrc = string.split('![image]')[1]
|
||||
return imageSrc.slice(1, imageSrc.length - 1)
|
||||
})
|
||||
setImageEvent(imageStringArray)
|
||||
if (target?.tagName?.toLowerCase() === 'a') {
|
||||
const { href } = target
|
||||
|
||||
// Intercept click event on internal links and navigate to pages to avoid full page reload
|
||||
if (href && !href.startsWith('#')) {
|
||||
try {
|
||||
const url = new URL(href)
|
||||
|
||||
if (url.origin === window.location.origin) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
history.push(url.pathname)
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error: MarkdownViewer/interceptClickEventOnViewerContainer', e)
|
||||
}
|
||||
}
|
||||
} else if (event.target.nodeName?.toLowerCase() === 'img') {
|
||||
setIsOpen(true)
|
||||
}
|
||||
},
|
||||
[history, source]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className={css.main} onClick={interceptClickEventOnViewerContainer}>
|
||||
<MarkdownEditor.Markdown
|
||||
source={source}
|
||||
skipHtml={true}
|
||||
warpperElement={{ 'data-color-mode': 'light' }}
|
||||
rehypeRewrite={(node, _index, parent) => {
|
||||
if ((node as unknown as HTMLDivElement).tagName === 'a') {
|
||||
if (parent && /^h(1|2|3|4|5|6)/.test((parent as unknown as HTMLDivElement).tagName)) {
|
||||
parent.children = parent.children.slice(1)
|
||||
}
|
||||
}
|
||||
}}
|
||||
rehypePlugins={[
|
||||
rehypeVideo,
|
||||
[rehypeExternalLinks, { rel: ['nofollow noreferrer noopener'], target: '_blank' }]
|
||||
]}
|
||||
/>
|
||||
<ImageCarousel
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
setZoomLevel={setZoomLevel}
|
||||
zoomLevel={zoomLevel}
|
||||
imgEvent={imgEvent}
|
||||
getString={getString}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -7,9 +7,14 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 0 0 0 var(--spacing-small) !important;
|
||||
padding-left: var(--spacing-small) !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&.spinnerOnRight {
|
||||
padding-left: 0 !important;
|
||||
padding-right: var(--spacing-small) !important;
|
||||
}
|
||||
|
||||
.input {
|
||||
span[data-icon],
|
||||
span[icon] {
|
||||
|
@ -4,6 +4,7 @@ declare const styles: {
|
||||
readonly main: string
|
||||
readonly layout: string
|
||||
readonly wrapper: string
|
||||
readonly spinnerOnRight: string
|
||||
readonly input: string
|
||||
}
|
||||
export default styles
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import cx from 'classnames'
|
||||
import { Color, Container, Icon, IconName, Layout, TextInput } from '@harness/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import css from './SearchInputWithSpinner.module.scss'
|
||||
@ -11,6 +13,7 @@ interface SearchInputWithSpinnerProps {
|
||||
placeholder?: string
|
||||
icon?: IconName
|
||||
spinnerIcon?: IconName
|
||||
spinnerPosition?: 'left' | 'right'
|
||||
}
|
||||
|
||||
export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
@ -20,16 +23,20 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
width = 250,
|
||||
placeholder,
|
||||
icon = 'search',
|
||||
spinnerIcon = 'spinner'
|
||||
spinnerIcon = 'spinner',
|
||||
spinnerPosition = 'left'
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const spinner = <Icon name={spinnerIcon as IconName} color={Color.PRIMARY_7} />
|
||||
const spinnerOnRight = spinnerPosition === 'right'
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<Layout.Horizontal className={css.layout}>
|
||||
{loading && <Icon name={spinnerIcon as IconName} color={Color.PRIMARY_7} />}
|
||||
<Render when={loading && !spinnerOnRight}>{spinner}</Render>
|
||||
<TextInput
|
||||
value={query}
|
||||
wrapperClassName={css.wrapper}
|
||||
wrapperClassName={cx(css.wrapper, { [css.spinnerOnRight]: spinnerOnRight })}
|
||||
className={css.input}
|
||||
placeholder={placeholder || getString('search')}
|
||||
leftIcon={icon as IconName}
|
||||
@ -38,6 +45,7 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
onFocus={event => event.target.select()}
|
||||
onInput={event => setQuery(event.currentTarget.value || '')}
|
||||
/>
|
||||
<Render when={loading && spinnerOnRight}>{spinner}</Render>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
)
|
||||
|
@ -1,93 +1,6 @@
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import React, { Suspense, useCallback, useState } from 'react'
|
||||
import { Container, Text } from '@harness/uicore'
|
||||
import MarkdownEditor from '@uiw/react-markdown-editor'
|
||||
import rehypeVideo from 'rehype-video'
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import React from 'react'
|
||||
import { SourceCodeEditor } from 'components/SourceCodeEditor/SourceCodeEditor'
|
||||
import { INITIAL_ZOOM_LEVEL, SourceCodeEditorProps } from 'utils/Utils'
|
||||
|
||||
import ImageCarousel from 'components/ImageCarousel/ImageCarousel'
|
||||
import css from './SourceCodeViewer.module.scss'
|
||||
|
||||
interface MarkdownViewerProps {
|
||||
source: string
|
||||
}
|
||||
|
||||
export function MarkdownViewer({ source }: MarkdownViewerProps) {
|
||||
const { getString } = useStrings()
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const history = useHistory()
|
||||
const [zoomLevel, setZoomLevel] = useState(INITIAL_ZOOM_LEVEL)
|
||||
|
||||
const [imgEvent, setImageEvent] = useState<string[]>([])
|
||||
|
||||
const interceptClickEventOnViewerContainer = useCallback(
|
||||
event => {
|
||||
const { target } = event
|
||||
|
||||
const imageArray = source.split('\n').filter(string => string.includes('![image]'))
|
||||
const imageStringArray = imageArray.map(string => {
|
||||
const imageSrc = string.split('![image]')[1]
|
||||
return imageSrc.slice(1, imageSrc.length - 1)
|
||||
})
|
||||
setImageEvent(imageStringArray)
|
||||
if (target?.tagName?.toLowerCase() === 'a') {
|
||||
const { href } = target
|
||||
|
||||
// Intercept click event on internal links and navigate to pages to avoid full page reload
|
||||
if (href && !href.startsWith('#')) {
|
||||
try {
|
||||
const url = new URL(href)
|
||||
|
||||
if (url.origin === window.location.origin) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
history.push(url.pathname)
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error: MarkdownViewer/interceptClickEventOnViewerContainer', e)
|
||||
}
|
||||
}
|
||||
} else if (event.target.nodeName?.toLowerCase() === 'img') {
|
||||
setIsOpen(true)
|
||||
}
|
||||
},
|
||||
[history, source]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className={css.main} onClick={interceptClickEventOnViewerContainer}>
|
||||
<Suspense fallback={<Text>{getString('loading')}</Text>}>
|
||||
<MarkdownEditor.Markdown
|
||||
source={source}
|
||||
skipHtml={true}
|
||||
warpperElement={{ 'data-color-mode': 'light' }}
|
||||
rehypeRewrite={(node, _index, parent) => {
|
||||
if ((node as unknown as HTMLDivElement).tagName === 'a') {
|
||||
if (parent && /^h(1|2|3|4|5|6)/.test((parent as unknown as HTMLDivElement).tagName)) {
|
||||
parent.children = parent.children.slice(1)
|
||||
}
|
||||
}
|
||||
}}
|
||||
rehypePlugins={[
|
||||
rehypeVideo,
|
||||
[rehypeExternalLinks, { rel: ['nofollow noreferrer noopener'], target: '_blank' }]
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
<ImageCarousel
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
setZoomLevel={setZoomLevel}
|
||||
zoomLevel={zoomLevel}
|
||||
imgEvent={imgEvent}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
import type { SourceCodeEditorProps } from 'utils/Utils'
|
||||
|
||||
type SourceCodeViewerProps = Omit<SourceCodeEditorProps, 'readOnly' | 'autoHeight'>
|
||||
|
||||
|
@ -23,7 +23,7 @@ import ReactTimeago from 'react-timeago'
|
||||
import { orderBy } from 'lodash-es'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import type { TypesPullReqActivity } from 'services/code'
|
||||
@ -680,6 +680,7 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
|
||||
margin={{ top: 'medium', left: 'xxxlarge' }}
|
||||
style={{ maxWidth: 'calc(100vw - 450px)', overflow: 'auto' }}>
|
||||
<MarkdownViewer
|
||||
getString={getString}
|
||||
source={[getString('pr.titleChangedTable').replace(/\n$/, '')]
|
||||
.concat(
|
||||
commentItems
|
||||
|
@ -2,7 +2,7 @@ import React, { useState } from 'react'
|
||||
import { Container, useToaster } from '@harness/uicore'
|
||||
import cx from 'classnames'
|
||||
import { useMutate } from 'restful-react'
|
||||
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { OpenapiUpdatePullReqRequest } from 'services/code'
|
||||
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||
@ -63,7 +63,7 @@ export const DescriptionBox: React.FC<ConversationProps> = ({
|
||||
/>
|
||||
)) || (
|
||||
<Container className={css.mdWrapper}>
|
||||
<MarkdownViewer source={content} />
|
||||
<MarkdownViewer source={content} getString={getString} />
|
||||
<Container className={css.menuWrapper}>
|
||||
<OptionsMenuButton
|
||||
isDark={true}
|
||||
|
@ -61,6 +61,7 @@ export function PullRequestsContentHeader({
|
||||
setSearchTerm(value)
|
||||
onSearchTermChanged(value)
|
||||
}}
|
||||
spinnerPosition="right"
|
||||
/>
|
||||
<FlexExpander />
|
||||
<Button
|
||||
|
@ -18,7 +18,7 @@ import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { OpenapiGetContentOutput, TypesRepository } from 'services/code'
|
||||
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import { useDisableCodeMainLinks } from 'hooks/useDisableCodeMainLinks'
|
||||
import { Images } from 'images'
|
||||
@ -163,6 +163,7 @@ const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'resourc
|
||||
padding={{ top: 'xxlarge', bottom: 'xxlarge', left: 'xxlarge', right: 'xxlarge' }}
|
||||
className={css.divContainer}>
|
||||
<MarkdownViewer
|
||||
getString={getString}
|
||||
source={getString('repoEmptyMarkdownClone')
|
||||
.replace(/REPO_URL/g, repoMetadata.git_url || '')
|
||||
.replace(/REPO_NAME/g, repoMetadata.uid || '')}
|
||||
@ -173,6 +174,7 @@ const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'resourc
|
||||
padding={{ top: 'xxlarge', bottom: 'xxlarge', left: 'xxlarge', right: 'xxlarge' }}
|
||||
className={css.divContainer}>
|
||||
<MarkdownViewer
|
||||
getString={getString}
|
||||
source={getString('repoEmptyMarkdownExisting')
|
||||
.replace(/REPO_URL/g, '...')
|
||||
.replace(/REPO_NAME/g, repoMetadata.uid || '')
|
||||
|
@ -4,8 +4,9 @@ import { Render } from 'react-jsx-match'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useGet } from 'restful-react'
|
||||
import cx from 'classnames'
|
||||
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent, TypesRepository } from 'services/code'
|
||||
import { useShowRequestError } from 'hooks/useShowRequestError'
|
||||
import { CodeIcon, decodeGitContent } from 'utils/GitUtils'
|
||||
@ -21,6 +22,7 @@ interface FolderContentProps {
|
||||
|
||||
function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: FolderContentProps) {
|
||||
const history = useHistory()
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
|
||||
const { data, error, loading } = useGet<OpenapiGetContentOutput>({
|
||||
@ -61,7 +63,7 @@ function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: F
|
||||
|
||||
<Render when={(data?.content as RepoFileContent)?.data}>
|
||||
<Container className={css.readmeContent}>
|
||||
<MarkdownViewer source={decodeGitContent((data?.content as RepoFileContent)?.data)} />
|
||||
<MarkdownViewer source={decodeGitContent((data?.content as RepoFileContent)?.data)} getString={getString} />
|
||||
</Container>
|
||||
</Render>
|
||||
</Container>
|
||||
|
Loading…
Reference in New Issue
Block a user