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",
|
||||
"cron-validator": "^1.2.1",
|
||||
"cronstrue": "^1.114.0",
|
||||
"diff2html": "3.4.22",
|
||||
"event-source-polyfill": "^1.0.22",
|
||||
"formik": "2.2.9",
|
||||
"github-markdown-css": "^5.1.0",
|
||||
@ -83,6 +84,7 @@
|
||||
"react-contenteditable": "^3.3.5",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.2",
|
||||
"react-intersection-observer": "^9.4.1",
|
||||
"react-join": "^1.1.4",
|
||||
"react-keywords": "^0.0.5",
|
||||
"react-lottie-player": "^1.4.0",
|
||||
|
@ -8,6 +8,7 @@ export interface CODEProps {
|
||||
branch?: string
|
||||
diffRefs?: string
|
||||
pullRequestId?: string
|
||||
pullRequestSection?: string
|
||||
}
|
||||
|
||||
export interface CODEQueryProps {
|
||||
@ -21,7 +22,8 @@ export const pathProps: Readonly<Omit<Required<CODEProps>, 'repoPath' | 'branch'
|
||||
resourcePath: ':resourcePath*',
|
||||
commitRef: ':commitRef*',
|
||||
diffRefs: ':diffRefs*',
|
||||
pullRequestId: ':pullRequestId'
|
||||
pullRequestId: ':pullRequestId',
|
||||
pullRequestSection: ':pullRequestSection*'
|
||||
}
|
||||
|
||||
export interface CODERoutes {
|
||||
@ -32,7 +34,12 @@ export interface CODERoutes {
|
||||
toCODEFileEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'gitRef' | 'resourcePath'>>) => string
|
||||
toCODECommits: (args: Required<Pick<CODEProps, 'repoPath' | 'commitRef'>>) => 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
|
||||
toCODEBranches: (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}`,
|
||||
toCODECommits: ({ repoPath, commitRef }) => `/${repoPath}/commits/${commitRef}`,
|
||||
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}`,
|
||||
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
||||
toCODESettings: ({ repoPath }) => `/${repoPath}/settings`,
|
||||
|
@ -1,7 +1,6 @@
|
||||
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 { StringSubstitute } from 'components/StringSubstitute/StringSubstitute'
|
||||
import css from './CommitDivergence.module.scss'
|
||||
|
||||
interface CommitDivergenceProps {
|
||||
|
@ -8,13 +8,13 @@ import type { RepoCommit } from 'services/code'
|
||||
import { CommitActions } from 'components/CommitActions/CommitActions'
|
||||
import { formatDate } from 'utils/Utils'
|
||||
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[]
|
||||
}
|
||||
|
||||
export function CommitsContent({ repoMetadata, commits }: CommitsContentProps) {
|
||||
export function CommitsView({ repoMetadata, commits }: CommitsViewProps) {
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
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
|
||||
addLicense: string
|
||||
addReadMe: string
|
||||
all: string
|
||||
allBranches: string
|
||||
allEvents: string
|
||||
botAlerts: string
|
||||
@ -31,6 +32,7 @@ export interface StringsMap {
|
||||
checkSuites: string
|
||||
clone: string
|
||||
cloneHTTPS: string
|
||||
closed: string
|
||||
commit: string
|
||||
commitChanges: string
|
||||
commitDirectlyTo: string
|
||||
@ -91,6 +93,7 @@ export interface StringsMap {
|
||||
inactiveBranches: string
|
||||
individualEvents: string
|
||||
loading: string
|
||||
merged: string
|
||||
name: string
|
||||
nameYourBranch: string
|
||||
nameYourFile: string
|
||||
@ -105,6 +108,7 @@ export interface StringsMap {
|
||||
none: string
|
||||
ok: string
|
||||
onDate: string
|
||||
open: string
|
||||
optional: string
|
||||
optionalExtendedDescription: string
|
||||
pageNotFound: string
|
||||
@ -115,10 +119,22 @@ export interface StringsMap {
|
||||
'pr.buttonText': string
|
||||
'pr.cantMerge': string
|
||||
'pr.descriptionPlaceHolder': string
|
||||
'pr.diffStatus': string
|
||||
'pr.diffView': string
|
||||
'pr.failedToCreate': string
|
||||
'pr.fileDeleted': string
|
||||
'pr.fileUnchanged': string
|
||||
'pr.metaLine': 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.unified': string
|
||||
prefixBase: string
|
||||
prefixCompare: string
|
||||
prev: string
|
||||
@ -128,6 +144,7 @@ export interface StringsMap {
|
||||
pullRequestEmpty: string
|
||||
pullRequests: string
|
||||
pushEvent: string
|
||||
rejected: string
|
||||
renameFile: string
|
||||
'repos.activities': string
|
||||
'repos.data': string
|
||||
@ -139,6 +156,7 @@ export interface StringsMap {
|
||||
repositories: string
|
||||
samplePayloadUrl: string
|
||||
scanAlerts: string
|
||||
scrollToTop: string
|
||||
search: string
|
||||
searchBranches: string
|
||||
secret: string
|
||||
@ -165,4 +183,5 @@ export interface StringsMap {
|
||||
webhookListingContent: string
|
||||
webhooks: 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
|
||||
buttonText: Open pull request
|
||||
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 ...'
|
||||
general: 'General'
|
||||
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 { Container, PageBody, NoDataCard, Tabs } from '@harness/uicore'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useGet } from 'restful-react'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
@ -8,6 +9,8 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import emptyStateImage from 'images/empty-state.svg'
|
||||
import { makeDiffRefs } from 'utils/GitUtils'
|
||||
import { CommitsView } from 'components/CommitsView/CommitsView'
|
||||
import type { RepoCommit } from 'services/code'
|
||||
import { CompareContentHeader } from './CompareContentHeader/CompareContentHeader'
|
||||
import css from './Compare.module.scss'
|
||||
|
||||
@ -18,6 +21,18 @@ export default function Compare() {
|
||||
const { repoMetadata, error, loading, refetch, diffRefs } = useGetRepositoryMetadata()
|
||||
const [sourceGitRef, setSourceGitRef] = useState(diffRefs.sourceGitRef)
|
||||
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 (
|
||||
<Container className={css.main}>
|
||||
@ -60,22 +75,28 @@ export default function Compare() {
|
||||
</Container>
|
||||
))}
|
||||
|
||||
{!!targetGitRef && !!sourceGitRef && (
|
||||
{!!repoMetadata && !!targetGitRef && !!sourceGitRef && (
|
||||
<Container className={css.tabsContainer} padding="xlarge">
|
||||
<Tabs
|
||||
id="branchesTags"
|
||||
defaultSelectedTabId={'diff'}
|
||||
defaultSelectedTabId={'commits'}
|
||||
large={false}
|
||||
tabList={[
|
||||
{
|
||||
id: 'commits',
|
||||
title: getString('commits'),
|
||||
panel: <div>Commit</div>
|
||||
panel: commits?.length ? (
|
||||
<Container padding={{ top: 'xlarge' }}>
|
||||
<CommitsView commits={commits} repoMetadata={repoMetadata} />
|
||||
</Container>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: '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(
|
||||
routes.toCODEPullRequest({
|
||||
repoPath: repoMetadata.path as string,
|
||||
pullRequestId: String(data.id)
|
||||
pullRequestId: String(data.number)
|
||||
})
|
||||
)
|
||||
}}
|
||||
|
@ -9,6 +9,14 @@
|
||||
.tabsContainer {
|
||||
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'] {
|
||||
background-color: var(--white) !important;
|
||||
padding-left: var(--spacing-medium);
|
||||
@ -21,9 +29,10 @@
|
||||
}
|
||||
|
||||
[aria-selected='true'] {
|
||||
.tabTitle {
|
||||
color: var(--grey-900);
|
||||
font-weight: 600;
|
||||
.tabTitle,
|
||||
.tabTitle:hover {
|
||||
color: var(--grey-900) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,3 +60,8 @@
|
||||
color: var(--grey-500);
|
||||
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 count: string
|
||||
readonly prNumber: string
|
||||
readonly tabContentContainer: string
|
||||
}
|
||||
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 { useGet } from 'restful-react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
@ -8,16 +9,30 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import type { PullRequestResponse } from 'utils/types'
|
||||
import { CodeIcon } from 'utils/GitUtils'
|
||||
import { PullRequestMetadataInfo } from './PullRequestMetadataInfo'
|
||||
import { PullRequestMetaLine } from './PullRequestMetaLine'
|
||||
import { PullRequestConversation } from './PullRequestConversation/PullRequestConversation'
|
||||
import { PullRequestDiff } from './PullRequestDiff/PullRequestDiff'
|
||||
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
||||
import css from './PullRequest.module.scss'
|
||||
|
||||
enum PullRequestSection {
|
||||
CONVERSATION = 'conversation',
|
||||
COMMITS = 'commits',
|
||||
DIFFS = 'diffs'
|
||||
}
|
||||
|
||||
export default function PullRequest() {
|
||||
const history = useHistory()
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
const { repoMetadata, error, loading, refetch, pullRequestId } = useGetRepositoryMetadata()
|
||||
const {
|
||||
repoMetadata,
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
pullRequestId,
|
||||
pullRequestSection = PullRequestSection.CONVERSATION
|
||||
} = useGetRepositoryMetadata()
|
||||
const {
|
||||
data: prData,
|
||||
error: prError,
|
||||
@ -26,6 +41,13 @@ export default function PullRequest() {
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
|
||||
lazy: !repoMetadata
|
||||
})
|
||||
const activeTab = useMemo(
|
||||
() =>
|
||||
Object.values(PullRequestSection).find(value => value === pullRequestSection)
|
||||
? pullRequestSection
|
||||
: PullRequestSection.CONVERSATION,
|
||||
[pullRequestSection]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
@ -46,26 +68,35 @@ export default function PullRequest() {
|
||||
{repoMetadata ? (
|
||||
prData ? (
|
||||
<>
|
||||
<PullRequestMetadataInfo repoMetadata={repoMetadata} {...prData} />
|
||||
<PullRequestMetaLine repoMetadata={repoMetadata} {...prData} />
|
||||
<Container className={css.tabsContainer}>
|
||||
<Tabs
|
||||
id="pullRequestTabs"
|
||||
defaultSelectedTabId={'conversation'}
|
||||
defaultSelectedTabId={activeTab}
|
||||
large={false}
|
||||
onChange={tabId => {
|
||||
history.replace(
|
||||
routes.toCODEPullRequest({
|
||||
repoPath: repoMetadata.path as string,
|
||||
pullRequestId,
|
||||
pullRequestSection: tabId !== PullRequestSection.CONVERSATION ? (tabId as string) : undefined
|
||||
})
|
||||
)
|
||||
}}
|
||||
tabList={[
|
||||
{
|
||||
id: 'conversation',
|
||||
id: PullRequestSection.CONVERSATION,
|
||||
title: <TabTitle icon={CodeIcon.Chat} title={getString('conversation')} count={100} />,
|
||||
panel: <PullRequestConversation repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||
},
|
||||
{
|
||||
id: 'commits',
|
||||
id: PullRequestSection.COMMITS,
|
||||
title: <TabTitle icon={CodeIcon.Commit} title={getString('commits')} count={15} />,
|
||||
panel: <PullRequestCommits repoMetadata={repoMetadata} pullRequestMetadata={prData} />
|
||||
},
|
||||
{
|
||||
id: 'diff',
|
||||
title: <TabTitle icon={CodeIcon.Commit} title={getString('diff')} count={20} />,
|
||||
id: PullRequestSection.DIFFS,
|
||||
title: <TabTitle icon={CodeIcon.File} title={getString('diff')} count={20} />,
|
||||
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 }) => (
|
||||
<Text icon={icon} className={css.tabTitle}>
|
||||
{title}{' '}
|
||||
{title}
|
||||
{!!count && (
|
||||
<Text inline className={css.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 { Container } from '@harness/uicore'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useGet } from 'restful-react'
|
||||
import type { RepoCommit } from 'services/code'
|
||||
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'>> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
const {
|
||||
data: commits,
|
||||
error,
|
||||
loading,
|
||||
refetch
|
||||
} = useGet<RepoCommit[]>({
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
|
||||
queryParams: {
|
||||
git_ref: pullRequestMetadata.sourceBranch
|
||||
},
|
||||
lazy: !repoMetadata
|
||||
})
|
||||
|
||||
return (
|
||||
<Container className={css.main} padding="xlarge">
|
||||
COMMITS...
|
||||
</Container>
|
||||
<PullRequestTabContentWrapper loading={loading} error={error} onRetry={() => refetch()}>
|
||||
{!!commits?.length && <CommitsView commits={commits} repoMetadata={repoMetadata} />}
|
||||
</PullRequestTabContentWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +1,18 @@
|
||||
import React from 'react'
|
||||
import { Container } from '@harness/uicore'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useStrings } from 'framework/strings'
|
||||
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'>> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
|
||||
return (
|
||||
<Container className={css.main} padding="xlarge">
|
||||
CONVERSATION...
|
||||
</Container>
|
||||
<PullRequestTabContentWrapper loading={undefined} error={undefined} onRetry={() => {}}>
|
||||
<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 { Container } from '@harness/uicore'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
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 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'
|
||||
|
||||
export const PullRequestDiff: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata
|
||||
}) => {
|
||||
const STICKY_TOP_POSITION = 64
|
||||
|
||||
export const PullRequestDiff: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = () => {
|
||||
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 (
|
||||
<Container className={css.main} padding="xlarge">
|
||||
DIFF
|
||||
</Container>
|
||||
<PullRequestTabContentWrapper loading={undefined} error={undefined} onRetry={noop} className={css.wrapper}>
|
||||
<Container className={css.header}>
|
||||
<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: {
|
||||
readonly main: string
|
||||
readonly state: string
|
||||
readonly merged: string
|
||||
readonly closed: string
|
||||
readonly rejected: string
|
||||
readonly metaline: 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 {
|
||||
min-height: var(--page-min-height, 100%);
|
||||
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
|
||||
declare const styles: {
|
||||
readonly main: string
|
||||
readonly table: string
|
||||
readonly row: string
|
||||
readonly title: string
|
||||
}
|
||||
export default styles
|
||||
|
@ -1,20 +1,88 @@
|
||||
import React from 'react'
|
||||
import { Button, Container, ButtonVariation, PageBody } from '@harness/uicore'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
ButtonVariation,
|
||||
PageBody,
|
||||
Text,
|
||||
Color,
|
||||
TableV2,
|
||||
Layout,
|
||||
StringSubstitute
|
||||
} from '@harness/uicore'
|
||||
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 { useAppContext } from 'AppContext'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
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 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'
|
||||
|
||||
export default function PullRequests() {
|
||||
const { getString } = useStrings()
|
||||
const history = useHistory()
|
||||
const { routes } = useAppContext()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [pageIndex, setPageIndex] = usePageIndex()
|
||||
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 (
|
||||
<Container className={css.main}>
|
||||
@ -24,11 +92,13 @@ export default function PullRequests() {
|
||||
dataTooltipId="repositoryPullRequests"
|
||||
/>
|
||||
<PageBody
|
||||
loading={loading}
|
||||
error={getErrorMessage(error)}
|
||||
loading={loading || prLoading}
|
||||
error={getErrorMessage(error || prError)}
|
||||
retryOnError={() => refetch()}
|
||||
noData={{
|
||||
when: () => repoMetadata !== null,
|
||||
// TODO: Use NoDataCard, this won't render toolbar
|
||||
// when search returns empty response
|
||||
when: () => data?.length === 0,
|
||||
message: getString('pullRequestEmpty'),
|
||||
image: emptyStateImage,
|
||||
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>
|
||||
</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 { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||
import { CommitsContent } from './RepositoryCommitsContent/CommitsContent/CommitsContent'
|
||||
import { CommitsView } from '../../components/CommitsView/CommitsView'
|
||||
import css from './RepositoryCommits.module.scss'
|
||||
|
||||
export default function RepositoryCommits() {
|
||||
@ -71,7 +71,7 @@ export default function RepositoryCommits() {
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
|
||||
<CommitsContent commits={commits} repoMetadata={repoMetadata} />
|
||||
<CommitsView commits={commits} repoMetadata={repoMetadata} />
|
||||
|
||||
<Container margin={{ left: 'large', right: 'large' }}>
|
||||
<Pagination
|
||||
|
@ -53,7 +53,6 @@ export default function CreateWehookForm() {
|
||||
tooltipProps={{
|
||||
dataTooltipId: 'secret'
|
||||
}}
|
||||
inputGroup={{ autoFocus: true }}
|
||||
/>
|
||||
<FormGroup className={css.eventRadioGroup}>
|
||||
<FormInput.RadioGroup
|
||||
|
@ -1,25 +1,37 @@
|
||||
import React from 'react'
|
||||
import { Container, PageBody } from '@harness/uicore'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
|
||||
import { RepositoryCreateWebhookHeader } from './RepositoryCreateWebhookHeader'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import CreateWebhookForm from './CreateWebhookForm'
|
||||
|
||||
import css from './RepositoryCreateWebhook.module.scss'
|
||||
|
||||
export default function RepositoryCreateWebhook() {
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
const { repoMetadata, error, loading } = useGetRepositoryMetadata()
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{repoMetadata ? (
|
||||
<>
|
||||
<RepositoryCreateWebhookHeader repoMetadata={repoMetadata} />
|
||||
<Container className={css.resourceContent}>
|
||||
<CreateWebhookForm />
|
||||
</Container>
|
||||
</>
|
||||
<Container className={css.resourceContent}>
|
||||
<CreateWebhookForm />
|
||||
</Container>
|
||||
) : null}
|
||||
</PageBody>
|
||||
</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'
|
||||
}
|
||||
|
||||
export enum PullRequestState {
|
||||
OPEN = 'open',
|
||||
CLOSED = 'closed',
|
||||
MERGED = 'merged',
|
||||
REJECTED = 'rejected'
|
||||
}
|
||||
|
||||
export const PullRequestFilterOption = {
|
||||
...PullRequestState,
|
||||
YOURS: 'yours',
|
||||
ALL: 'all'
|
||||
}
|
||||
|
||||
export const CodeIcon = {
|
||||
PullRequest: 'git-pull' as IconName,
|
||||
PullRequestRejected: 'main-close' as IconName,
|
||||
Add: 'plus' as IconName,
|
||||
BranchSmall: 'code-branch-small' 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 =>
|
||||
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 {
|
||||
source: string
|
||||
@ -78,6 +78,15 @@ export function formatDate(timestamp: number | string, dateStyle = 'medium'): st
|
||||
}).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
|
||||
* support (hit Enter/Space will trigger click event)
|
||||
@ -89,7 +98,8 @@ export const ButtonRoleProps = {
|
||||
}
|
||||
},
|
||||
tabIndex: 0,
|
||||
role: 'button'
|
||||
role: 'button',
|
||||
style: { cursor: 'pointer ' }
|
||||
}
|
||||
|
||||
const MONACO_SUPPORTED_LANGUAGES = [
|
||||
@ -173,3 +183,15 @@ export const filenameToLanguage = (name?: string): string | undefined => {
|
||||
|
||||
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"
|
||||
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:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
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:
|
||||
version "5.0.3"
|
||||
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"
|
||||
integrity sha512-K7HUuKhEylZ1pMdzGR35kPgUmpp0MDNpaWhEMkGiC5Jfzg/endtTLHJN2lsFqEO+xoN7AykBK98XaJPEpsrLyA==
|
||||
|
||||
highlight.js@~11.6.0:
|
||||
highlight.js@11.6.0, highlight.js@~11.6.0:
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.6.0.tgz#a50e9da05763f1bb0c1322c8f4f755242cff3f5a"
|
||||
integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==
|
||||
@ -10188,6 +10198,14 @@ hmac-drbg@^1.0.1:
|
||||
minimalistic-assert "^1.0.0"
|
||||
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:
|
||||
version "3.3.2"
|
||||
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"
|
||||
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:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
@ -14123,7 +14146,7 @@ noms@0.0.0:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "~1.0.31"
|
||||
|
||||
nopt@~1.0.10:
|
||||
nopt@1.0.10, nopt@~1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
|
||||
integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
|
||||
@ -15873,6 +15896,11 @@ react-inspector@^5.1.0:
|
||||
is-dom "^1.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:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
|
Loading…
Reference in New Issue
Block a user