From 12f1547616180fc110d8b2772f6e51c87ff35241 Mon Sep 17 00:00:00 2001 From: cuisailong Date: Tue, 19 Aug 2025 17:46:39 +0800 Subject: [PATCH 1/3] feat(UI):The tree component child nodes collapse with the parent node --- .../components/CodeView/TreeView/RepoTree.tsx | 36 ++++++++++++------- .../components/CodeView/TreeView/TreeUtils.ts | 22 ++++++++++++ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx index b1d26a487..d27c1a3bd 100644 --- a/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx +++ b/moon/apps/web/components/CodeView/TreeView/RepoTree.tsx @@ -6,7 +6,7 @@ import { usePathname } from 'next/navigation'; import { useRouter } from 'next/router'; import { useGetTree } from '@/hooks/useGetTree'; import { legacyApiClient } from '@/utils/queryClient'; -import { convertToTreeData, generateExpandedPaths, mergeTreeNodes, findNode } from './TreeUtils'; +import { convertToTreeData, generateExpandedPaths, mergeTreeNodes, findNode, getDescendantIds } from './TreeUtils'; import { CustomTreeItem } from './CustomTreeItem'; import toast from 'react-hot-toast'; import { useAtom } from 'jotai'; @@ -50,20 +50,32 @@ const RepoTree = ({ onCommitInfoChange }: { onCommitInfoChange?:Function }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [treeItems]); - const handleNodeToggle = useCallback((_event: React.SyntheticEvent | null, nodeIds: string[]) => { - const newlyExpandedIds = nodeIds.filter(id => !expandedNodes.includes(id)); - - newlyExpandedIds.forEach(nodeId => { - const existingNode = findNode(treeAllData, nodeId); - const hasRealData = existingNode?.children && !existingNode?.children[0].isPlaceholder +const handleNodeToggle = useCallback((_event: React.SyntheticEvent | null, nodeIds: string[]) => { + const collapsedNodes = expandedNodes.filter(id => !nodeIds.includes(id)); + + let newExpandedIds = [...nodeIds]; + + if (collapsedNodes.length > 0) { + collapsedNodes.forEach(collapsedId => { + const descendantIds = getDescendantIds(treeAllData, collapsedId); - if (!loadingDirectories.has(nodeId) && !hasRealData) { - setLoadingDirectories(prev => new Set(prev).add(nodeId)); - } + newExpandedIds = newExpandedIds.filter(id => !descendantIds.includes(id)); }); + } + + const newlyExpandedIds = newExpandedIds.filter(id => !expandedNodes.includes(id)); + + newlyExpandedIds.forEach(nodeId => { + const existingNode = findNode(treeAllData, nodeId); + const hasRealData = existingNode?.children && !existingNode?.children[0].isPlaceholder; + + if (!loadingDirectories.has(nodeId) && !hasRealData) { + setLoadingDirectories(prev => new Set(prev).add(nodeId)); + } + }); - setExpandedNodes(nodeIds); - }, [expandedNodes, loadingDirectories, treeAllData, setLoadingDirectories, setExpandedNodes]); + setExpandedNodes(newExpandedIds); +}, [expandedNodes, loadingDirectories, treeAllData, setLoadingDirectories, setExpandedNodes]); useEffect(() => { loadingDirectories.forEach(path => { diff --git a/moon/apps/web/components/CodeView/TreeView/TreeUtils.ts b/moon/apps/web/components/CodeView/TreeView/TreeUtils.ts index 50347491c..34092b058 100644 --- a/moon/apps/web/components/CodeView/TreeView/TreeUtils.ts +++ b/moon/apps/web/components/CodeView/TreeView/TreeUtils.ts @@ -310,4 +310,26 @@ export function mergeTreeNodes(nodes1: MuiTreeNode[], nodes2: MuiTreeNode[]): Mu return sortProjectsByType(result); } +/** +* Recursively retrieves the IDs of all descendant nodes of a node. +* @param treeData: The complete tree data. +* @param nodeId: The ID of the parent node to be found. +* @returns: An array of the IDs of all descendant nodes under the node. +*/ +export function getDescendantIds(treeData: MuiTreeNode[], nodeId: string): string[] { + const node = findNode(treeData, nodeId); + + if (!node || !node.children) { + return []; + } + + let ids: string[] = []; + + node.children.forEach(child => { + ids.push(child.id); + ids = ids.concat(getDescendantIds(treeData, child.id)); + }); + + return ids; +} From bfa6748ca87467835827f37900953ddc0207f8d3 Mon Sep 17 00:00:00 2001 From: cuisailong Date: Tue, 19 Aug 2025 17:39:37 +0800 Subject: [PATCH 2/3] feat(UI):Tree component persistent data clearing processing --- .../components/CodeView/TreeView/codeTreeAtom.ts | 14 ++++++++++++++ moon/apps/web/pages/_app.tsx | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/moon/apps/web/components/CodeView/TreeView/codeTreeAtom.ts b/moon/apps/web/components/CodeView/TreeView/codeTreeAtom.ts index 94ac2d95f..a3f8b4343 100644 --- a/moon/apps/web/components/CodeView/TreeView/codeTreeAtom.ts +++ b/moon/apps/web/components/CodeView/TreeView/codeTreeAtom.ts @@ -1,7 +1,21 @@ import { atomWithWebStorage } from '@/utils/atomWithWebStorage' import { MuiTreeNode } from './TreeUtils' +import { RESET } from "jotai/utils"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; export const treeAllDataAtom = atomWithWebStorage('treeAllDataAtom', []) export const expandedNodesAtom = atomWithWebStorage('expandedNodes', []) + +export function useClearTreeAtoms(setTreeAllData: (v: any) => void, setExpandedNodes: (v: any) => void) { + const router = useRouter(); + + useEffect(() => { + if (!router.asPath.startsWith(`/${router.query.org}/code`)) { + setTreeAllData(RESET); + setExpandedNodes(RESET); + } + }, [router.asPath, router.query.org, setTreeAllData, setExpandedNodes]); +} \ No newline at end of file diff --git a/moon/apps/web/pages/_app.tsx b/moon/apps/web/pages/_app.tsx index 9f00c6f21..33a01a331 100644 --- a/moon/apps/web/pages/_app.tsx +++ b/moon/apps/web/pages/_app.tsx @@ -16,6 +16,8 @@ import { IS_PRODUCTION, LAST_CLIENT_JS_BUILD_ID_LS_KEY } from '@gitmono/config' import { useClearEmptyDrafts } from '@/hooks/useClearEmptyDrafts' import { useStoredState } from '@/hooks/useStoredState' import { AppPropsWithLayout } from '@/utils/types' +import { useSetAtom } from "jotai"; +import { treeAllDataAtom, expandedNodesAtom, useClearTreeAtoms } from "@/components/CodeView/TreeView/codeTreeAtom"; const inter = Inter({ subsets: ['latin'], @@ -26,8 +28,13 @@ export default function App({ Component, pageProps }: AppPropsWithLayout): const getProviders = Component.getProviders ?? ((page) => page) const [_, setLsLastChecked] = useStoredState(LAST_CLIENT_JS_BUILD_ID_LS_KEY, null) + const setTreeAllData = useSetAtom(treeAllDataAtom); + const setExpandedNodes = useSetAtom(expandedNodesAtom); + // TODO: Delete this hook and implementation after 4/30/24 useClearEmptyDrafts() + useClearTreeAtoms(setTreeAllData, setExpandedNodes); + /* Whenever the app mounts for the first time, track the current time in local storage From e1b966ebc4fad919ee0cb9cc4271e64f0f590733 Mon Sep 17 00:00:00 2001 From: cuisailong Date: Tue, 26 Aug 2025 16:42:03 +0800 Subject: [PATCH 3/3] refactor(UI):Diff files data uses chunked requests --- .../apps/web/components/DiffView/FileDiff.tsx | 32 +++++++-------- .../web/components/DiffView/parsedDiffs.ts | 41 +++++++++---------- moon/apps/web/hooks/useGetMrFilesChanged.ts | 12 ------ moon/apps/web/hooks/useMrFilesChanged.ts | 18 ++++++++ moon/apps/web/pages/[org]/mr/[link]/index.tsx | 19 ++++++--- 5 files changed, 66 insertions(+), 56 deletions(-) delete mode 100644 moon/apps/web/hooks/useGetMrFilesChanged.ts create mode 100644 moon/apps/web/hooks/useMrFilesChanged.ts diff --git a/moon/apps/web/components/DiffView/FileDiff.tsx b/moon/apps/web/components/DiffView/FileDiff.tsx index e4456a31f..81f31fb99 100644 --- a/moon/apps/web/components/DiffView/FileDiff.tsx +++ b/moon/apps/web/components/DiffView/FileDiff.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { DiffFile, DiffModeEnum, DiffView } from '@git-diff-view/react' -import { CommonResultFilesChangedList } from '@gitmono/types/generated' +import { CommonResultFilesChangedPage, DiffItem } from '@gitmono/types/generated' import { ExpandIcon, SparklesIcon } from '@gitmono/ui/Icons' import { cn } from '@gitmono/ui/src/utils' @@ -9,8 +9,6 @@ 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') @@ -35,7 +33,7 @@ function generateParsedFiles(diffFiles: { path: string; lang: string; diff: stri stats: { additions: number; deletions: number } }[] { return diffFiles.map((file) => { - if (file.lang === 'binary') { + if (file.lang === 'binary' || file.diff === 'EMPTY_DIFF_MARKER\n') { return { file, instance: null, @@ -64,8 +62,8 @@ export default function FileDiff({ diffs, treeData }: { - diffs: string - treeData: CommonResultFilesChangedList['data'] + diffs: DiffItem[] + treeData: CommonResultFilesChangedPage['data'] }) { const diffFiles = useMemo(() => parsedDiffs(diffs), [diffs]) @@ -94,21 +92,23 @@ export default function FileDiff({ file: { path: string; lang: string; diff: string } instance: DiffFile | null }) => { - if (file.lang === 'binary' || instance === null) { + if (instance){ + return ( + + ) + }else if (file.lang === 'binary') { return
Binary file
} else if (file.diff === 'EMPTY_DIFF_MARKER\n') { return
No change
} - return ( - - ) + return null } return ( diff --git a/moon/apps/web/components/DiffView/parsedDiffs.ts b/moon/apps/web/components/DiffView/parsedDiffs.ts index 29578187a..208e576c0 100644 --- a/moon/apps/web/components/DiffView/parsedDiffs.ts +++ b/moon/apps/web/components/DiffView/parsedDiffs.ts @@ -1,3 +1,5 @@ +import { DiffItem } from "@gitmono/types/generated" + const extensionToLangMap: Record = { // Note that the key here is lowercase '.ts': 'typescript', @@ -21,15 +23,15 @@ const extensionToLangMap: Record = { '.html': 'html', '.vue': 'vue', '.toml': 'toml', - dockerfile: 'dockerfile', + 'dockerfile': 'dockerfile', '.dockerfile': 'dockerfile', 'license-mit': 'plaintext', - buck: 'plaintext', + 'buck': 'plaintext', '.gitignore': 'plaintext', '.env': 'plaintext', 'license-third-party': 'plaintext', 'license-apache': 'plaintext', - workspace: 'plaintext', + 'workspace': 'plaintext', '.buckroot': 'plaintext', '.buckconfig': 'plaintext' } @@ -50,18 +52,13 @@ function getLangFromPath(path: string): string { return 'binary' } -export function parsedDiffs(diffText: string): { path: string; lang: string; diff: string }[] { - if (!diffText) return [] - - const parts = diffText - .split(/(?=^diff --git )/gm) - .map((block) => block.trim()) - .filter(Boolean) +export function parsedDiffs(diffText: DiffItem[]): { path: string; lang: string; diff: string }[] { + if (diffText.length < 1) return [] - return parts.map((block) => { + return diffText.map((block) => { let path = '' - const diffGitMatch = block.match(/^diff --git a\/[^\s]+ b\/([^\s]+)/m) + const diffGitMatch = block.data.match(/^diff --git a\/[^\s]+ b\/([^\s]+)/m) if (diffGitMatch) { const originalPath = diffGitMatch[1]?.trim() @@ -78,20 +75,20 @@ export function parsedDiffs(diffText: string): { path: string; lang: string; dif return { path, lang: getLangFromPath(path), - diff: block + diff: block.data } } - let diffWithHeader = block - const plusMatch = block.match(/^\+\+\+ b\/([^\n\r]+)/m) - const hunkIndex = block.indexOf('@@') + let diffWithHeader = block.data + const headerRegex = /^(diff|index|---|\+\+\+|new file mode|@@)/; + const hunkContent = block.data + .split('\n') + .filter(line => !headerRegex.test(line.trim())); - if (!plusMatch) { - let prefix = `--- a/${path}\n+++ b/${path}\n` + const isEmptyHunk = hunkContent.every(line => line.trim() === ''); - diffWithHeader = hunkIndex >= 0 ? block.slice(0, hunkIndex) + prefix + block.slice(hunkIndex) : prefix + block - } else if (hunkIndex < 0) { - diffWithHeader = 'EMPTY_DIFF_MARKER' + if (!block.data.includes('@@') || isEmptyHunk) { + diffWithHeader = 'EMPTY_DIFF_MARKER'; } if (!diffWithHeader.endsWith('\n')) { @@ -99,7 +96,7 @@ export function parsedDiffs(diffText: string): { path: string; lang: string; dif } return { - path, + path: block.path, lang: getLangFromPath(path), diff: diffWithHeader } diff --git a/moon/apps/web/hooks/useGetMrFilesChanged.ts b/moon/apps/web/hooks/useGetMrFilesChanged.ts deleted file mode 100644 index d860f50f4..000000000 --- a/moon/apps/web/hooks/useGetMrFilesChanged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { legacyApiClient } from '@/utils/queryClient' -import type { RequestParams } from '@gitmono/types' -import type { GetApiMrFilesChangedData } from '@gitmono/types/generated' - -export function useGetMrFilesChanged(id: string, params?: RequestParams) { - return useQuery({ - queryKey: legacyApiClient.v1.getApiMrFilesChanged().requestKey(id), - queryFn: () => legacyApiClient.v1.getApiMrFilesChanged().request(id, params), - enabled: !!id, - }) -} \ No newline at end of file diff --git a/moon/apps/web/hooks/useMrFilesChanged.ts b/moon/apps/web/hooks/useMrFilesChanged.ts new file mode 100644 index 000000000..f9a989d85 --- /dev/null +++ b/moon/apps/web/hooks/useMrFilesChanged.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { PostApiMrFilesChangedData, PageParamsString, RequestParams } from '@gitmono/types' + +export function useMrFilesChanged( + link: string, + data: PageParamsString, + params?: RequestParams +) { + return useQuery({ + queryKey: [ + ...legacyApiClient.v1.postApiMrFilesChanged().requestKey(link), + data, + ], + queryFn: () => + legacyApiClient.v1.postApiMrFilesChanged().request(link, data, params) + }) +} \ No newline at end of file diff --git a/moon/apps/web/pages/[org]/mr/[link]/index.tsx b/moon/apps/web/pages/[org]/mr/[link]/index.tsx index d909fcfec..a6fcd5735 100644 --- a/moon/apps/web/pages/[org]/mr/[link]/index.tsx +++ b/moon/apps/web/pages/[org]/mr/[link]/index.tsx @@ -13,6 +13,7 @@ import { PicturePlusIcon } from '@gitmono/ui/Icons' import { cn } from '@gitmono/ui/utils' import { EMPTY_HTML } from '@/atoms/markdown' +import FileDiff from '@/components/DiffView/FileDiff' import { BadgeItem } from '@/components/Issues/IssueNewPage' import { splitFun, @@ -38,7 +39,7 @@ import { useScope } from '@/contexts/scope' import { usePostMRAssignees } from '@/hooks/issues/usePostMRAssignees' import { usePostMrTitle } from '@/hooks/MR/usePostMrTitle' import { useGetMrDetail } from '@/hooks/useGetMrDetail' -import { useGetMrFilesChanged } from '@/hooks/useGetMrFilesChanged' +import { useMrFilesChanged } from '@/hooks/useMrFilesChanged' import { usePostMrClose } from '@/hooks/usePostMrClose' import { usePostMrComment } from '@/hooks/usePostMrComment' import { usePostMRLabels } from '@/hooks/usePostMRLabels' @@ -72,12 +73,19 @@ const MRDetailPage: PageWithLayout = () => { const [editTitle, setEditTitle] = useState(mrDetail?.title) const [loading, setLoading] = useState(false) const Checks = dynamic(() => import('@/components/MrView/components/Checks')) + const [page, _setPage] = useState(1) if (mrDetail && typeof mrDetail.status === 'string') { mrDetail.status = mrDetail.status.toLowerCase() } - const { data: MrFilesChangedData, isLoading: fileChgIsLoading } = useGetMrFilesChanged(id) + const { data: MrFilesChangedData, isLoading: fileChgIsLoading } = useMrFilesChanged(id, { + additional: 'string', + pagination: { + page, + per_page: 10, + }, + }) const { mutate: modifyTitle } = usePostMrTitle() const { mutate: approveMr, isPending: mrMergeIsPending } = usePostMrMerge(id) @@ -395,10 +403,9 @@ const MRDetailPage: PageWithLayout = () => {
- ) : MrFilesChangedData?.data?.content ? ( - // -
files
- ) : ( + ) : MrFilesChangedData?.data?.page.items ? ( + + ) : (
No files changed
)}