Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 69 additions & 64 deletions moon/apps/web/components/DiffView/FileDiff.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { DiffFile, DiffModeEnum, DiffView } from '@git-diff-view/react'

import { CommonResultFilesChangedList } from '@gitmono/types/generated'
import { ExpandIcon, SparklesIcon } from '@gitmono/ui/Icons'
import { cn } from '@gitmono/ui/src/utils'

import { parsedDiffs } from '@/components/DiffView/parsedDiffs'

import StableTreeView from './StableTreeView'

// import TreeView from './TreeView'

function calculateDiffStatsFromRawDiff(diffText: string): { additions: number; deletions: number } {
const lines = diffText.split('\n');
const lines = diffText.split('\n')

let additions = 0;
let additions = 0

let deletions = 0;
let deletions = 0

for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
Expand All @@ -24,66 +29,75 @@ function calculateDiffStatsFromRawDiff(diffText: string): { additions: number; d
return { additions, deletions }
}

function generateParsedFiles(diffFiles: { path: string; lang: string; diff: string }[]): {
file: { path: string; lang: string; diff: string };
instance: DiffFile | null;
stats: { additions: number; deletions: number }
function generateParsedFiles(diffFiles: { path: string; lang: string; diff: string }[]): {
file: { path: string; lang: string; diff: string }
instance: DiffFile | null
stats: { additions: number; deletions: number }
}[] {
return diffFiles.map((file) => {
if (file.lang === 'binary') {
return {
file,
instance: null,
stats: { additions: 0, deletions: 0 },
};
stats: { additions: 0, deletions: 0 }
}
}

const instance = new DiffFile('', '', '', '', [file.diff], file.lang);
const instance = new DiffFile('', '', '', '', [file.diff], file.lang)

try {
instance.init();
instance.buildSplitDiffLines();
instance.buildUnifiedDiffLines();
instance.init()
instance.buildSplitDiffLines()
instance.buildUnifiedDiffLines()
} catch (e) {
/* eslint-disable-next-line no-console */
console.error("error:", e);
console.error('error:', e)
}

const stats = calculateDiffStatsFromRawDiff(file.diff);
const stats = calculateDiffStatsFromRawDiff(file.diff)

return { file, instance, stats }
})
}

export default function FileDiff({ diffs }: { diffs: string }) {
const diffFiles = useMemo(() => parsedDiffs(diffs), [diffs]);
export default function FileDiff({
diffs,
treeData
}: {
diffs: string
treeData: CommonResultFilesChangedList['data']
}) {
const diffFiles = useMemo(() => parsedDiffs(diffs), [diffs])

const parsedFiles = useMemo(() => generateParsedFiles(diffFiles), [diffFiles]);
const parsedFiles = useMemo(() => generateParsedFiles(diffFiles), [diffFiles])

const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [_selectedPath, setSelectedPath] = useState<string | null>(null)

const [expandedMap, setExpandedMap] = useState<Record<string, boolean>>(() =>
Object.fromEntries(diffFiles.map((f) => [f.path, false]))
);
const fileRefs = useRef<Record<string, HTMLDivElement | null>>({});
)

const fileRefs = useRef<Record<string, HTMLDivElement | null>>({})

const toggleExpanded = (path: string) => {
setExpandedMap((prev) => ({ ...prev, [path]: !prev[path] }))
};
}

useEffect(() => {
setExpandedMap(Object.fromEntries(diffFiles.map((f) => [f.path, false])));
}, [diffFiles]);

const RenderDiffView = ({ file, instance }: {
file: { path: string; lang: string; diff: string };
instance: DiffFile | null;
setExpandedMap(Object.fromEntries(diffFiles.map((f) => [f.path, false])))
}, [diffFiles])

const RenderDiffView = ({
file,
instance
}: {
file: { path: string; lang: string; diff: string }
instance: DiffFile | null
}) => {
if (file.lang === 'binary' || instance === null) {
return <div className='text-center p-2'>Binary file</div>
}else if(file.diff === 'EMPTY_DIFF_MARKER\n') {
return <div className='text-center p-2'>No change</div>
return <div className='p-2 text-center'>Binary file</div>
} else if (file.diff === 'EMPTY_DIFF_MARKER\n') {
return <div className='p-2 text-center'>No change</div>
}

return (
Expand All @@ -98,32 +112,22 @@ export default function FileDiff({ diffs }: { diffs: string }) {
}

return (
<div className='flex font-sans mt-3'>
<div
className='rounded-lg w-[300px] h-[85vh] p-2 overflow-y-auto sticky top-5'
>
<ul>
{parsedFiles.map(({ file }) => (
<li
key={file.path}
onClick={() => {
setSelectedPath(file.path)
setExpandedMap((prev) => ({ ...prev, [file.path]: true }))
const el = fileRefs.current[file.path]

if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}}
className={cn('px-2 py-1 text-sm cursor-pointer rounded-md mb-1', selectedPath === file.path ? 'bg-[#e6f0ff]' : 'bg-transparent')}
>
{file.path}
</li>
))
}

</ul>
{/* <TreeView directory={parsedFiles} /> */}
<div className='mt-3 flex font-sans'>
<div className='sticky top-5 h-[85vh] w-[300px] overflow-y-auto rounded-lg p-2'>
{treeData?.mui_trees && (
<StableTreeView
treeData={treeData?.mui_trees}
handleClick={(file) => {
setSelectedPath(file)
setExpandedMap((prev) => ({ ...prev, [file]: true }))
const el = fileRefs.current[file]

if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}}
/>
)}
</div>

<div className='flex-1 overflow-y-auto px-4'>
Expand All @@ -138,9 +142,12 @@ export default function FileDiff({ diffs }: { diffs: string }) {
>
<div
onClick={() => toggleExpanded(file.path)}
className={cn('flex items-center justify-between px-4 py-2 text-sm', isExpanded && 'border-b border-gray-300')}
className={cn(
'flex items-center justify-between px-4 py-2 text-sm',
isExpanded && 'border-b border-gray-300'
)}
>
<span className='flex items-center cursor-pointer'>
<span className='flex cursor-pointer items-center'>
{isExpanded ? (
<SparklesIcon className='align-middle text-xl' />
) : (
Expand All @@ -153,10 +160,8 @@ export default function FileDiff({ diffs }: { diffs: string }) {
<span className='text-red-500'>−{stats.deletions}</span>
</span>
</div>

<div className='copyable-text'>
{isExpanded && <RenderDiffView file={file} instance={instance} />}
</div>

<div className='copyable-text'>{isExpanded && <RenderDiffView file={file} instance={instance} />}</div>
</div>
)
})}
Expand Down
70 changes: 70 additions & 0 deletions moon/apps/web/components/DiffView/StableTreeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { memo, useRef } from 'react'
import { FileIcon } from '@primer/octicons-react'
import { TreeView } from '@primer/react'

import { MuiTreeNode } from '@gitmono/types/generated'

const TreeViewItem = TreeView.Item
const TreeViewSub = TreeView.SubTree

const StableTreeView = ({
treeData,
handleClick
}: {
treeData: MuiTreeNode[]
handleClick?: (file: string) => void
}) => {
const currentId = useRef('')

return (
<TreeView aria-label='Files changed'>
<TreeLoop nodes={treeData} onSelect={(file) => handleClick?.(file)} currentId={currentId} />
</TreeView>
)
}

const TreeLoop = ({
nodes,
basePath,
onSelect,
currentId
}: {
nodes: MuiTreeNode[]
basePath?: string
onSelect?: (path: string) => void
currentId: React.MutableRefObject<string>
}) => {
return (
<>
{nodes.map((node) => {
const currentPath = basePath ? `${basePath}/${node.label}` : node.label

return (
<TreeViewItem
id={node.id}
key={node.id}
defaultExpanded
onSelect={() => {
currentId.current = node.id
onSelect?.(currentPath)
}}
current={currentId.current === node.id && !node.children}
>
<TreeView.LeadingVisual>
{node.children && node.children.length > 0 ? <TreeView.DirectoryIcon /> : <FileIcon />}
</TreeView.LeadingVisual>
{node.label}

{node.children && node.children.length > 0 && (
<TreeViewSub>
<TreeLoop nodes={node.children} basePath={currentPath} onSelect={onSelect} currentId={currentId} />
</TreeViewSub>
)}
</TreeViewItem>
)
})}
</>
)
}

export default memo(StableTreeView)
4 changes: 2 additions & 2 deletions moon/apps/web/pages/[org]/mr/[link]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const MRDetailPage: PageWithLayout<any> = () => {
<div className='flex gap-40'>
<div className='mt-3 flex w-[60%] flex-col'>
{detailIsLoading ? (
<div className='flex items-center justify-center h-16'>
<div className='flex h-16 items-center justify-center'>
<LoadingSpinner />
</div>
) : (
Expand Down Expand Up @@ -372,7 +372,7 @@ const MRDetailPage: PageWithLayout<any> = () => {
<LoadingSpinner />
</div>
) : MrFilesChangedData?.data?.content ? (
<FileDiff diffs={MrFilesChangedData.data.content} />
<FileDiff diffs={MrFilesChangedData.data.content} treeData={MrFilesChangedData.data} />
) : (
<div>No files changed</div>
)}
Expand Down