mirror of
https://github.com/harness/drone.git
synced 2025-05-20 02:50:05 +08:00
Add PR listing page + Commits tab for PR detail page (#123)
* Add PR listing page + Commits tab for PR detail page * Add commits place-holder in Compare view * Add commits place-holder in Compare view * Correct PR number after creation * Minor CSS improvement * Add diff sample * Diff side by side (3.4.2 is better than latest) * Big PR diff example * Implement diff view * Scrolling optimization * Add placeholder to allow click at line number
This commit is contained in:
parent
fe9118e074
commit
619fd2c9de
@ -56,6 +56,7 @@
|
|||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
"cron-validator": "^1.2.1",
|
"cron-validator": "^1.2.1",
|
||||||
"cronstrue": "^1.114.0",
|
"cronstrue": "^1.114.0",
|
||||||
|
"diff2html": "3.4.22",
|
||||||
"event-source-polyfill": "^1.0.22",
|
"event-source-polyfill": "^1.0.22",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"github-markdown-css": "^5.1.0",
|
"github-markdown-css": "^5.1.0",
|
||||||
@ -83,6 +84,7 @@
|
|||||||
"react-contenteditable": "^3.3.5",
|
"react-contenteditable": "^3.3.5",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-draggable": "^4.4.2",
|
"react-draggable": "^4.4.2",
|
||||||
|
"react-intersection-observer": "^9.4.1",
|
||||||
"react-join": "^1.1.4",
|
"react-join": "^1.1.4",
|
||||||
"react-keywords": "^0.0.5",
|
"react-keywords": "^0.0.5",
|
||||||
"react-lottie-player": "^1.4.0",
|
"react-lottie-player": "^1.4.0",
|
||||||
|
@ -8,6 +8,7 @@ export interface CODEProps {
|
|||||||
branch?: string
|
branch?: string
|
||||||
diffRefs?: string
|
diffRefs?: string
|
||||||
pullRequestId?: string
|
pullRequestId?: string
|
||||||
|
pullRequestSection?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CODEQueryProps {
|
export interface CODEQueryProps {
|
||||||
@ -21,7 +22,8 @@ export const pathProps: Readonly<Omit<Required<CODEProps>, 'repoPath' | 'branch'
|
|||||||
resourcePath: ':resourcePath*',
|
resourcePath: ':resourcePath*',
|
||||||
commitRef: ':commitRef*',
|
commitRef: ':commitRef*',
|
||||||
diffRefs: ':diffRefs*',
|
diffRefs: ':diffRefs*',
|
||||||
pullRequestId: ':pullRequestId'
|
pullRequestId: ':pullRequestId',
|
||||||
|
pullRequestSection: ':pullRequestSection*'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CODERoutes {
|
export interface CODERoutes {
|
||||||
@ -32,7 +34,12 @@ export interface CODERoutes {
|
|||||||
toCODEFileEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'gitRef' | 'resourcePath'>>) => string
|
toCODEFileEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'gitRef' | 'resourcePath'>>) => string
|
||||||
toCODECommits: (args: Required<Pick<CODEProps, 'repoPath' | 'commitRef'>>) => string
|
toCODECommits: (args: Required<Pick<CODEProps, 'repoPath' | 'commitRef'>>) => string
|
||||||
toCODEPullRequests: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODEPullRequests: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODEPullRequest: (args: Required<Pick<CODEProps, 'repoPath' | 'pullRequestId'>>) => string
|
toCODEPullRequest: (
|
||||||
|
args: RequiredField<
|
||||||
|
Pick<CODEProps, 'repoPath' | 'pullRequestId' | 'pullRequestSection'>,
|
||||||
|
'repoPath' | 'pullRequestId'
|
||||||
|
>
|
||||||
|
) => string
|
||||||
toCODECompare: (args: Required<Pick<CODEProps, 'repoPath' | 'diffRefs'>>) => string
|
toCODECompare: (args: Required<Pick<CODEProps, 'repoPath' | 'diffRefs'>>) => string
|
||||||
toCODEBranches: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODEBranches: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODESettings: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODESettings: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
@ -48,7 +55,8 @@ export const routes: CODERoutes = {
|
|||||||
toCODEFileEdit: ({ repoPath, gitRef, resourcePath }) => `/${repoPath}/edit/${gitRef}/~/${resourcePath}`,
|
toCODEFileEdit: ({ repoPath, gitRef, resourcePath }) => `/${repoPath}/edit/${gitRef}/~/${resourcePath}`,
|
||||||
toCODECommits: ({ repoPath, commitRef }) => `/${repoPath}/commits/${commitRef}`,
|
toCODECommits: ({ repoPath, commitRef }) => `/${repoPath}/commits/${commitRef}`,
|
||||||
toCODEPullRequests: ({ repoPath }) => `/${repoPath}/pulls`,
|
toCODEPullRequests: ({ repoPath }) => `/${repoPath}/pulls`,
|
||||||
toCODEPullRequest: ({ repoPath, pullRequestId }) => `/${repoPath}/pulls/${pullRequestId}`,
|
toCODEPullRequest: ({ repoPath, pullRequestId, pullRequestSection }) =>
|
||||||
|
`/${repoPath}/pulls/${pullRequestId}${pullRequestSection ? '/' + pullRequestSection : ''}`,
|
||||||
toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`,
|
toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`,
|
||||||
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
||||||
toCODESettings: ({ repoPath }) => `/${repoPath}/settings`,
|
toCODESettings: ({ repoPath }) => `/${repoPath}/settings`,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { CSSProperties } from 'react'
|
import React, { CSSProperties } from 'react'
|
||||||
import { Container, Popover, Text } from '@harness/uicore'
|
import { Container, Popover, StringSubstitute, Text } from '@harness/uicore'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { StringSubstitute } from 'components/StringSubstitute/StringSubstitute'
|
|
||||||
import css from './CommitDivergence.module.scss'
|
import css from './CommitDivergence.module.scss'
|
||||||
|
|
||||||
interface CommitDivergenceProps {
|
interface CommitDivergenceProps {
|
||||||
|
@ -8,13 +8,13 @@ import type { RepoCommit } from 'services/code'
|
|||||||
import { CommitActions } from 'components/CommitActions/CommitActions'
|
import { CommitActions } from 'components/CommitActions/CommitActions'
|
||||||
import { formatDate } from 'utils/Utils'
|
import { formatDate } from 'utils/Utils'
|
||||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||||
import css from './CommitsContent.module.scss'
|
import css from './CommitsView.module.scss'
|
||||||
|
|
||||||
interface CommitsContentProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
interface CommitsViewProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
||||||
commits: RepoCommit[]
|
commits: RepoCommit[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommitsContent({ repoMetadata, commits }: CommitsContentProps) {
|
export function CommitsView({ repoMetadata, commits }: CommitsViewProps) {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { routes } = useAppContext()
|
const { routes } = useAppContext()
|
||||||
const columns: Column<RepoCommit>[] = useMemo(
|
const columns: Column<RepoCommit>[] = useMemo(
|
@ -0,0 +1,8 @@
|
|||||||
|
.spinner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
}
|
6
web/src/components/ContainerSpinner/ContainerSpinner.module.scss.d.ts
vendored
Normal file
6
web/src/components/ContainerSpinner/ContainerSpinner.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// this is an auto-generated file
|
||||||
|
declare const styles: {
|
||||||
|
readonly spinner: string
|
||||||
|
}
|
||||||
|
export default styles
|
12
web/src/components/ContainerSpinner/ContainerSpinner.tsx
Normal file
12
web/src/components/ContainerSpinner/ContainerSpinner.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import { Container, PageSpinner } from '@harness/uicore'
|
||||||
|
import css from './ContainerSpinner.module.scss'
|
||||||
|
|
||||||
|
export const ContainerSpinner: React.FC<React.ComponentProps<typeof Container>> = ({ className, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Container className={cx(css.spinner, className)} {...props}>
|
||||||
|
<PageSpinner />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
106
web/src/components/DiffViewer/DiffViewer.module.scss
Normal file
106
web/src/components/DiffViewer/DiffViewer.module.scss
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
.main {
|
||||||
|
--border-color: var(--grey-200);
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 36px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.d2h-wrapper > div {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-file-wrapper {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-file-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-files-diff {
|
||||||
|
.d2h-code-side-linenumber {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-file-side-diff {
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-code-side-linenumber {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-diff-tbody {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
.diffHeader {
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffContent {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offscreen {
|
||||||
|
.diffContent {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffHeader {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
position: sticky;
|
||||||
|
top: var(--diff-viewer-sticky-top, 0);
|
||||||
|
z-index: 1;
|
||||||
|
padding: 5px 10px 5px 5px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
|
||||||
|
.fname {
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--grey-900) !important;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewLabel {
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--grey-600);
|
||||||
|
border: 1px solid var(--grey-200);
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: var(--spacing-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffContent {
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
|
||||||
|
max-width: calc(100vw - 320px);
|
||||||
|
|
||||||
|
// &[data-display='none'] {
|
||||||
|
// visibility: hidden;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
12
web/src/components/DiffViewer/DiffViewer.module.scss.d.ts
vendored
Normal file
12
web/src/components/DiffViewer/DiffViewer.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// this is an auto-generated file
|
||||||
|
declare const styles: {
|
||||||
|
readonly main: string
|
||||||
|
readonly collapsed: string
|
||||||
|
readonly diffHeader: string
|
||||||
|
readonly diffContent: string
|
||||||
|
readonly offscreen: string
|
||||||
|
readonly fname: string
|
||||||
|
readonly viewLabel: string
|
||||||
|
}
|
||||||
|
export default styles
|
285
web/src/components/DiffViewer/DiffViewer.tsx
Normal file
285
web/src/components/DiffViewer/DiffViewer.tsx
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useInView } from 'react-intersection-observer'
|
||||||
|
import { Button, Color, Container, FlexExpander, ButtonVariation, Layout, Text, ButtonSize } from '@harness/uicore'
|
||||||
|
import type * as Diff2Html from 'diff2html'
|
||||||
|
import HoganJsUtils from 'diff2html/lib/hoganjs-utils'
|
||||||
|
import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui'
|
||||||
|
import 'highlight.js/styles/github.css'
|
||||||
|
import 'diff2html/bundles/css/diff2html.min.css'
|
||||||
|
import type { DiffFile } from 'diff2html/lib/types'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { CodeIcon } from 'utils/GitUtils'
|
||||||
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
|
import css from './DiffViewer.module.scss'
|
||||||
|
|
||||||
|
export enum DiffViewStyle {
|
||||||
|
SPLIT = 'side-by-side',
|
||||||
|
UNIFIED = 'line-by-line'
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIFF_HEADER_HEIGHT = 36
|
||||||
|
const LINE_NUMBER_CLASS = 'diff-viewer-line-number'
|
||||||
|
|
||||||
|
export const DIFF2HTML_CONFIG = {
|
||||||
|
outputFormat: 'side-by-side',
|
||||||
|
drawFileList: false,
|
||||||
|
fileListStartVisible: false,
|
||||||
|
fileContentToggle: true,
|
||||||
|
matching: 'lines',
|
||||||
|
synchronisedScroll: true,
|
||||||
|
highlight: true,
|
||||||
|
renderNothingWhenEmpty: false,
|
||||||
|
compiledTemplates: {
|
||||||
|
'generic-line': HoganJsUtils.compile(`
|
||||||
|
<tr data-line="{{lineNumber}}">
|
||||||
|
<td class="{{lineClass}} {{type}} ${LINE_NUMBER_CLASS}">
|
||||||
|
{{{lineNumber}}}
|
||||||
|
</td>
|
||||||
|
<td class="{{type}}">
|
||||||
|
<div class="{{contentClass}}">
|
||||||
|
{{#prefix}}
|
||||||
|
<span class="d2h-code-line-prefix">{{{prefix}}}</span>
|
||||||
|
{{/prefix}}
|
||||||
|
{{^prefix}}
|
||||||
|
<span class="d2h-code-line-prefix"> </span>
|
||||||
|
{{/prefix}}
|
||||||
|
{{#content}}
|
||||||
|
<span class="d2h-code-line-ctn">{{{content}}}</span>
|
||||||
|
{{/content}}
|
||||||
|
{{^content}}
|
||||||
|
<span class="d2h-code-line-ctn"><br></span>
|
||||||
|
{{/content}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`),
|
||||||
|
'line-by-line-numbers': HoganJsUtils.compile(`
|
||||||
|
<div class="line-num1 ${LINE_NUMBER_CLASS}">{{oldNumber}}</div>
|
||||||
|
<div class="line-num2 ${LINE_NUMBER_CLASS}">{{newNumber}}</div>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
} as Readonly<Diff2Html.Diff2HtmlConfig>
|
||||||
|
|
||||||
|
interface DiffViewerProps {
|
||||||
|
index: number
|
||||||
|
diff: DiffFile
|
||||||
|
viewStyle: DiffViewStyle
|
||||||
|
stickyTopPosition?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Note: Lots of direct DOM manipulations are used to boost performance
|
||||||
|
// Avoid React re-rendering at all cost as it might cause unresponsive UI
|
||||||
|
// when diff content is big, or when a PR has a lot of changed files.
|
||||||
|
//
|
||||||
|
export const DiffViewer: React.FC<DiffViewerProps> = ({ index, diff, viewStyle, stickyTopPosition = 0 }) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const [viewed, setViewed] = useState(false)
|
||||||
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
|
const [fileUnchanged] = useState(diff.unchangedPercentage === 100)
|
||||||
|
const [fileDeleted] = useState(diff.isDeleted)
|
||||||
|
const [renderCustomContent, setRenderCustomContent] = useState(fileUnchanged || fileDeleted)
|
||||||
|
const containerId = useMemo(() => `file-diff-container-${index}`, [index])
|
||||||
|
const contentId = useMemo(() => `${containerId}-content`, [containerId])
|
||||||
|
const [height, setHeight] = useState<number | string>('auto')
|
||||||
|
const [diffRenderer, setDiffRenderer] = useState<Diff2HtmlUI>()
|
||||||
|
const { ref: inViewRef, inView } = useInView({ rootMargin: '100px 0px' })
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const setContainerRef = useCallback(
|
||||||
|
node => {
|
||||||
|
containerRef.current = node
|
||||||
|
inViewRef(node)
|
||||||
|
},
|
||||||
|
[inViewRef]
|
||||||
|
)
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const setupViewerInitialStates = useCallback(() => {
|
||||||
|
setDiffRenderer(
|
||||||
|
new Diff2HtmlUI(
|
||||||
|
document.getElementById(contentId) as HTMLElement,
|
||||||
|
[diff],
|
||||||
|
Object.assign({}, DIFF2HTML_CONFIG, {
|
||||||
|
outputFormat: viewStyle
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, [diff, contentId, viewStyle])
|
||||||
|
const renderDiffAndUpdateContainerHeightIfNeeded = useCallback(
|
||||||
|
(enforced = false) => {
|
||||||
|
const contentDOM = contentRef.current as HTMLDivElement
|
||||||
|
const containerDOM = containerRef.current as HTMLDivElement
|
||||||
|
|
||||||
|
if (!contentDOM.dataset.rendered || enforced) {
|
||||||
|
if (!renderCustomContent || enforced) {
|
||||||
|
containerDOM.style.height = 'auto'
|
||||||
|
diffRenderer?.draw()
|
||||||
|
}
|
||||||
|
contentDOM.dataset.rendered = 'true'
|
||||||
|
setHeight(containerDOM.clientHeight)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[diffRenderer, renderCustomContent]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function createDiffRenderer() {
|
||||||
|
if (inView && !diffRenderer) {
|
||||||
|
setupViewerInitialStates()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[inView, diffRenderer, setupViewerInitialStates]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function renderInitialContent() {
|
||||||
|
if (diffRenderer) {
|
||||||
|
const container = containerRef.current as HTMLDivElement
|
||||||
|
const { classList: containerClassList } = container
|
||||||
|
|
||||||
|
if (inView) {
|
||||||
|
containerClassList.remove(css.offscreen)
|
||||||
|
renderDiffAndUpdateContainerHeightIfNeeded()
|
||||||
|
} else {
|
||||||
|
containerClassList.add(css.offscreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[inView, diffRenderer, renderDiffAndUpdateContainerHeightIfNeeded]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function handleCollapsedState() {
|
||||||
|
const containerDOM = containerRef.current as HTMLDivElement & { scrollIntoViewIfNeeded: () => void }
|
||||||
|
const { classList: containerClassList, style: containerStyle } = containerDOM
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
containerClassList.add(css.collapsed)
|
||||||
|
|
||||||
|
// Fix scrolling position messes up with sticky header
|
||||||
|
const { y } = containerDOM.getBoundingClientRect()
|
||||||
|
if (y - stickyTopPosition < 1) {
|
||||||
|
containerDOM.scrollIntoView()
|
||||||
|
|
||||||
|
if (stickyTopPosition) {
|
||||||
|
window.scroll({ top: window.scrollY - stickyTopPosition })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseInt(containerStyle.height) != DIFF_HEADER_HEIGHT) {
|
||||||
|
containerStyle.height = `${DIFF_HEADER_HEIGHT}px`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
containerClassList.remove(css.collapsed)
|
||||||
|
|
||||||
|
if (parseInt(containerStyle.height) != height) {
|
||||||
|
containerStyle.height = `${height}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[collapsed, height, stickyTopPosition]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(function () {
|
||||||
|
const onClick = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLDivElement
|
||||||
|
|
||||||
|
if (target.classList.contains(LINE_NUMBER_CLASS)) {
|
||||||
|
console.log('line number clicked')
|
||||||
|
}
|
||||||
|
console.log({ event, target })
|
||||||
|
}
|
||||||
|
const containerDOM = containerRef.current as HTMLDivElement
|
||||||
|
containerDOM.addEventListener('click', onClick)
|
||||||
|
return () => {
|
||||||
|
containerDOM.removeEventListener('click', onClick)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
ref={setContainerRef}
|
||||||
|
id={containerId}
|
||||||
|
className={css.main}
|
||||||
|
style={{ '--diff-viewer-sticky-top': `${stickyTopPosition}px` } as React.CSSProperties}>
|
||||||
|
<Layout.Vertical>
|
||||||
|
<Container className={css.diffHeader} height={DIFF_HEADER_HEIGHT}>
|
||||||
|
<Layout.Horizontal>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.ICON}
|
||||||
|
icon={collapsed ? 'chevron-right' : 'chevron-down'}
|
||||||
|
size={ButtonSize.SMALL}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
/>
|
||||||
|
<Container style={{ alignSelf: 'center' }} padding={{ right: 'small' }}>
|
||||||
|
<Layout.Horizontal spacing="xsmall">
|
||||||
|
{!!diff.addedLines && (
|
||||||
|
<Text color={Color.GREEN_600} style={{ fontSize: '12px' }}>
|
||||||
|
+{diff.addedLines}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!!diff.addedLines && !!diff.deletedLines && <PipeSeparator height={8} />}
|
||||||
|
{!!diff.deletedLines && (
|
||||||
|
<Text color={Color.RED_500} style={{ fontSize: '12px' }}>
|
||||||
|
-{diff.deletedLines}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
<Text inline className={css.fname}>
|
||||||
|
{diff.isDeleted ? diff.oldName : diff.isRename ? `${diff.oldName} -> ${diff.newName}` : diff.newName}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.ICON}
|
||||||
|
icon={CodeIcon.Copy}
|
||||||
|
size={ButtonSize.SMALL}
|
||||||
|
tooltip={
|
||||||
|
<Container style={{ overflow: 'auto', width: 500, height: 400 }}>
|
||||||
|
<pre>{JSON.stringify(diff, null, 2)}</pre>
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FlexExpander />
|
||||||
|
<Container>
|
||||||
|
<label className={css.viewLabel}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value="viewed"
|
||||||
|
checked={viewed}
|
||||||
|
onChange={() => {
|
||||||
|
setViewed(!viewed)
|
||||||
|
setCollapsed(!viewed)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Viewed
|
||||||
|
</label>
|
||||||
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container id={contentId} className={css.diffContent} ref={contentRef}>
|
||||||
|
{renderCustomContent && (
|
||||||
|
<Container>
|
||||||
|
<Layout.Vertical
|
||||||
|
padding="xlarge"
|
||||||
|
style={{
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
{fileDeleted && (
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.LINK}
|
||||||
|
onClick={() => {
|
||||||
|
setRenderCustomContent(false)
|
||||||
|
setTimeout(() => renderDiffAndUpdateContainerHeightIfNeeded(true), 0)
|
||||||
|
}}>
|
||||||
|
{getString('pr.showDiff')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Text>{getString(fileDeleted ? 'pr.fileDeleted' : 'pr.fileUnchanged')}</Text>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -1,66 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
|
|
||||||
type SubstituteVars = Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
|
|
||||||
function translateExpression(str: string, key: string, vars: SubstituteVars) {
|
|
||||||
// Replace simple i18n expression {key|match1:value1,match2:value2}
|
|
||||||
// Sample: '{user} wants to merge {number} {number|1:commit,commits} into {target} from {source}'
|
|
||||||
// Find `{number|`
|
|
||||||
const startIndex = str.indexOf(`{${key}|`)
|
|
||||||
const MATCH_ELSE_KEY = '___'
|
|
||||||
|
|
||||||
if (startIndex !== -1) {
|
|
||||||
// Find closing `}`
|
|
||||||
const endIndex = str.indexOf('}', startIndex)
|
|
||||||
|
|
||||||
if (endIndex !== -1) {
|
|
||||||
// Get whole expression of `{number|1:commit,commits}`
|
|
||||||
const expression = str.substring(startIndex, endIndex + 1)
|
|
||||||
|
|
||||||
// Build value mapping from expression
|
|
||||||
const mapping = expression
|
|
||||||
.split('|')[1] // Get `1:commit,commits}`
|
|
||||||
.slice(0, -1) // Remove last closing `}`
|
|
||||||
.split(',') // ['1:commit', 'commits']
|
|
||||||
.reduce((map, token) => {
|
|
||||||
// Convert to a map { 1: commit, [MATCH_ELSE_KEY]: commits }
|
|
||||||
const [k, v] = token.split(':')
|
|
||||||
map[v ? k : MATCH_ELSE_KEY] = v || k
|
|
||||||
return map
|
|
||||||
}, {} as Record<string, string>)
|
|
||||||
|
|
||||||
const matchedValue = mapping[vars[key]] || mapping[MATCH_ELSE_KEY]
|
|
||||||
|
|
||||||
if (matchedValue) {
|
|
||||||
return str.replace(expression, matchedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StringSubstitute: React.FC<{
|
|
||||||
str: string
|
|
||||||
vars: SubstituteVars
|
|
||||||
}> = ({ str, vars }) => {
|
|
||||||
const re = Object.keys(vars)
|
|
||||||
.map(key => {
|
|
||||||
str = translateExpression(str, key, vars)
|
|
||||||
return `{${key}}`
|
|
||||||
})
|
|
||||||
.join('|')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{str
|
|
||||||
.split(new RegExp('(' + re + ')', 'gi'))
|
|
||||||
.filter(token => !!(token || '').trim())
|
|
||||||
.map((token, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
{token.startsWith('{') && token.endsWith('}') ? vars[token.substring(1, token.length - 1)] || token : token}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ export interface StringsMap {
|
|||||||
addGitIgnore: string
|
addGitIgnore: string
|
||||||
addLicense: string
|
addLicense: string
|
||||||
addReadMe: string
|
addReadMe: string
|
||||||
|
all: string
|
||||||
allBranches: string
|
allBranches: string
|
||||||
allEvents: string
|
allEvents: string
|
||||||
botAlerts: string
|
botAlerts: string
|
||||||
@ -31,6 +32,7 @@ export interface StringsMap {
|
|||||||
checkSuites: string
|
checkSuites: string
|
||||||
clone: string
|
clone: string
|
||||||
cloneHTTPS: string
|
cloneHTTPS: string
|
||||||
|
closed: string
|
||||||
commit: string
|
commit: string
|
||||||
commitChanges: string
|
commitChanges: string
|
||||||
commitDirectlyTo: string
|
commitDirectlyTo: string
|
||||||
@ -91,6 +93,7 @@ export interface StringsMap {
|
|||||||
inactiveBranches: string
|
inactiveBranches: string
|
||||||
individualEvents: string
|
individualEvents: string
|
||||||
loading: string
|
loading: string
|
||||||
|
merged: string
|
||||||
name: string
|
name: string
|
||||||
nameYourBranch: string
|
nameYourBranch: string
|
||||||
nameYourFile: string
|
nameYourFile: string
|
||||||
@ -105,6 +108,7 @@ export interface StringsMap {
|
|||||||
none: string
|
none: string
|
||||||
ok: string
|
ok: string
|
||||||
onDate: string
|
onDate: string
|
||||||
|
open: string
|
||||||
optional: string
|
optional: string
|
||||||
optionalExtendedDescription: string
|
optionalExtendedDescription: string
|
||||||
pageNotFound: string
|
pageNotFound: string
|
||||||
@ -115,10 +119,22 @@ export interface StringsMap {
|
|||||||
'pr.buttonText': string
|
'pr.buttonText': string
|
||||||
'pr.cantMerge': string
|
'pr.cantMerge': string
|
||||||
'pr.descriptionPlaceHolder': string
|
'pr.descriptionPlaceHolder': string
|
||||||
|
'pr.diffStatus': string
|
||||||
|
'pr.diffView': string
|
||||||
'pr.failedToCreate': string
|
'pr.failedToCreate': string
|
||||||
|
'pr.fileDeleted': string
|
||||||
|
'pr.fileUnchanged': string
|
||||||
'pr.metaLine': string
|
'pr.metaLine': string
|
||||||
'pr.modalTitle': string
|
'pr.modalTitle': string
|
||||||
|
'pr.openBy': string
|
||||||
|
'pr.reviewChanges': string
|
||||||
|
'pr.showDiff': string
|
||||||
|
'pr.showLabel': string
|
||||||
|
'pr.showLink': string
|
||||||
|
'pr.split': string
|
||||||
|
'pr.state': string
|
||||||
'pr.titlePlaceHolder': string
|
'pr.titlePlaceHolder': string
|
||||||
|
'pr.unified': string
|
||||||
prefixBase: string
|
prefixBase: string
|
||||||
prefixCompare: string
|
prefixCompare: string
|
||||||
prev: string
|
prev: string
|
||||||
@ -128,6 +144,7 @@ export interface StringsMap {
|
|||||||
pullRequestEmpty: string
|
pullRequestEmpty: string
|
||||||
pullRequests: string
|
pullRequests: string
|
||||||
pushEvent: string
|
pushEvent: string
|
||||||
|
rejected: string
|
||||||
renameFile: string
|
renameFile: string
|
||||||
'repos.activities': string
|
'repos.activities': string
|
||||||
'repos.data': string
|
'repos.data': string
|
||||||
@ -139,6 +156,7 @@ export interface StringsMap {
|
|||||||
repositories: string
|
repositories: string
|
||||||
samplePayloadUrl: string
|
samplePayloadUrl: string
|
||||||
scanAlerts: string
|
scanAlerts: string
|
||||||
|
scrollToTop: string
|
||||||
search: string
|
search: string
|
||||||
searchBranches: string
|
searchBranches: string
|
||||||
secret: string
|
secret: string
|
||||||
@ -165,4 +183,5 @@ export interface StringsMap {
|
|||||||
webhookListingContent: string
|
webhookListingContent: string
|
||||||
webhooks: string
|
webhooks: string
|
||||||
yourBranches: string
|
yourBranches: string
|
||||||
|
yours: string
|
||||||
}
|
}
|
||||||
|
19
web/src/hooks/useUserPreference.ts
Normal file
19
web/src/hooks/useUserPreference.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
|
export enum UserPreference {
|
||||||
|
DIFF_VIEW_STYLE = 'DIFF_VIEW_STYLE'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserPreference(key: UserPreference, defaultValue: string) {
|
||||||
|
const prefKey = `CODE_MODULE_USER_PREFERENCE_${key}`
|
||||||
|
const [preference, setPreference] = useState(localStorage[prefKey] || defaultValue)
|
||||||
|
const savePreference = useCallback(
|
||||||
|
val => {
|
||||||
|
localStorage[prefKey] = val
|
||||||
|
setPreference(val)
|
||||||
|
},
|
||||||
|
[prefKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [preference, savePreference]
|
||||||
|
}
|
@ -161,6 +161,25 @@ pr:
|
|||||||
modalTitle: Open a pull request
|
modalTitle: Open a pull request
|
||||||
buttonText: Open pull request
|
buttonText: Open pull request
|
||||||
metaLine: '{user} wants to merge {number} {number|1:commit,commits} into {target} from {source}'
|
metaLine: '{user} wants to merge {number} {number|1:commit,commits} into {target} from {source}'
|
||||||
|
state: '{state|closed:Closed,merged:Merged,rejected:Rejected,Open}'
|
||||||
|
openBy: '#{number} opened {time} by {user}'
|
||||||
|
diffStatus: '{status|deleted:Deleted,new:Added,renamed:Renamed,copied:Copied,Changed}'
|
||||||
|
showDiff: Show Diff
|
||||||
|
fileDeleted: This file was deleted.
|
||||||
|
fileUnchanged: File without changes.
|
||||||
|
showLink: '{count} changed {count|1:file,files}'
|
||||||
|
showLabel: 'Showing {showLink} with {addedLines} {addedLines|1:addition,additions} and {deletedLines} {deletedLines|1:deletion,deletions} {config}'
|
||||||
|
diffView: Diff View
|
||||||
|
split: Split
|
||||||
|
unified: Unified
|
||||||
|
reviewChanges: Review changes
|
||||||
webhookListingContent: 'create,delete,deployment ...'
|
webhookListingContent: 'create,delete,deployment ...'
|
||||||
general: 'General'
|
general: 'General'
|
||||||
webhooks: 'Webhooks'
|
webhooks: 'Webhooks'
|
||||||
|
open: Open
|
||||||
|
merged: Merged
|
||||||
|
closed: Closed
|
||||||
|
rejected: Rejected
|
||||||
|
yours: Yours
|
||||||
|
all: All
|
||||||
|
scrollToTop: Scroll to top
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Container, PageBody, NoDataCard, Tabs } from '@harness/uicore'
|
import { Container, PageBody, NoDataCard, Tabs } from '@harness/uicore'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { useGet } from 'restful-react'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
@ -8,6 +9,8 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
|||||||
import { getErrorMessage } from 'utils/Utils'
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
import emptyStateImage from 'images/empty-state.svg'
|
import emptyStateImage from 'images/empty-state.svg'
|
||||||
import { makeDiffRefs } from 'utils/GitUtils'
|
import { makeDiffRefs } from 'utils/GitUtils'
|
||||||
|
import { CommitsView } from 'components/CommitsView/CommitsView'
|
||||||
|
import type { RepoCommit } from 'services/code'
|
||||||
import { CompareContentHeader } from './CompareContentHeader/CompareContentHeader'
|
import { CompareContentHeader } from './CompareContentHeader/CompareContentHeader'
|
||||||
import css from './Compare.module.scss'
|
import css from './Compare.module.scss'
|
||||||
|
|
||||||
@ -18,6 +21,18 @@ export default function Compare() {
|
|||||||
const { repoMetadata, error, loading, refetch, diffRefs } = useGetRepositoryMetadata()
|
const { repoMetadata, error, loading, refetch, diffRefs } = useGetRepositoryMetadata()
|
||||||
const [sourceGitRef, setSourceGitRef] = useState(diffRefs.sourceGitRef)
|
const [sourceGitRef, setSourceGitRef] = useState(diffRefs.sourceGitRef)
|
||||||
const [targetGitRef, setTargetGitRef] = useState(diffRefs.targetGitRef)
|
const [targetGitRef, setTargetGitRef] = useState(diffRefs.targetGitRef)
|
||||||
|
const {
|
||||||
|
data: commits,
|
||||||
|
error: commitsError,
|
||||||
|
loading: commitsLoading,
|
||||||
|
refetch: commitsRefetch
|
||||||
|
} = useGet<RepoCommit[]>({
|
||||||
|
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
|
||||||
|
queryParams: {
|
||||||
|
git_ref: sourceGitRef
|
||||||
|
},
|
||||||
|
lazy: !repoMetadata
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
@ -60,22 +75,28 @@ export default function Compare() {
|
|||||||
</Container>
|
</Container>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!!targetGitRef && !!sourceGitRef && (
|
{!!repoMetadata && !!targetGitRef && !!sourceGitRef && (
|
||||||
<Container className={css.tabsContainer} padding="xlarge">
|
<Container className={css.tabsContainer} padding="xlarge">
|
||||||
<Tabs
|
<Tabs
|
||||||
id="branchesTags"
|
id="branchesTags"
|
||||||
defaultSelectedTabId={'diff'}
|
defaultSelectedTabId={'commits'}
|
||||||
large={false}
|
large={false}
|
||||||
tabList={[
|
tabList={[
|
||||||
{
|
{
|
||||||
id: 'commits',
|
id: 'commits',
|
||||||
title: getString('commits'),
|
title: getString('commits'),
|
||||||
panel: <div>Commit</div>
|
panel: commits?.length ? (
|
||||||
|
<Container padding={{ top: 'xlarge' }}>
|
||||||
|
<CommitsView commits={commits} repoMetadata={repoMetadata} />
|
||||||
|
</Container>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'diff',
|
id: 'diff',
|
||||||
title: getString('diff'),
|
title: getString('diff'),
|
||||||
panel: <div>Diff</div>
|
panel: <Container padding={{ top: 'xlarge' }}>To be defined...</Container>
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -60,7 +60,7 @@ export function CompareContentHeader({
|
|||||||
history.replace(
|
history.replace(
|
||||||
routes.toCODEPullRequest({
|
routes.toCODEPullRequest({
|
||||||
repoPath: repoMetadata.path as string,
|
repoPath: repoMetadata.path as string,
|
||||||
pullRequestId: String(data.id)
|
pullRequestId: String(data.number)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
@ -9,6 +9,14 @@
|
|||||||
.tabsContainer {
|
.tabsContainer {
|
||||||
padding-top: var(--spacing-xsmall);
|
padding-top: var(--spacing-xsmall);
|
||||||
|
|
||||||
|
// TODO: This looks bad and does not
|
||||||
|
// perform well. Need a way to fix
|
||||||
|
:global {
|
||||||
|
.Tabs--main .bp3-tab:not([aria-disabled='true']):hover .bp3-icon svg path {
|
||||||
|
fill: var(--grey-50) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[role='tablist'] {
|
[role='tablist'] {
|
||||||
background-color: var(--white) !important;
|
background-color: var(--white) !important;
|
||||||
padding-left: var(--spacing-medium);
|
padding-left: var(--spacing-medium);
|
||||||
@ -21,9 +29,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[aria-selected='true'] {
|
[aria-selected='true'] {
|
||||||
.tabTitle {
|
.tabTitle,
|
||||||
color: var(--grey-900);
|
.tabTitle:hover {
|
||||||
font-weight: 600;
|
color: var(--grey-900) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,3 +60,8 @@
|
|||||||
color: var(--grey-500);
|
color: var(--grey-500);
|
||||||
padding-left: var(--spacing-small);
|
padding-left: var(--spacing-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabContentContainer {
|
||||||
|
min-height: calc(100vh - 153px) !important;
|
||||||
|
background-color: var(--primary-bg) !important;
|
||||||
|
}
|
||||||
|
@ -6,5 +6,6 @@ declare const styles: {
|
|||||||
readonly tabTitle: string
|
readonly tabTitle: string
|
||||||
readonly count: string
|
readonly count: string
|
||||||
readonly prNumber: string
|
readonly prNumber: string
|
||||||
|
readonly tabContentContainer: string
|
||||||
}
|
}
|
||||||
export default styles
|
export default styles
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Container, PageBody, Text, FontVariation, Tabs, IconName } from '@harness/uicore'
|
import { Container, PageBody, Text, FontVariation, Tabs, IconName } from '@harness/uicore'
|
||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
@ -8,16 +9,30 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
|||||||
import { getErrorMessage } from 'utils/Utils'
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
import type { PullRequestResponse } from 'utils/types'
|
import type { PullRequestResponse } from 'utils/types'
|
||||||
import { CodeIcon } from 'utils/GitUtils'
|
import { CodeIcon } from 'utils/GitUtils'
|
||||||
import { PullRequestMetadataInfo } from './PullRequestMetadataInfo'
|
import { PullRequestMetaLine } from './PullRequestMetaLine'
|
||||||
import { PullRequestConversation } from './PullRequestConversation/PullRequestConversation'
|
import { PullRequestConversation } from './PullRequestConversation/PullRequestConversation'
|
||||||
import { PullRequestDiff } from './PullRequestDiff/PullRequestDiff'
|
import { PullRequestDiff } from './PullRequestDiff/PullRequestDiff'
|
||||||
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
||||||
import css from './PullRequest.module.scss'
|
import css from './PullRequest.module.scss'
|
||||||
|
|
||||||
|
enum PullRequestSection {
|
||||||
|
CONVERSATION = 'conversation',
|
||||||
|
COMMITS = 'commits',
|
||||||
|
DIFFS = 'diffs'
|
||||||
|
}
|
||||||
|
|
||||||
export default function PullRequest() {
|
export default function PullRequest() {
|
||||||
|
const history = useHistory()
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { routes } = useAppContext()
|
const { routes } = useAppContext()
|
||||||
const { repoMetadata, error, loading, refetch, pullRequestId } = useGetRepositoryMetadata()
|
const {
|
||||||
|
repoMetadata,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
pullRequestId,
|
||||||
|
pullRequestSection = PullRequestSection.CONVERSATION
|
||||||
|
} = useGetRepositoryMetadata()
|
||||||
const {
|
const {
|
||||||
data: prData,
|
data: prData,
|
||||||
error: prError,
|
error: prError,
|
||||||
@ -26,6 +41,13 @@ export default function PullRequest() {
|
|||||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
|
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
|
||||||
lazy: !repoMetadata
|
lazy: !repoMetadata
|
||||||
})
|
})
|
||||||
|
const activeTab = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(PullRequestSection).find(value => value === pullRequestSection)
|
||||||
|
? pullRequestSection
|
||||||
|
: PullRequestSection.CONVERSATION,
|
||||||
|
[pullRequestSection]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
@ -46,26 +68,35 @@ export default function PullRequest() {
|
|||||||
{repoMetadata ? (
|
{repoMetadata ? (
|
||||||
prData ? (
|
prData ? (
|
||||||
<>
|
<>
|
||||||
<PullRequestMetadataInfo repoMetadata={repoMetadata} {...prData} />
|
<PullRequestMetaLine repoMetadata={repoMetadata} {...prData} />
|
||||||
<Container className={css.tabsContainer}>
|
<Container className={css.tabsContainer}>
|
||||||
<Tabs
|
<Tabs
|
||||||
id="pullRequestTabs"
|
id="pullRequestTabs"
|
||||||
defaultSelectedTabId={'conversation'}
|
defaultSelectedTabId={activeTab}
|
||||||
large={false}
|
large={false}
|
||||||
|
onChange={tabId => {
|
||||||
|
history.replace(
|
||||||
|
routes.toCODEPullRequest({
|
||||||
|
repoPath: repoMetadata.path as string,
|
||||||
|
pullRequestId,
|
||||||
|
pullRequestSection: tabId !== PullRequestSection.CONVERSATION ? (tabId as string) : undefined
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}}
|
||||||
tabList={[
|
tabList={[
|
||||||
{
|
{
|
||||||
id: 'conversation',
|
id: PullRequestSection.CONVERSATION,
|
||||||
title: <TabTitle icon={CodeIcon.Chat} title={getString('conversation')} count={100} />,
|
title: <TabTitle icon={CodeIcon.Chat} title={getString('conversation')} count={100} />,
|
||||||
panel: <PullRequestConversation repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
panel: <PullRequestConversation repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'commits',
|
id: PullRequestSection.COMMITS,
|
||||||
title: <TabTitle icon={CodeIcon.Commit} title={getString('commits')} count={15} />,
|
title: <TabTitle icon={CodeIcon.Commit} title={getString('commits')} count={15} />,
|
||||||
panel: <PullRequestCommits repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
panel: <PullRequestCommits repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'diff',
|
id: PullRequestSection.DIFFS,
|
||||||
title: <TabTitle icon={CodeIcon.Commit} title={getString('diff')} count={20} />,
|
title: <TabTitle icon={CodeIcon.File} title={getString('diff')} count={20} />,
|
||||||
panel: <PullRequestDiff repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
panel: <PullRequestDiff repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
@ -87,7 +118,7 @@ const PullRequestTitle: React.FC<PullRequestResponse> = ({ title, number }) => (
|
|||||||
|
|
||||||
const TabTitle: React.FC<{ icon: IconName; title: string; count?: number }> = ({ icon, title, count }) => (
|
const TabTitle: React.FC<{ icon: IconName; title: string; count?: number }> = ({ icon, title, count }) => (
|
||||||
<Text icon={icon} className={css.tabTitle}>
|
<Text icon={icon} className={css.tabTitle}>
|
||||||
{title}{' '}
|
{title}
|
||||||
{!!count && (
|
{!!count && (
|
||||||
<Text inline className={css.count}>
|
<Text inline className={css.count}>
|
||||||
{count}
|
{count}
|
||||||
|
6
web/src/pages/PullRequest/PullRequestCommits/PullRequestCommits.module.scss.d.ts
vendored
Normal file
6
web/src/pages/PullRequest/PullRequestCommits/PullRequestCommits.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
|
@ -1,20 +1,30 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Container } from '@harness/uicore'
|
import { useGet } from 'restful-react'
|
||||||
import { useAppContext } from 'AppContext'
|
import type { RepoCommit } from 'services/code'
|
||||||
import { useStrings } from 'framework/strings'
|
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
import css from './PullRequestCommits.module.scss'
|
import { CommitsView } from 'components/CommitsView/CommitsView'
|
||||||
|
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
|
||||||
|
|
||||||
export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
|
export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
pullRequestMetadata
|
pullRequestMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const { getString } = useStrings()
|
const {
|
||||||
const { routes } = useAppContext()
|
data: commits,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetch
|
||||||
|
} = useGet<RepoCommit[]>({
|
||||||
|
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
|
||||||
|
queryParams: {
|
||||||
|
git_ref: pullRequestMetadata.sourceBranch
|
||||||
|
},
|
||||||
|
lazy: !repoMetadata
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main} padding="xlarge">
|
<PullRequestTabContentWrapper loading={loading} error={error} onRetry={() => refetch()}>
|
||||||
COMMITS...
|
{!!commits?.length && <CommitsView commits={commits} repoMetadata={repoMetadata} />}
|
||||||
</Container>
|
</PullRequestTabContentWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Container } from '@harness/uicore'
|
import { Container } from '@harness/uicore'
|
||||||
import { useAppContext } from 'AppContext'
|
|
||||||
import { useStrings } from 'framework/strings'
|
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
import css from './PullRequestConversation.module.scss'
|
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||||
|
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
|
||||||
|
|
||||||
export const PullRequestConversation: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
|
export const PullRequestConversation: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
pullRequestMetadata
|
pullRequestMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const { getString } = useStrings()
|
|
||||||
const { routes } = useAppContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main} padding="xlarge">
|
<PullRequestTabContentWrapper loading={undefined} error={undefined} onRetry={() => {}}>
|
||||||
CONVERSATION...
|
<Container>
|
||||||
</Container>
|
<MarkdownViewer source={pullRequestMetadata.description} />
|
||||||
|
</Container>
|
||||||
|
</PullRequestTabContentWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,52 @@
|
|||||||
.main {
|
.wrapper {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
padding: var(--spacing-medium) 0 !important;
|
||||||
|
background-color: var(--white) !important;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showLabel {
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--grey-700) !important;
|
||||||
|
|
||||||
|
.showLabelLink {
|
||||||
|
all: unset !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--grey-700) !important;
|
||||||
|
color: var(--primary-7) !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
margin: 0 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filesMenu {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--spacing-xsmall) !important;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.bp3-menu-item:hover {
|
||||||
|
background-color: rgba(167, 182, 194, 0.3);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
web/src/pages/PullRequest/PullRequestDiff/PullRequestDiff.module.scss.d.ts
vendored
Normal file
12
web/src/pages/PullRequest/PullRequestDiff/PullRequestDiff.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// this is an auto-generated file
|
||||||
|
declare const styles: {
|
||||||
|
readonly wrapper: string
|
||||||
|
readonly header: string
|
||||||
|
readonly showLabel: string
|
||||||
|
readonly showLabelLink: string
|
||||||
|
readonly layout: string
|
||||||
|
readonly filesMenu: string
|
||||||
|
readonly menuItem: string
|
||||||
|
}
|
||||||
|
export default styles
|
@ -1,20 +1,227 @@
|
|||||||
import React from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { Container } from '@harness/uicore'
|
import {
|
||||||
import { useAppContext } from 'AppContext'
|
Container,
|
||||||
|
FlexExpander,
|
||||||
|
ButtonVariation,
|
||||||
|
Layout,
|
||||||
|
Text,
|
||||||
|
StringSubstitute,
|
||||||
|
FontVariation,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Color
|
||||||
|
} from '@harness/uicore'
|
||||||
|
import { ButtonGroup, Button as BButton, Classes, Menu, MenuItem } from '@blueprintjs/core'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import * as Diff2Html from 'diff2html'
|
||||||
|
// import { useAppContext } from 'AppContext'
|
||||||
|
// import { useStrings } from 'framework/strings'
|
||||||
|
import 'highlight.js/styles/github.css'
|
||||||
|
import 'diff2html/bundles/css/diff2html.min.css'
|
||||||
|
import type { DiffFile } from 'diff2html/lib/types'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||||
|
import { ButtonRoleProps, formatNumber, waitUntil } from 'utils/Utils'
|
||||||
|
import { DiffViewer, DIFF2HTML_CONFIG, DiffViewStyle } from 'components/DiffViewer/DiffViewer'
|
||||||
|
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
||||||
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
|
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
|
||||||
|
import diffExample from 'raw-loader!./example2.diff'
|
||||||
import css from './PullRequestDiff.module.scss'
|
import css from './PullRequestDiff.module.scss'
|
||||||
|
|
||||||
export const PullRequestDiff: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
|
const STICKY_TOP_POSITION = 64
|
||||||
repoMetadata,
|
|
||||||
pullRequestMetadata
|
export const PullRequestDiff: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = () => {
|
||||||
}) => {
|
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { routes } = useAppContext()
|
const [viewStyle, setViewStyle] = useUserPreference(UserPreference.DIFF_VIEW_STYLE, DiffViewStyle.SPLIT)
|
||||||
|
const [diffs, setDiffs] = useState<DiffFile[]>([])
|
||||||
|
const [stickyInAction, setStickyInAction] = useState(false)
|
||||||
|
const diffStats = useMemo(() => {
|
||||||
|
return (diffs || []).reduce(
|
||||||
|
(obj, diff) => {
|
||||||
|
obj.addedLines += diff.addedLines
|
||||||
|
obj.deletedLines += diff.deletedLines
|
||||||
|
return obj
|
||||||
|
},
|
||||||
|
{ addedLines: 0, deletedLines: 0 }
|
||||||
|
)
|
||||||
|
}, [diffs])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDiffs(Diff2Html.parse(diffExample, DIFF2HTML_CONFIG))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => {
|
||||||
|
if (window.scrollY >= 150) {
|
||||||
|
if (!stickyInAction) {
|
||||||
|
setStickyInAction(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (stickyInAction) {
|
||||||
|
setStickyInAction(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', onScroll)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', onScroll)
|
||||||
|
}
|
||||||
|
}, [stickyInAction])
|
||||||
|
|
||||||
|
// console.log({ diffs, viewStyle })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main} padding="xlarge">
|
<PullRequestTabContentWrapper loading={undefined} error={undefined} onRetry={noop} className={css.wrapper}>
|
||||||
DIFF
|
<Container className={css.header}>
|
||||||
</Container>
|
<Layout.Horizontal>
|
||||||
|
<Container flex={{ alignItems: 'center' }}>
|
||||||
|
<Text icon="accordion-collapsed" iconProps={{ size: 12 }} className={css.showLabel}>
|
||||||
|
<StringSubstitute
|
||||||
|
str={getString('pr.showLabel')}
|
||||||
|
vars={{
|
||||||
|
showLink: (
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.LINK}
|
||||||
|
className={css.showLabelLink}
|
||||||
|
tooltip={
|
||||||
|
<Menu className={css.filesMenu}>
|
||||||
|
{diffs?.map((diff, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={index}
|
||||||
|
className={css.menuItem}
|
||||||
|
icon={<Icon name={CodeIcon.File} padding={{ right: 'xsmall' }} />}
|
||||||
|
labelElement={
|
||||||
|
<Layout.Horizontal spacing="xsmall">
|
||||||
|
{!!diff.addedLines && (
|
||||||
|
<Text color={Color.GREEN_600} style={{ fontSize: '12px' }}>
|
||||||
|
+{diff.addedLines}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!!diff.addedLines && !!diff.deletedLines && <PipeSeparator height={8} />}
|
||||||
|
{!!diff.deletedLines && (
|
||||||
|
<Text color={Color.RED_500} style={{ fontSize: '12px' }}>
|
||||||
|
-{diff.deletedLines}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Layout.Horizontal>
|
||||||
|
}
|
||||||
|
text={
|
||||||
|
diff.isDeleted
|
||||||
|
? diff.oldName
|
||||||
|
: diff.isRename
|
||||||
|
? `${diff.oldName} -> ${diff.newName}`
|
||||||
|
: diff.newName
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
const containerDOM = document.getElementById(`file-diff-container-${index}`)
|
||||||
|
|
||||||
|
if (containerDOM) {
|
||||||
|
containerDOM.scrollIntoView()
|
||||||
|
waitUntil(
|
||||||
|
() => !!containerDOM.querySelector('[data-rendered="true"]'),
|
||||||
|
() => {
|
||||||
|
containerDOM.scrollIntoView()
|
||||||
|
// Fix scrolling position messes up with sticky header
|
||||||
|
const { y } = containerDOM.getBoundingClientRect()
|
||||||
|
if (y - STICKY_TOP_POSITION < 1) {
|
||||||
|
if (STICKY_TOP_POSITION) {
|
||||||
|
window.scroll({ top: window.scrollY - STICKY_TOP_POSITION })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
tooltipProps={{ interactionKind: 'click', hasBackdrop: true }}>
|
||||||
|
<StringSubstitute str={getString('pr.showLink')} vars={{ count: diffs?.length || 0 }} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
addedLines: formatNumber(diffStats.addedLines),
|
||||||
|
deletedLines: formatNumber(diffStats.deletedLines),
|
||||||
|
config: (
|
||||||
|
<Text
|
||||||
|
icon="cog"
|
||||||
|
rightIcon="caret-down"
|
||||||
|
tooltip={
|
||||||
|
<Container padding="large">
|
||||||
|
<Layout.Horizontal spacing="xsmall" flex={{ alignItems: 'center' }}>
|
||||||
|
<Text width={100} font={{ variation: FontVariation.SMALL_BOLD }}>
|
||||||
|
{getString('pr.diffView')}
|
||||||
|
</Text>
|
||||||
|
<ButtonGroup>
|
||||||
|
<BButton
|
||||||
|
className={cx(
|
||||||
|
Classes.POPOVER_DISMISS,
|
||||||
|
viewStyle === DiffViewStyle.SPLIT ? Classes.ACTIVE : ''
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setViewStyle(DiffViewStyle.SPLIT)
|
||||||
|
window.scroll({ top: 0 })
|
||||||
|
}}>
|
||||||
|
{getString('pr.split')}
|
||||||
|
</BButton>
|
||||||
|
<BButton
|
||||||
|
className={cx(
|
||||||
|
Classes.POPOVER_DISMISS,
|
||||||
|
viewStyle === DiffViewStyle.UNIFIED ? Classes.ACTIVE : ''
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setViewStyle(DiffViewStyle.UNIFIED)
|
||||||
|
window.scroll({ top: 0 })
|
||||||
|
}}>
|
||||||
|
{getString('pr.unified')}
|
||||||
|
</BButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
iconProps={{ size: 14, padding: { right: 3 } }}
|
||||||
|
rightIconProps={{ size: 13, padding: { left: 0 } }}
|
||||||
|
padding={{ left: 'small' }}
|
||||||
|
{...ButtonRoleProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
{stickyInAction && (
|
||||||
|
<Layout.Horizontal padding={{ left: 'small' }}>
|
||||||
|
<PipeSeparator height={10} />
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.ICON}
|
||||||
|
icon="arrow-up"
|
||||||
|
iconProps={{ size: 14 }}
|
||||||
|
onClick={() => window.scroll({ top: 0 })}
|
||||||
|
tooltip={getString('scrollToTop')}
|
||||||
|
tooltipProps={{ isDark: true }}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
<FlexExpander />
|
||||||
|
<Button text={getString('pr.reviewChanges')} variation={ButtonVariation.PRIMARY} intent="success" />
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
<Layout.Vertical spacing="medium" className={css.layout}>
|
||||||
|
{diffs?.map((diff, index) => (
|
||||||
|
// Note: `key={viewStyle + index}` will reset DiffView when viewStyle
|
||||||
|
// is changed. Making it easier to control states inside DiffView itself, as it does not have to deal with viewStyle
|
||||||
|
<DiffViewer
|
||||||
|
key={viewStyle + index}
|
||||||
|
index={index}
|
||||||
|
diff={diff}
|
||||||
|
viewStyle={viewStyle}
|
||||||
|
stickyTopPosition={STICKY_TOP_POSITION}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Layout.Vertical>
|
||||||
|
</PullRequestTabContentWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
31177
web/src/pages/PullRequest/PullRequestDiff/example.diff
Normal file
31177
web/src/pages/PullRequest/PullRequestDiff/example.diff
Normal file
File diff suppressed because one or more lines are too long
358
web/src/pages/PullRequest/PullRequestDiff/example2.diff
Normal file
358
web/src/pages/PullRequest/PullRequestDiff/example2.diff
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
diff --git a/README.md b/README.md
|
||||||
|
index 132c8a28..46909f25 100644
|
||||||
|
--- a/README.md
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -98,6 +98,9 @@ The HTML output accepts a Javascript object with configuration. Possible options
|
||||||
|
- `synchronisedScroll`: scroll both panes in side-by-side mode: `true` or `false`, default is `false`
|
||||||
|
- `matchWordsThreshold`: similarity threshold for word matching, default is 0.25
|
||||||
|
- `matchingMaxComparisons`: perform at most this much comparisons for line matching a block of changes, default is `2500`
|
||||||
|
+ - `templates`: object with previously compiled templates to replace parts of the html
|
||||||
|
+ - `rawTemplates`: object with raw not compiled templates to replace parts of the html
|
||||||
|
+ > For more information regarding the possible templates look into [src/templates](https://github.com/rtfpessoa/diff2html/tree/master/src/templates)
|
||||||
|
|
||||||
|
## Diff2HtmlUI Helper
|
||||||
|
|
||||||
|
diff --git a/scripts/hulk.js b/scripts/hulk.js
|
||||||
|
index 5a793c18..a4b1a4d5 100755
|
||||||
|
--- a/scripts/hulk.js
|
||||||
|
+++ b/scripts/hulk.js
|
||||||
|
@@ -173,11 +173,11 @@ function namespace(name) {
|
||||||
|
// write a template foreach file that matches template extension
|
||||||
|
templates = extractFiles(options.argv.remain)
|
||||||
|
.map(function(file) {
|
||||||
|
- var openedFile = fs.readFileSync(file, 'utf-8');
|
||||||
|
+ var openedFile = fs.readFileSync(file, 'utf-8').trim();
|
||||||
|
var name;
|
||||||
|
if (!openedFile) return;
|
||||||
|
name = namespace(path.basename(file).replace(/\..*$/, ''));
|
||||||
|
- openedFile = removeByteOrderMark(openedFile.trim());
|
||||||
|
+ openedFile = removeByteOrderMark(openedFile);
|
||||||
|
openedFile = wrap(file, name, openedFile);
|
||||||
|
if (!options.outputdir) return openedFile;
|
||||||
|
fs.writeFileSync(path.join(options.outputdir, name + '.js')
|
||||||
|
diff --git a/src/diff2html.js b/src/diff2html.js
|
||||||
|
index 21b0119e..64e138f5 100644
|
||||||
|
--- a/src/diff2html.js
|
||||||
|
+++ b/src/diff2html.js
|
||||||
|
@@ -7,7 +7,6 @@
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var diffParser = require('./diff-parser.js').DiffParser;
|
||||||
|
- var fileLister = require('./file-list-printer.js').FileListPrinter;
|
||||||
|
var htmlPrinter = require('./html-printer.js').HtmlPrinter;
|
||||||
|
|
||||||
|
function Diff2Html() {
|
||||||
|
@@ -43,7 +42,7 @@
|
||||||
|
|
||||||
|
var fileList = '';
|
||||||
|
if (configOrEmpty.showFiles === true) {
|
||||||
|
- fileList = fileLister.generateFileList(diffJson, configOrEmpty);
|
||||||
|
+ fileList = htmlPrinter.generateFileListSummary(diffJson, configOrEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var diffOutput = '';
|
||||||
|
diff --git a/src/file-list-printer.js b/src/file-list-printer.js
|
||||||
|
index e408d9b2..1e0a2c61 100644
|
||||||
|
--- a/src/file-list-printer.js
|
||||||
|
+++ b/src/file-list-printer.js
|
||||||
|
@@ -8,11 +8,16 @@
|
||||||
|
(function() {
|
||||||
|
var printerUtils = require('./printer-utils.js').PrinterUtils;
|
||||||
|
|
||||||
|
- var hoganUtils = require('./hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+ var hoganUtils;
|
||||||
|
+
|
||||||
|
var baseTemplatesPath = 'file-summary';
|
||||||
|
var iconsBaseTemplatesPath = 'icon';
|
||||||
|
|
||||||
|
- function FileListPrinter() {
|
||||||
|
+ function FileListPrinter(config) {
|
||||||
|
+ this.config = config;
|
||||||
|
+
|
||||||
|
+ var HoganJsUtils = require('./hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+ hoganUtils = new HoganJsUtils(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileListPrinter.prototype.generateFileList = function(diffFiles) {
|
||||||
|
@@ -38,5 +43,5 @@
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
- module.exports.FileListPrinter = new FileListPrinter();
|
||||||
|
+ module.exports.FileListPrinter = FileListPrinter;
|
||||||
|
})();
|
||||||
|
diff --git a/src/hoganjs-utils.js b/src/hoganjs-utils.js
|
||||||
|
index 9949e5fa..b2e9c275 100644
|
||||||
|
--- a/src/hoganjs-utils.js
|
||||||
|
+++ b/src/hoganjs-utils.js
|
||||||
|
@@ -8,18 +8,26 @@
|
||||||
|
(function() {
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
-
|
||||||
|
var hogan = require('hogan.js');
|
||||||
|
|
||||||
|
var hoganTemplates = require('./templates/diff2html-templates.js');
|
||||||
|
|
||||||
|
- var templatesPath = path.resolve(__dirname, 'templates');
|
||||||
|
+ var extraTemplates;
|
||||||
|
+
|
||||||
|
+ function HoganJsUtils(configuration) {
|
||||||
|
+ this.config = configuration || {};
|
||||||
|
+ extraTemplates = this.config.templates || {};
|
||||||
|
|
||||||
|
- function HoganJsUtils() {
|
||||||
|
+ var rawTemplates = this.config.rawTemplates || {};
|
||||||
|
+ for (var templateName in rawTemplates) {
|
||||||
|
+ if (rawTemplates.hasOwnProperty(templateName)) {
|
||||||
|
+ if (!extraTemplates[templateName]) extraTemplates[templateName] = this.compile(rawTemplates[templateName]);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
- HoganJsUtils.prototype.render = function(namespace, view, params, configuration) {
|
||||||
|
- var template = this.template(namespace, view, configuration);
|
||||||
|
+ HoganJsUtils.prototype.render = function(namespace, view, params) {
|
||||||
|
+ var template = this.template(namespace, view);
|
||||||
|
if (template) {
|
||||||
|
return template.render(params);
|
||||||
|
}
|
||||||
|
@@ -27,17 +35,16 @@
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
- HoganJsUtils.prototype.template = function(namespace, view, configuration) {
|
||||||
|
- var config = configuration || {};
|
||||||
|
+ HoganJsUtils.prototype.template = function(namespace, view) {
|
||||||
|
var templateKey = this._templateKey(namespace, view);
|
||||||
|
|
||||||
|
- return this._getTemplate(templateKey, config);
|
||||||
|
+ return this._getTemplate(templateKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
- HoganJsUtils.prototype._getTemplate = function(templateKey, config) {
|
||||||
|
+ HoganJsUtils.prototype._getTemplate = function(templateKey) {
|
||||||
|
var template;
|
||||||
|
|
||||||
|
- if (!config.noCache) {
|
||||||
|
+ if (!this.config.noCache) {
|
||||||
|
template = this._readFromCache(templateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -53,6 +60,7 @@
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.readFileSync) {
|
||||||
|
+ var templatesPath = path.resolve(__dirname, 'templates');
|
||||||
|
var templatePath = path.join(templatesPath, templateKey);
|
||||||
|
var templateContent = fs.readFileSync(templatePath + '.mustache', 'utf8');
|
||||||
|
template = hogan.compile(templateContent);
|
||||||
|
@@ -66,12 +74,16 @@
|
||||||
|
};
|
||||||
|
|
||||||
|
HoganJsUtils.prototype._readFromCache = function(templateKey) {
|
||||||
|
- return hoganTemplates[templateKey];
|
||||||
|
+ return extraTemplates[templateKey] || hoganTemplates[templateKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
HoganJsUtils.prototype._templateKey = function(namespace, view) {
|
||||||
|
return namespace + '-' + view;
|
||||||
|
};
|
||||||
|
|
||||||
|
- module.exports.HoganJsUtils = new HoganJsUtils();
|
||||||
|
+ HoganJsUtils.prototype.compile = function(templateStr) {
|
||||||
|
+ return hogan.compile(templateStr);
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ module.exports.HoganJsUtils = HoganJsUtils;
|
||||||
|
})();
|
||||||
|
diff --git a/src/html-printer.js b/src/html-printer.js
|
||||||
|
index 585d5b66..13f83047 100644
|
||||||
|
--- a/src/html-printer.js
|
||||||
|
+++ b/src/html-printer.js
|
||||||
|
@@ -8,6 +8,7 @@
|
||||||
|
(function() {
|
||||||
|
var LineByLinePrinter = require('./line-by-line-printer.js').LineByLinePrinter;
|
||||||
|
var SideBySidePrinter = require('./side-by-side-printer.js').SideBySidePrinter;
|
||||||
|
+ var FileListPrinter = require('./file-list-printer.js').FileListPrinter;
|
||||||
|
|
||||||
|
function HtmlPrinter() {
|
||||||
|
}
|
||||||
|
@@ -22,5 +23,10 @@
|
||||||
|
return sideBySidePrinter.generateSideBySideJsonHtml(diffFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
+ HtmlPrinter.prototype.generateFileListSummary = function(diffJson, config) {
|
||||||
|
+ var fileListPrinter = new FileListPrinter(config);
|
||||||
|
+ return fileListPrinter.generateFileList(diffJson);
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
module.exports.HtmlPrinter = new HtmlPrinter();
|
||||||
|
})();
|
||||||
|
diff --git a/src/line-by-line-printer.js b/src/line-by-line-printer.js
|
||||||
|
index b07eb53c..d230bedd 100644
|
||||||
|
--- a/src/line-by-line-printer.js
|
||||||
|
+++ b/src/line-by-line-printer.js
|
||||||
|
@@ -11,7 +11,8 @@
|
||||||
|
var utils = require('./utils.js').Utils;
|
||||||
|
var Rematch = require('./rematch.js').Rematch;
|
||||||
|
|
||||||
|
- var hoganUtils = require('./hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+ var hoganUtils;
|
||||||
|
+
|
||||||
|
var genericTemplatesPath = 'generic';
|
||||||
|
var baseTemplatesPath = 'line-by-line';
|
||||||
|
var iconsBaseTemplatesPath = 'icon';
|
||||||
|
@@ -19,6 +20,9 @@
|
||||||
|
|
||||||
|
function LineByLinePrinter(config) {
|
||||||
|
this.config = config;
|
||||||
|
+
|
||||||
|
+ var HoganJsUtils = require('./hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+ hoganUtils = new HoganJsUtils(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
LineByLinePrinter.prototype.makeFileDiffHtml = function(file, diffs) {
|
||||||
|
diff --git a/src/side-by-side-printer.js b/src/side-by-side-printer.js
|
||||||
|
index bbf1dc8d..5e3033b3 100644
|
||||||
|
--- a/src/side-by-side-printer.js
|
||||||
|
+++ b/src/side-by-side-printer.js
|
||||||
|
@@ -11,7 +11,8 @@
|
||||||
|
var utils = require('./utils.js').Utils;
|
||||||
|
var Rematch = require('./rematch.js').Rematch;
|
||||||
|
|
||||||
|
- var hoganUtils = require('./hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+ var hoganUtils;
|
||||||
|
+
|
||||||
|
var genericTemplatesPath = 'generic';
|
||||||
|
var baseTemplatesPath = 'side-by-side';
|
||||||
|
var iconsBaseTemplatesPath = 'icon';
|
||||||
|
@@ -26,6 +27,9 @@
|
||||||
|
|
||||||
|
function SideBySidePrinter(config) {
|
||||||
|
this.config = config;
|
||||||
|
+
|
||||||
|
+ var HoganJsUtils = require('./hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+ hoganUtils = new HoganJsUtils(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
SideBySidePrinter.prototype.makeDiffHtml = function(file, diffs) {
|
||||||
|
diff --git a/test/file-list-printer-tests.js b/test/file-list-printer-tests.js
|
||||||
|
index a502a46f..60ea3208 100644
|
||||||
|
--- a/test/file-list-printer-tests.js
|
||||||
|
+++ b/test/file-list-printer-tests.js
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
-var fileListPrinter = require('../src/file-list-printer.js').FileListPrinter;
|
||||||
|
+var fileListPrinter = new (require('../src/file-list-printer.js').FileListPrinter)();
|
||||||
|
|
||||||
|
describe('FileListPrinter', function() {
|
||||||
|
describe('generateFileList', function() {
|
||||||
|
diff --git a/test/hogan-cache-tests.js b/test/hogan-cache-tests.js
|
||||||
|
index 190bf6f8..a34839c0 100644
|
||||||
|
--- a/test/hogan-cache-tests.js
|
||||||
|
+++ b/test/hogan-cache-tests.js
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
-var HoganJsUtils = require('../src/hoganjs-utils.js').HoganJsUtils;
|
||||||
|
+var HoganJsUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)();
|
||||||
|
var diffParser = require('../src/diff-parser.js').DiffParser;
|
||||||
|
|
||||||
|
describe('HoganJsUtils', function() {
|
||||||
|
@@ -21,16 +21,50 @@ describe('HoganJsUtils', function() {
|
||||||
|
});
|
||||||
|
assert.equal(emptyDiffHtml, result);
|
||||||
|
});
|
||||||
|
+
|
||||||
|
it('should render view without cache', function() {
|
||||||
|
var result = HoganJsUtils.render('generic', 'empty-diff', {
|
||||||
|
contentClass: 'd2h-code-line',
|
||||||
|
diffParser: diffParser
|
||||||
|
}, {noCache: true});
|
||||||
|
- assert.equal(emptyDiffHtml + '\n', result);
|
||||||
|
+ assert.equal(emptyDiffHtml, result);
|
||||||
|
});
|
||||||
|
+
|
||||||
|
it('should return null if template is missing', function() {
|
||||||
|
- var result = HoganJsUtils.render('generic', 'missing-template', {}, {noCache: true});
|
||||||
|
+ var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)({noCache: true});
|
||||||
|
+ var result = hoganUtils.render('generic', 'missing-template', {});
|
||||||
|
assert.equal(null, result);
|
||||||
|
});
|
||||||
|
+
|
||||||
|
+ it('should allow templates to be overridden with compiled templates', function() {
|
||||||
|
+ var emptyDiffTemplate = HoganJsUtils.compile('<p>{{myName}}</p>');
|
||||||
|
+
|
||||||
|
+ var config = {templates: {'generic-empty-diff': emptyDiffTemplate}};
|
||||||
|
+ var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(config);
|
||||||
|
+ var result = hoganUtils.render('generic', 'empty-diff', {myName: 'Rodrigo Fernandes'});
|
||||||
|
+ assert.equal('<p>Rodrigo Fernandes</p>', result);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ it('should allow templates to be overridden with uncompiled templates', function() {
|
||||||
|
+ var emptyDiffTemplate = '<p>{{myName}}</p>';
|
||||||
|
+
|
||||||
|
+ var config = {rawTemplates: {'generic-empty-diff': emptyDiffTemplate}};
|
||||||
|
+ var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(config);
|
||||||
|
+ var result = hoganUtils.render('generic', 'empty-diff', {myName: 'Rodrigo Fernandes'});
|
||||||
|
+ assert.equal('<p>Rodrigo Fernandes</p>', result);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ it('should allow templates to be overridden giving priority to compiled templates', function() {
|
||||||
|
+ var emptyDiffTemplate = HoganJsUtils.compile('<p>{{myName}}</p>');
|
||||||
|
+ var emptyDiffTemplateUncompiled = '<p>Not used!</p>';
|
||||||
|
+
|
||||||
|
+ var config = {
|
||||||
|
+ templates: {'generic-empty-diff': emptyDiffTemplate},
|
||||||
|
+ rawTemplates: {'generic-empty-diff': emptyDiffTemplateUncompiled}
|
||||||
|
+ };
|
||||||
|
+ var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(config);
|
||||||
|
+ var result = hoganUtils.render('generic', 'empty-diff', {myName: 'Rodrigo Fernandes'});
|
||||||
|
+ assert.equal('<p>Rodrigo Fernandes</p>', result);
|
||||||
|
+ });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
diff --git a/test/line-by-line-tests.js b/test/line-by-line-tests.js
|
||||||
|
index 1cd92073..8869b3df 100644
|
||||||
|
--- a/test/line-by-line-tests.js
|
||||||
|
+++ b/test/line-by-line-tests.js
|
||||||
|
@@ -14,7 +14,7 @@ describe('LineByLinePrinter', function() {
|
||||||
|
' File without changes\n' +
|
||||||
|
' </div>\n' +
|
||||||
|
' </td>\n' +
|
||||||
|
- '</tr>\n';
|
||||||
|
+ '</tr>';
|
||||||
|
|
||||||
|
assert.equal(expected, fileHtml);
|
||||||
|
});
|
||||||
|
@@ -422,7 +422,6 @@ describe('LineByLinePrinter', function() {
|
||||||
|
' </div>\n' +
|
||||||
|
' </td>\n' +
|
||||||
|
'</tr>\n' +
|
||||||
|
- '\n' +
|
||||||
|
' </tbody>\n' +
|
||||||
|
' </table>\n' +
|
||||||
|
' </div>\n' +
|
||||||
|
diff --git a/test/side-by-side-printer-tests.js b/test/side-by-side-printer-tests.js
|
||||||
|
index 76625f8e..771daaa5 100644
|
||||||
|
--- a/test/side-by-side-printer-tests.js
|
||||||
|
+++ b/test/side-by-side-printer-tests.js
|
||||||
|
@@ -14,7 +14,7 @@ describe('SideBySidePrinter', function() {
|
||||||
|
' File without changes\n' +
|
||||||
|
' </div>\n' +
|
||||||
|
' </td>\n' +
|
||||||
|
- '</tr>\n';
|
||||||
|
+ '</tr>';
|
||||||
|
|
||||||
|
assert.equal(expectedRight, fileHtml.right);
|
||||||
|
assert.equal(expectedLeft, fileHtml.left);
|
||||||
|
@@ -324,7 +324,6 @@ describe('SideBySidePrinter', function() {
|
||||||
|
' </div>\n' +
|
||||||
|
' </td>\n' +
|
||||||
|
'</tr>\n' +
|
||||||
|
- '\n' +
|
||||||
|
' </tbody>\n' +
|
||||||
|
' </table>\n' +
|
||||||
|
' </div>\n' +
|
43
web/src/pages/PullRequest/PullRequestMetaLine.module.scss
Normal file
43
web/src/pages/PullRequest/PullRequestMetaLine.module.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.main {
|
||||||
|
.state {
|
||||||
|
--color: var(--green-700) !important;
|
||||||
|
--bg: var(--green-50) !important;
|
||||||
|
|
||||||
|
color: var(--color);
|
||||||
|
background-color: var(--bg);
|
||||||
|
font-size: 10px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.merged {
|
||||||
|
--color: var(--purple-700) !important;
|
||||||
|
--bg: var(--purple-50) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
--color: var(--grey-700) !important;
|
||||||
|
--bg: var(--grey-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rejected {
|
||||||
|
--color: var(--red-700) !important;
|
||||||
|
--bg: var(--red-50) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaline {
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 20px !important;
|
||||||
|
color: var(--grey-500) !important;
|
||||||
|
|
||||||
|
&.time {
|
||||||
|
color: var(--grey-400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--grey-700) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,9 @@
|
|||||||
declare const styles: {
|
declare const styles: {
|
||||||
readonly main: string
|
readonly main: string
|
||||||
readonly state: string
|
readonly state: string
|
||||||
|
readonly merged: string
|
||||||
|
readonly closed: string
|
||||||
|
readonly rejected: string
|
||||||
readonly metaline: string
|
readonly metaline: string
|
||||||
readonly time: string
|
readonly time: string
|
||||||
}
|
}
|
87
web/src/pages/PullRequest/PullRequestMetaLine.tsx
Normal file
87
web/src/pages/PullRequest/PullRequestMetaLine.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Container, Text, Layout, Color, StringSubstitute, IconName } from '@harness/uicore'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import ReactTimeago from 'react-timeago'
|
||||||
|
import { CodeIcon, GitInfoProps, PullRequestState } from 'utils/GitUtils'
|
||||||
|
import { useAppContext } from 'AppContext'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
|
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
|
||||||
|
import type { PullRequestResponse } from 'utils/types'
|
||||||
|
import css from './PullRequestMetaLine.module.scss'
|
||||||
|
|
||||||
|
export const PullRequestMetaLine: React.FC<PullRequestResponse & Pick<GitInfoProps, 'repoMetadata'>> = ({
|
||||||
|
repoMetadata,
|
||||||
|
targetBranch,
|
||||||
|
sourceBranch,
|
||||||
|
createdBy = '',
|
||||||
|
updated,
|
||||||
|
merged,
|
||||||
|
state
|
||||||
|
}) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { routes } = useAppContext()
|
||||||
|
const vars = {
|
||||||
|
user: <strong>{createdBy}</strong>,
|
||||||
|
number: <strong>5</strong>, // TODO: No data from backend now
|
||||||
|
target: (
|
||||||
|
<GitRefLink
|
||||||
|
text={targetBranch}
|
||||||
|
url={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef: targetBranch })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
source: (
|
||||||
|
<GitRefLink
|
||||||
|
text={sourceBranch}
|
||||||
|
url={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef: sourceBranch })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container padding={{ left: 'xlarge' }} className={css.main}>
|
||||||
|
<Layout.Horizontal spacing="small">
|
||||||
|
<PullRequestStateLabel state={merged ? PullRequestState.MERGED : (state as PullRequestState)} />
|
||||||
|
<Text className={css.metaline}>
|
||||||
|
<StringSubstitute str={getString('pr.metaLine')} vars={vars} />
|
||||||
|
</Text>
|
||||||
|
<PipeSeparator height={9} />
|
||||||
|
<Text inline className={cx(css.metaline, css.time)}>
|
||||||
|
<ReactTimeago date={updated} />
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PullRequestStateLabel: React.FC<{ state: PullRequestState }> = ({ state }) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
|
||||||
|
let color = Color.GREEN_700
|
||||||
|
let icon: IconName = CodeIcon.PullRequest
|
||||||
|
let clazz: typeof css | string = ''
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case PullRequestState.MERGED:
|
||||||
|
color = Color.PURPLE_700
|
||||||
|
icon = CodeIcon.PullRequest
|
||||||
|
clazz = css.merged
|
||||||
|
break
|
||||||
|
case PullRequestState.CLOSED:
|
||||||
|
color = Color.GREY_600
|
||||||
|
icon = CodeIcon.PullRequest
|
||||||
|
clazz = css.closed
|
||||||
|
break
|
||||||
|
case PullRequestState.REJECTED:
|
||||||
|
color = Color.RED_600
|
||||||
|
icon = CodeIcon.PullRequestRejected
|
||||||
|
clazz = css.rejected
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text className={cx(css.state, clazz)} icon={icon} iconProps={{ color, size: 9 }}>
|
||||||
|
<StringSubstitute str={getString('pr.state')} vars={{ state }} />
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
.main {
|
|
||||||
.state {
|
|
||||||
color: var(--green-700) !important;
|
|
||||||
background-color: var(--green-50) !important;
|
|
||||||
font-size: 9px !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
padding: 2px 6px !important;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metaline {
|
|
||||||
font-size: 12px !important;
|
|
||||||
font-weight: 500 !important;
|
|
||||||
line-height: 20px !important;
|
|
||||||
color: var(--grey-500) !important;
|
|
||||||
|
|
||||||
&.time {
|
|
||||||
color: var(--grey-400) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--grey-700) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Container, Text, Layout, Color } from '@harness/uicore'
|
|
||||||
import cx from 'classnames'
|
|
||||||
import ReactTimeago from 'react-timeago'
|
|
||||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
|
||||||
// import { useAppContext } from 'AppContext'
|
|
||||||
import { useStrings } from 'framework/strings'
|
|
||||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
|
||||||
import { StringSubstitute } from 'components/StringSubstitute/StringSubstitute'
|
|
||||||
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
|
|
||||||
import type { PullRequestResponse } from 'utils/types'
|
|
||||||
import css from './PullRequestMetadataInfo.module.scss'
|
|
||||||
|
|
||||||
export const PullRequestMetadataInfo: React.FC<PullRequestResponse & Pick<GitInfoProps, 'repoMetadata'>> = ({
|
|
||||||
createdBy = '',
|
|
||||||
targetBranch,
|
|
||||||
sourceBranch,
|
|
||||||
updated
|
|
||||||
// repoMetadata
|
|
||||||
}) => {
|
|
||||||
const { getString } = useStrings()
|
|
||||||
// const { routes } = useAppContext()
|
|
||||||
const vars = {
|
|
||||||
user: <strong>{createdBy}</strong>,
|
|
||||||
number: 1, // TODO: No data from backend now
|
|
||||||
target: <GitRefLink text={targetBranch} url="TODO" />,
|
|
||||||
source: <GitRefLink text={sourceBranch} url="TODO" />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container padding={{ left: 'xlarge' }} className={css.main}>
|
|
||||||
<Layout.Horizontal spacing="small">
|
|
||||||
<Text className={css.state} icon={CodeIcon.PullRequest} iconProps={{ color: Color.GREEN_700, size: 9 }}>
|
|
||||||
Open
|
|
||||||
</Text>
|
|
||||||
<Text className={css.metaline}>
|
|
||||||
<StringSubstitute str={getString('pr.metaLine')} vars={vars} />
|
|
||||||
</Text>
|
|
||||||
<PipeSeparator height={9} />
|
|
||||||
<Text inline className={cx(css.metaline, css.time)}>
|
|
||||||
<ReactTimeago date={updated} />
|
|
||||||
</Text>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
30
web/src/pages/PullRequest/PullRequestTabContentWrapper.tsx
Normal file
30
web/src/pages/PullRequest/PullRequestTabContentWrapper.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Container, PageError } from '@harness/uicore'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import { ContainerSpinner } from 'components/ContainerSpinner/ContainerSpinner'
|
||||||
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
|
import css from './PullRequest.module.scss'
|
||||||
|
|
||||||
|
interface PullRequestTabContentWrapperProps {
|
||||||
|
className?: string
|
||||||
|
loading?: boolean
|
||||||
|
error?: Unknown
|
||||||
|
onRetry: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PullRequestTabContentWrapper: React.FC<PullRequestTabContentWrapperProps> = ({
|
||||||
|
className,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
children
|
||||||
|
}) => (
|
||||||
|
<Container
|
||||||
|
className={cx(css.tabContentContainer, className)}
|
||||||
|
padding="xlarge"
|
||||||
|
{...(!!loading || !!error ? { flex: true } : {})}>
|
||||||
|
{loading && <ContainerSpinner />}
|
||||||
|
{error && <PageError message={getErrorMessage(error)} onClick={onRetry} />}
|
||||||
|
{!loading && !error && children}
|
||||||
|
</Container>
|
||||||
|
)
|
@ -1,4 +1,16 @@
|
|||||||
.main {
|
.main {
|
||||||
min-height: var(--page-min-height, 100%);
|
min-height: var(--page-min-height, 100%);
|
||||||
background-color: var(--primary-bg) !important;
|
background-color: var(--primary-bg) !important;
|
||||||
|
|
||||||
|
.table {
|
||||||
|
.row {
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,8 @@
|
|||||||
// this is an auto-generated file
|
// this is an auto-generated file
|
||||||
declare const styles: {
|
declare const styles: {
|
||||||
readonly main: string
|
readonly main: string
|
||||||
|
readonly table: string
|
||||||
|
readonly row: string
|
||||||
|
readonly title: string
|
||||||
}
|
}
|
||||||
export default styles
|
export default styles
|
||||||
|
@ -1,20 +1,88 @@
|
|||||||
import React from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { Button, Container, ButtonVariation, PageBody } from '@harness/uicore'
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
ButtonVariation,
|
||||||
|
PageBody,
|
||||||
|
Text,
|
||||||
|
Color,
|
||||||
|
TableV2,
|
||||||
|
Layout,
|
||||||
|
StringSubstitute
|
||||||
|
} from '@harness/uicore'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { useGet } from 'restful-react'
|
||||||
|
import type { CellProps, Column } from 'react-table'
|
||||||
|
import ReactTimeago from 'react-timeago'
|
||||||
import { CodeIcon, makeDiffRefs } from 'utils/GitUtils'
|
import { CodeIcon, makeDiffRefs } from 'utils/GitUtils'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||||
import { getErrorMessage } from 'utils/Utils'
|
import { getErrorMessage, LIST_FETCHING_PER_PAGE } from 'utils/Utils'
|
||||||
import emptyStateImage from 'images/empty-state.svg'
|
import emptyStateImage from 'images/empty-state.svg'
|
||||||
|
import type { PullRequestResponse } from 'utils/types'
|
||||||
|
import { usePageIndex } from 'hooks/usePageIndex'
|
||||||
|
import { PullRequestsContentHeader } from './PullRequestsContentHeader/PullRequestsContentHeader'
|
||||||
|
import prOpenImg from './pull-request-open.svg'
|
||||||
import css from './PullRequests.module.scss'
|
import css from './PullRequests.module.scss'
|
||||||
|
|
||||||
export default function PullRequests() {
|
export default function PullRequests() {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { routes } = useAppContext()
|
const { routes } = useAppContext()
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [pageIndex, setPageIndex] = usePageIndex()
|
||||||
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error: prError,
|
||||||
|
loading: prLoading
|
||||||
|
} = useGet<PullRequestResponse[]>({
|
||||||
|
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq`,
|
||||||
|
queryParams: {
|
||||||
|
per_page: LIST_FETCHING_PER_PAGE,
|
||||||
|
page: pageIndex + 1,
|
||||||
|
sort: 'date',
|
||||||
|
direction: 'desc',
|
||||||
|
include_commit: true,
|
||||||
|
query: searchTerm
|
||||||
|
},
|
||||||
|
lazy: !repoMetadata
|
||||||
|
})
|
||||||
|
const columns: Column<PullRequestResponse>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'title',
|
||||||
|
width: '100%',
|
||||||
|
Cell: ({ row }: CellProps<PullRequestResponse>) => {
|
||||||
|
return (
|
||||||
|
<Layout.Horizontal spacing="medium" padding={{ left: 'medium' }}>
|
||||||
|
<img src={prOpenImg} />
|
||||||
|
<Container padding={{ left: 'small' }}>
|
||||||
|
<Layout.Vertical spacing="small">
|
||||||
|
<Text icon="success-tick" color={Color.GREY_800} className={css.title}>
|
||||||
|
{row.original.title}
|
||||||
|
</Text>
|
||||||
|
<Text color={Color.GREY_500}>
|
||||||
|
<StringSubstitute
|
||||||
|
str={getString('pr.openBy')}
|
||||||
|
vars={{
|
||||||
|
number: <Text inline>{row.original.number}</Text>,
|
||||||
|
time: <ReactTimeago date={row.original.updated} />,
|
||||||
|
user: row.original.createdBy
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[getString]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
@ -24,11 +92,13 @@ export default function PullRequests() {
|
|||||||
dataTooltipId="repositoryPullRequests"
|
dataTooltipId="repositoryPullRequests"
|
||||||
/>
|
/>
|
||||||
<PageBody
|
<PageBody
|
||||||
loading={loading}
|
loading={loading || prLoading}
|
||||||
error={getErrorMessage(error)}
|
error={getErrorMessage(error || prError)}
|
||||||
retryOnError={() => refetch()}
|
retryOnError={() => refetch()}
|
||||||
noData={{
|
noData={{
|
||||||
when: () => repoMetadata !== null,
|
// TODO: Use NoDataCard, this won't render toolbar
|
||||||
|
// when search returns empty response
|
||||||
|
when: () => data?.length === 0,
|
||||||
message: getString('pullRequestEmpty'),
|
message: getString('pullRequestEmpty'),
|
||||||
image: emptyStateImage,
|
image: emptyStateImage,
|
||||||
button: (
|
button: (
|
||||||
@ -47,7 +117,39 @@ export default function PullRequests() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}>
|
}}>
|
||||||
{/* TODO: Render pull request table here - https://www.figma.com/file/PgBvi804VdQNyLS8fD9K0p/SCM?node-id=1220%3A119902&t=D3DaDpST8oO95WSu-0 */}
|
{repoMetadata && (
|
||||||
|
<Layout.Vertical>
|
||||||
|
<PullRequestsContentHeader
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
|
onPullRequestFilterChanged={_filter => {
|
||||||
|
setPageIndex(0)
|
||||||
|
}}
|
||||||
|
onSearchTermChanged={value => {
|
||||||
|
setSearchTerm(value)
|
||||||
|
setPageIndex(0)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!!data?.length && (
|
||||||
|
<Container padding="xlarge">
|
||||||
|
<TableV2<PullRequestResponse>
|
||||||
|
className={css.table}
|
||||||
|
hideHeaders
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
getRowClassName={() => css.row}
|
||||||
|
onRowClick={row => {
|
||||||
|
history.push(
|
||||||
|
routes.toCODEPullRequest({
|
||||||
|
repoPath: repoMetadata.path as string,
|
||||||
|
pullRequestId: String(row.number)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Layout.Vertical>
|
||||||
|
)}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
.main {
|
||||||
|
div[class*='TextInput'] {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branchDropdown {
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// this is an auto-generated file
|
||||||
|
declare const styles: {
|
||||||
|
readonly main: string
|
||||||
|
readonly branchDropdown: string
|
||||||
|
}
|
||||||
|
export default styles
|
@ -0,0 +1,78 @@
|
|||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { Container, Layout, FlexExpander, DropDown, ButtonVariation, TextInput, Button } from '@harness/uicore'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { CodeIcon, GitInfoProps, makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
|
||||||
|
import { useAppContext } from 'AppContext'
|
||||||
|
import css from './PullRequestsContentHeader.module.scss'
|
||||||
|
|
||||||
|
interface PullRequestsContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
||||||
|
activePullRequestFilterOption?: string
|
||||||
|
onPullRequestFilterChanged: (filter: string) => void
|
||||||
|
onSearchTermChanged: (searchTerm: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PullRequestsContentHeader({
|
||||||
|
onPullRequestFilterChanged,
|
||||||
|
onSearchTermChanged,
|
||||||
|
activePullRequestFilterOption = PullRequestFilterOption.ALL,
|
||||||
|
repoMetadata
|
||||||
|
}: PullRequestsContentHeaderProps) {
|
||||||
|
const history = useHistory()
|
||||||
|
const { routes } = useAppContext()
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const [filterOption, setFilterOption] = useState(activePullRequestFilterOption)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const items = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: getString('open'), value: PullRequestFilterOption.OPEN },
|
||||||
|
{ label: getString('merged'), value: PullRequestFilterOption.MERGED },
|
||||||
|
{ label: getString('closed'), value: PullRequestFilterOption.CLOSED },
|
||||||
|
{ label: getString('rejected'), value: PullRequestFilterOption.REJECTED },
|
||||||
|
{ label: getString('yours'), value: PullRequestFilterOption.YOURS },
|
||||||
|
{ label: getString('all'), value: PullRequestFilterOption.ALL }
|
||||||
|
],
|
||||||
|
[getString]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className={css.main} padding="xlarge">
|
||||||
|
<Layout.Horizontal spacing="medium">
|
||||||
|
<DropDown
|
||||||
|
value={filterOption}
|
||||||
|
items={items}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
setFilterOption(value as string)
|
||||||
|
onPullRequestFilterChanged(value as string)
|
||||||
|
}}
|
||||||
|
popoverClassName={css.branchDropdown}
|
||||||
|
/>
|
||||||
|
<FlexExpander />
|
||||||
|
<TextInput
|
||||||
|
placeholder={getString('search')}
|
||||||
|
autoFocus
|
||||||
|
onFocus={event => event.target.select()}
|
||||||
|
value={searchTerm}
|
||||||
|
onInput={event => {
|
||||||
|
const value = event.currentTarget.value
|
||||||
|
setSearchTerm(value)
|
||||||
|
onSearchTermChanged(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.PRIMARY}
|
||||||
|
text={getString('newPullRequest')}
|
||||||
|
icon={CodeIcon.Add}
|
||||||
|
onClick={() => {
|
||||||
|
history.push(
|
||||||
|
routes.toCODECompare({
|
||||||
|
repoPath: repoMetadata?.path as string,
|
||||||
|
diffRefs: makeDiffRefs(repoMetadata?.defaultBranch as string, '')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
7
web/src/pages/PullRequests/pull-request-failed.svg
Normal file
7
web/src/pages/PullRequests/pull-request-failed.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="4.64844" cy="4.65039" r="2.25" stroke="#383946"/>
|
||||||
|
<path d="M4.64844 6.84475V12.4926V17.1981" stroke="#383946"/>
|
||||||
|
<circle cx="4.64844" cy="19.1768" r="2.25" stroke="#383946"/>
|
||||||
|
<path d="M19.3438 3.92676V16.9268" stroke="#383946" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 2"/>
|
||||||
|
<circle cx="19.3438" cy="19.1768" r="2.25" stroke="#383946"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 475 B |
8
web/src/pages/PullRequests/pull-request-open.svg
Normal file
8
web/src/pages/PullRequests/pull-request-open.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<ellipse cx="4.51611" cy="4.2373" rx="2.25" ry="2.25" stroke="#4F5162"/>
|
||||||
|
<path d="M4.51611 6.43166V12.0795V16.785" stroke="#4F5162"/>
|
||||||
|
<ellipse cx="4.51611" cy="19.2373" rx="2.25" ry="2.25" stroke="#4F5162"/>
|
||||||
|
<path d="M14.506 7.60547L10.7088 4.7207M10.7088 4.7207L14.506 1.82822M10.7088 4.7207H16.7878" stroke="#4F5162" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13.7483 4.71635H18.8538C19.1299 4.71635 19.3538 4.9402 19.3538 5.21635V16.5127" stroke="#4F5162" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<ellipse cx="19.4841" cy="19.2373" rx="2.25" ry="2.25" stroke="#4F5162"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 700 B |
@ -11,7 +11,7 @@ import { useGetPaginationInfo } from 'hooks/useGetPaginationInfo'
|
|||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||||
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||||
import { CommitsContent } from './RepositoryCommitsContent/CommitsContent/CommitsContent'
|
import { CommitsView } from '../../components/CommitsView/CommitsView'
|
||||||
import css from './RepositoryCommits.module.scss'
|
import css from './RepositoryCommits.module.scss'
|
||||||
|
|
||||||
export default function RepositoryCommits() {
|
export default function RepositoryCommits() {
|
||||||
@ -71,7 +71,7 @@ export default function RepositoryCommits() {
|
|||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<CommitsContent commits={commits} repoMetadata={repoMetadata} />
|
<CommitsView commits={commits} repoMetadata={repoMetadata} />
|
||||||
|
|
||||||
<Container margin={{ left: 'large', right: 'large' }}>
|
<Container margin={{ left: 'large', right: 'large' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
@ -53,7 +53,6 @@ export default function CreateWehookForm() {
|
|||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
dataTooltipId: 'secret'
|
dataTooltipId: 'secret'
|
||||||
}}
|
}}
|
||||||
inputGroup={{ autoFocus: true }}
|
|
||||||
/>
|
/>
|
||||||
<FormGroup className={css.eventRadioGroup}>
|
<FormGroup className={css.eventRadioGroup}>
|
||||||
<FormInput.RadioGroup
|
<FormInput.RadioGroup
|
||||||
|
@ -1,25 +1,37 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Container, PageBody } from '@harness/uicore'
|
import { Container, PageBody } from '@harness/uicore'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
import { RepositoryCreateWebhookHeader } from './RepositoryCreateWebhookHeader'
|
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||||
|
import { useAppContext } from 'AppContext'
|
||||||
import CreateWebhookForm from './CreateWebhookForm'
|
import CreateWebhookForm from './CreateWebhookForm'
|
||||||
|
|
||||||
import css from './RepositoryCreateWebhook.module.scss'
|
import css from './RepositoryCreateWebhook.module.scss'
|
||||||
|
|
||||||
export default function RepositoryCreateWebhook() {
|
export default function RepositoryCreateWebhook() {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { routes } = useAppContext()
|
||||||
const { repoMetadata, error, loading } = useGetRepositoryMetadata()
|
const { repoMetadata, error, loading } = useGetRepositoryMetadata()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
|
<RepositoryPageHeader
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
|
title={getString('webhook')}
|
||||||
|
dataTooltipId="settingsWebhook"
|
||||||
|
extraBreadcrumbLinks={
|
||||||
|
repoMetadata && [
|
||||||
|
{
|
||||||
|
label: getString('settings'),
|
||||||
|
url: routes.toCODESettings({ repoPath: repoMetadata.path as string })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
<PageBody loading={loading} error={error}>
|
<PageBody loading={loading} error={error}>
|
||||||
{repoMetadata ? (
|
{repoMetadata ? (
|
||||||
<>
|
<Container className={css.resourceContent}>
|
||||||
<RepositoryCreateWebhookHeader repoMetadata={repoMetadata} />
|
<CreateWebhookForm />
|
||||||
<Container className={css.resourceContent}>
|
</Container>
|
||||||
<CreateWebhookForm />
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Container, Layout, Text, Color, Icon, FontVariation } from '@harness/uicore'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { useStrings } from 'framework/strings'
|
|
||||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
|
||||||
import { useAppContext } from 'AppContext'
|
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
|
||||||
|
|
||||||
import css from './RepositoryCreateWebhook.module.scss'
|
|
||||||
|
|
||||||
export function RepositoryCreateWebhookHeader({ repoMetadata }: Pick<GitInfoProps, 'repoMetadata'>) {
|
|
||||||
const { getString } = useStrings()
|
|
||||||
const space = useGetSpaceParam()
|
|
||||||
const { routes } = useAppContext()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className={css.header}>
|
|
||||||
<Container>
|
|
||||||
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
|
||||||
<Link to={routes.toCODERepositories({ space })}>{getString('repositories')}</Link>
|
|
||||||
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} />
|
|
||||||
<Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string })}>{repoMetadata.uid}</Link>
|
|
||||||
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} />
|
|
||||||
<Link to={routes.toCODESettings({ repoPath: repoMetadata.path as string })}>Settings</Link>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
<Container padding={{ top: 'medium', bottom: 'medium' }}>
|
|
||||||
<Text font={{ variation: FontVariation.H4 }}>
|
|
||||||
<Icon name="main-chevron-right" size={10} color={Color.GREY_500} className={css.addVerticalAlign} />
|
|
||||||
{getString('webhook')}
|
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
</Container>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
@ -48,8 +48,22 @@ export enum GitCommitAction {
|
|||||||
MOVE = 'MOVE'
|
MOVE = 'MOVE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PullRequestState {
|
||||||
|
OPEN = 'open',
|
||||||
|
CLOSED = 'closed',
|
||||||
|
MERGED = 'merged',
|
||||||
|
REJECTED = 'rejected'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PullRequestFilterOption = {
|
||||||
|
...PullRequestState,
|
||||||
|
YOURS: 'yours',
|
||||||
|
ALL: 'all'
|
||||||
|
}
|
||||||
|
|
||||||
export const CodeIcon = {
|
export const CodeIcon = {
|
||||||
PullRequest: 'git-pull' as IconName,
|
PullRequest: 'git-pull' as IconName,
|
||||||
|
PullRequestRejected: 'main-close' as IconName,
|
||||||
Add: 'plus' as IconName,
|
Add: 'plus' as IconName,
|
||||||
BranchSmall: 'code-branch-small' as IconName,
|
BranchSmall: 'code-branch-small' as IconName,
|
||||||
Branch: 'code-branch' as IconName,
|
Branch: 'code-branch' as IconName,
|
||||||
|
@ -21,7 +21,7 @@ export function showToaster(message: string, props?: Partial<IToastProps>): IToa
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getErrorMessage = (error: Unknown): string =>
|
export const getErrorMessage = (error: Unknown): string =>
|
||||||
get(error, 'data.error', get(error, 'data.message', error?.message))
|
get(error, 'data.error', get(error, 'data.message', get(error, 'message', error)))
|
||||||
|
|
||||||
export interface SourceCodeEditorProps {
|
export interface SourceCodeEditorProps {
|
||||||
source: string
|
source: string
|
||||||
@ -78,6 +78,15 @@ export function formatDate(timestamp: number | string, dateStyle = 'medium'): st
|
|||||||
}).format(new Date(timestamp))
|
}).format(new Date(timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a number with current locale.
|
||||||
|
* @param num number
|
||||||
|
* @returns Formatted string.
|
||||||
|
*/
|
||||||
|
export function formatNumber(num: number | bigint): string {
|
||||||
|
return new Intl.NumberFormat(LOCALE).format(num)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make any HTML element as a clickable button with keyboard accessibility
|
* Make any HTML element as a clickable button with keyboard accessibility
|
||||||
* support (hit Enter/Space will trigger click event)
|
* support (hit Enter/Space will trigger click event)
|
||||||
@ -89,7 +98,8 @@ export const ButtonRoleProps = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
role: 'button'
|
role: 'button',
|
||||||
|
style: { cursor: 'pointer ' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONACO_SUPPORTED_LANGUAGES = [
|
const MONACO_SUPPORTED_LANGUAGES = [
|
||||||
@ -173,3 +183,15 @@ export const filenameToLanguage = (name?: string): string | undefined => {
|
|||||||
|
|
||||||
return PLAIN_TEXT
|
return PLAIN_TEXT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function waitUntil(condition: () => boolean, callback: () => void, maxCount = 100, timeout = 50) {
|
||||||
|
if (condition()) {
|
||||||
|
callback()
|
||||||
|
} else {
|
||||||
|
if (maxCount) {
|
||||||
|
setTimeout(() => {
|
||||||
|
waitUntil(condition, callback, maxCount - 1)
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7802,16 +7802,26 @@ diff-sequences@^29.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f"
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f"
|
||||||
integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==
|
integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==
|
||||||
|
|
||||||
|
diff2html@3.4.22:
|
||||||
|
version "3.4.22"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff2html/-/diff2html-3.4.22.tgz#ae9d0f1949a5858141c737dd0b3a6d7fbcee85a2"
|
||||||
|
integrity sha512-n7b0cfjqG+NLAmxdlSRMXIGoeoK+OhCNxd/6InjjMxy3v5Gk1L5Ms5r4jziFiRBoNaB6L8lLyZ8KQ8Kv+DOgnw==
|
||||||
|
dependencies:
|
||||||
|
diff "5.1.0"
|
||||||
|
hogan.js "3.0.2"
|
||||||
|
optionalDependencies:
|
||||||
|
highlight.js "11.6.0"
|
||||||
|
|
||||||
|
diff@5.1.0, diff@^5.0.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
|
||||||
|
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
|
||||||
|
|
||||||
diff@^4.0.1:
|
diff@^4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||||
|
|
||||||
diff@^5.0.0:
|
|
||||||
version "5.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
|
|
||||||
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
|
|
||||||
|
|
||||||
diffie-hellman@^5.0.0:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
@ -10162,7 +10172,7 @@ highcharts@9.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-9.1.0.tgz#2cdb38e2e03530b4fde022bb05fbce5b34651e39"
|
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-9.1.0.tgz#2cdb38e2e03530b4fde022bb05fbce5b34651e39"
|
||||||
integrity sha512-K7HUuKhEylZ1pMdzGR35kPgUmpp0MDNpaWhEMkGiC5Jfzg/endtTLHJN2lsFqEO+xoN7AykBK98XaJPEpsrLyA==
|
integrity sha512-K7HUuKhEylZ1pMdzGR35kPgUmpp0MDNpaWhEMkGiC5Jfzg/endtTLHJN2lsFqEO+xoN7AykBK98XaJPEpsrLyA==
|
||||||
|
|
||||||
highlight.js@~11.6.0:
|
highlight.js@11.6.0, highlight.js@~11.6.0:
|
||||||
version "11.6.0"
|
version "11.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.6.0.tgz#a50e9da05763f1bb0c1322c8f4f755242cff3f5a"
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.6.0.tgz#a50e9da05763f1bb0c1322c8f4f755242cff3f5a"
|
||||||
integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==
|
integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==
|
||||||
@ -10188,6 +10198,14 @@ hmac-drbg@^1.0.1:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
|
hogan.js@3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd"
|
||||||
|
integrity sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==
|
||||||
|
dependencies:
|
||||||
|
mkdirp "0.3.0"
|
||||||
|
nopt "1.0.10"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
@ -13771,6 +13789,11 @@ mixin-deep@^1.2.0:
|
|||||||
for-in "^1.0.2"
|
for-in "^1.0.2"
|
||||||
is-extendable "^1.0.1"
|
is-extendable "^1.0.1"
|
||||||
|
|
||||||
|
mkdirp@0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
|
||||||
|
integrity sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==
|
||||||
|
|
||||||
mkdirp@0.5.x, mkdirp@^0.5.1, mkdirp@^0.5.3:
|
mkdirp@0.5.x, mkdirp@^0.5.1, mkdirp@^0.5.3:
|
||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||||
@ -14123,7 +14146,7 @@ noms@0.0.0:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
readable-stream "~1.0.31"
|
readable-stream "~1.0.31"
|
||||||
|
|
||||||
nopt@~1.0.10:
|
nopt@1.0.10, nopt@~1.0.10:
|
||||||
version "1.0.10"
|
version "1.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
|
||||||
integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
|
integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
|
||||||
@ -15873,6 +15896,11 @@ react-inspector@^5.1.0:
|
|||||||
is-dom "^1.0.0"
|
is-dom "^1.0.0"
|
||||||
prop-types "^15.0.0"
|
prop-types "^15.0.0"
|
||||||
|
|
||||||
|
react-intersection-observer@^9.4.1:
|
||||||
|
version "9.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz#4ccb21e16acd0b9cf5b28d275af7055bef878f6b"
|
||||||
|
integrity sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==
|
||||||
|
|
||||||
react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
|
react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
|
Loading…
Reference in New Issue
Block a user