Merge branch 'pr-loading' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#6)

This commit is contained in:
Tan Nhu 2023-03-16 19:15:18 +00:00 committed by Harness
commit 7344959cbb
14 changed files with 152 additions and 106 deletions

View File

@ -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>

View File

@ -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

View 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);
}
}
}
}
}
}

View File

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

View 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>
)
}

View File

@ -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] {

View File

@ -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

View File

@ -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>
)

View File

@ -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'>

View File

@ -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

View File

@ -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}

View File

@ -61,6 +61,7 @@ export function PullRequestsContentHeader({
setSearchTerm(value)
onSearchTermChanged(value)
}}
spinnerPosition="right"
/>
<FlexExpander />
<Button

View File

@ -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 || '')

View File

@ -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>