diff --git a/webapp/src/dogma/common/components/Breadcrumbs.tsx b/webapp/src/dogma/common/components/Breadcrumbs.tsx index a2c84ff566..13f8af2a51 100644 --- a/webapp/src/dogma/common/components/Breadcrumbs.tsx +++ b/webapp/src/dogma/common/components/Breadcrumbs.tsx @@ -5,18 +5,22 @@ import NextLink from 'next/link'; interface BreadcrumbsProps { path: string; omitIndexList?: number[]; + omitQueryList?: number[]; unlinkedList?: number[]; replaces?: { [key: number]: string }; suffixes?: { [key: number]: string }; + query?: string; } export const Breadcrumbs = ({ path, omitIndexList = [], + omitQueryList = [], unlinkedList = [], replaces = {}, // /project/projectName/repos/repoName -> /project/projectName/repos/repoName/tree/head suffixes = {}, + query = '', }: BreadcrumbsProps) => { const asPathNestedRoutes = path // If the path belongs to a file, the top level should be a directory @@ -30,18 +34,24 @@ export const Breadcrumbs = ({ return ( } mb={8} fontWeight="medium" fontSize="2xl"> {asPathNestedRoutes.map((page, i) => { - prefixes.push(page); const item = replaces[i] || page; + prefixes.push(item); if (omitIndexList.includes(i)) { return null; } + let query0; + if (omitQueryList.includes(i) || omitQueryList.includes(i - asPathNestedRoutes.length)) { + query0 = ''; + } else { + query0 = query ? `?${query}` : ''; + } return ( {!unlinkedList.includes(i) && i < asPathNestedRoutes.length - 1 ? ( {decodeURI(item)} diff --git a/webapp/src/dogma/common/components/CompareButton.tsx b/webapp/src/dogma/common/components/CompareButton.tsx index e88bd5f9b6..8911c2e970 100644 --- a/webapp/src/dogma/common/components/CompareButton.tsx +++ b/webapp/src/dogma/common/components/CompareButton.tsx @@ -51,7 +51,7 @@ const CompareButton = ({ projectName, repoName, headRevision }: CompareButtonPro Compare - +
@@ -59,7 +59,7 @@ const CompareButton = ({ projectName, repoName, headRevision }: CompareButtonPro diff --git a/webapp/src/dogma/common/components/Deferred.tsx b/webapp/src/dogma/common/components/Deferred.tsx index 61d62d0500..06ddb9dcf5 100644 --- a/webapp/src/dogma/common/components/Deferred.tsx +++ b/webapp/src/dogma/common/components/Deferred.tsx @@ -21,6 +21,7 @@ import { Loading } from './Loading'; interface LoadingProps { isLoading: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any; children: () => ReactNode; } diff --git a/webapp/src/dogma/common/components/editor/FileEditor.tsx b/webapp/src/dogma/common/components/editor/FileEditor.tsx index ef972508b5..b8d215cc8f 100644 --- a/webapp/src/dogma/common/components/editor/FileEditor.tsx +++ b/webapp/src/dogma/common/components/editor/FileEditor.tsx @@ -28,7 +28,7 @@ import { newNotification } from 'dogma/features/notification/notificationSlice'; import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; import Router from 'next/router'; import Link from 'next/link'; -import { FaCodeCommit } from 'react-icons/fa6'; +import { FaHistory } from 'react-icons/fa'; export type FileEditorProps = { projectName: string; @@ -37,7 +37,7 @@ export type FileEditorProps = { originalContent: string; path: string; name: string; - commitRevision: number; + revision: string | number; }; // Map file extension to language identifier @@ -66,7 +66,7 @@ const FileEditor = ({ originalContent, path, name, - commitRevision, + revision, }: FileEditorProps) => { const dispatch = useAppDispatch(); const language = extensionToLanguageMap[extension] || extension; @@ -117,12 +117,12 @@ const FileEditor = ({ + {isDirectory ? ( + + ) : null} ({ - pageIndex: 0, - pageSize: 10, - }); - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize], - ); - - const targetPath = isDirectory ? `${filePath}/**` : filePath; - const { data, isLoading, error } = useGetHistoryQuery({ - projectName, - repoName, - filePath: targetPath, - // revision starts from -1, for example for pageSize=20 - // The first page /projects/{projectName}/repos/{repoName}/commits/-1?to=-20 - // The second page /projects/{projectName}/repos/{repoName}/commits/-20?to=-40 - revision: -pageIndex * pageSize - 1, - to: Math.max(-totalRevision, -(pageIndex + 1) * pageSize), - }); - return ( - - {() => ( - - )} - + ); }; diff --git a/webapp/src/dogma/features/services/ErrorMessageParser.ts b/webapp/src/dogma/features/services/ErrorMessageParser.ts index c93f50822b..e7ff53dbb5 100644 --- a/webapp/src/dogma/features/services/ErrorMessageParser.ts +++ b/webapp/src/dogma/features/services/ErrorMessageParser.ts @@ -1,5 +1,5 @@ class ErrorMessageParser { - // eslint-disable-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any static parse(object: any): string { if (object.response && object.response.data.message) { return object.response.data.message; diff --git a/webapp/src/dogma/util/path-util.ts b/webapp/src/dogma/util/path-util.ts index ac14c9274e..b3ea72d56c 100644 --- a/webapp/src/dogma/util/path-util.ts +++ b/webapp/src/dogma/util/path-util.ts @@ -30,11 +30,16 @@ export type UrlAndSegment = { url: string; }; -export function makeTraversalFileLinks(projectName: string, repoName: string, path: string): UrlAndSegment[] { +export function makeTraversalFileLinks( + projectName: string, + repoName: string, + infix: string, + path: string, +): UrlAndSegment[] { const links: UrlAndSegment[] = []; const segments = path.split('/'); for (let i = 1; i < segments.length; i++) { - const url = `/app/projects/${projectName}/repos/${repoName}/tree/head/${segments.slice(1, i + 1).join('/')}`; + const url = `/app/projects/${projectName}/repos/${repoName}/${infix}/${segments.slice(1, i + 1).join('/')}`; links.push({ segment: segments[i], url }); } return links; diff --git a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/commit/[revision]/[[...path]]/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/commit/[revision]/[[...path]]/index.tsx index 82a4d76c5e..d079d11ab6 100644 --- a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/commit/[revision]/[[...path]]/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/commit/[revision]/[[...path]]/index.tsx @@ -83,7 +83,7 @@ const CommitViewPage = () => { data: oldData, isLoading: isOldLoading, error: oldError, - }: any = useGetFilesQuery( + } = useGetFilesQuery( { projectName, repoName, @@ -114,7 +114,8 @@ const CommitViewPage = () => { return ; } // 404 Not Found is returned if the file does not exist in the old revision. - const oldError0 = oldError && oldError.status != 404 ? oldError : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const oldError0 = oldError && (oldError as any).status != 404 ? oldError : null; return ( { const router = useRouter(); const repoName = router.query.repoName ? (router.query.repoName as string) : ''; const projectName = router.query.projectName ? (router.query.projectName as string) : ''; - const filePath = router.query.path ? `/${Array.from(router.query.path).join('/')}` : ''; + const filePath = router.query.path ? toFilePath(router.query.path) : ''; + const from = parseInt(router.query.from as string) || -1; const directoryPath = router.asPath; + let type = router.query.type as string; + if (!type && !filePath) { + type = 'tree'; + } + const targetPath = type == 'tree' ? `${filePath}/**` : filePath; const { data, isLoading, error } = useGetNormalisedRevisionQuery({ projectName, repoName, revision: -1 }); + const headRevision = data?.revision || -1; + let fromRevision = from; + if (from <= -1) { + fromRevision = headRevision + from + 1; + } + let baseRevision = parseInt(router.query.base as string) || fromRevision - DEFAULT_PAGE_SIZE + 1; + const pageSize = fromRevision - baseRevision + 1; + if (baseRevision < 1) { + baseRevision = 1; + } + + const fromPageCount = Math.ceil(fromRevision / pageSize); + let headPageCount; + if (fromRevision > headRevision) { + headPageCount = 0; + } else { + headPageCount = Math.ceil((headRevision - fromRevision) / pageSize); + } + + const pageCount = headPageCount + fromPageCount; + const pageIndex = headPageCount; + const pagination = { pageIndex, pageSize }; + function setPagination(updater: (old: PaginationState) => PaginationState): void { + if (headRevision <= 0) { + return; + } + + const newPagination = updater({ pageIndex, pageSize }); + let newPageSize = newPagination.pageSize; + if (newPageSize == -1) { + newPageSize = pageSize; + } + const newPageIndex = newPagination.pageIndex; + + if (newPageIndex != pageIndex || newPageSize != pageSize) { + const from = fromRevision - (newPageIndex - pageIndex) * newPageSize; + const base = from - newPageSize + 1; + const query: { [key: string]: string | number } = { from, base }; + if (type) { + query['type'] = type; + } + router.push({ + pathname: `/app/projects/${projectName}/repos/${repoName}/commits/${filePath}`, + query, + }); + return; + } + } + + const historyFrom = Math.min(fromRevision, headRevision); + const historyTo = Math.max(baseRevision, 1); + const { + data: historyData, + isLoading: isHistoryLoading, + error: historyError, + } = useGetHistoryQuery( + { + projectName, + repoName, + filePath: targetPath, + revision: historyFrom, + to: historyTo, + maxCommits: historyFrom - historyTo + 1, + }, + { + skip: headRevision === -1 || historyFrom < historyTo, + }, + ); + + const onEmptyData = ( + + + + + No changes detected for {filePath} in range [{baseRevision}..{fromRevision}] + + + + + ); + + const urlAndSegments = makeTraversalFileLinks(projectName, repoName, 'commits', filePath); return ( - - {() => ( - - - - - {filePath ? ( - - - - - {makeTraversalFileLinks(projectName, repoName, filePath).map(({ segment, url }) => { - return ( - - {'/'} - {segment} - - ); - })} -  commits - - ) : ( - - - - - - {repoName} - - {filePath} commits - - )} - - - - - )} + + {() => { + const omitQueryList = [1, 2, 4, 5]; + if (type !== 'tree' && router.query.from) { + // Omit the 'type=tree' query parameter when the type is a file. + omitQueryList.push(-2); + } + return ( + + + + + {filePath ? ( + + + {type === 'tree' ? : } + + {urlAndSegments.map(({ segment, url }, index) => { + let query = ''; + if (type === 'tree' || index < urlAndSegments.length - 1) { + query = '?type=tree'; + } + const targetUrl = url + query; + return ( + + {'/'} + {segment} + + ); + })} +  commits + + ) : ( + + + + + + + {repoName} + + + {filePath} commits + + )} + + + + + ); + }} ); }; diff --git a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/compare/[revision]/base/[baseRevision]/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/compare/[revision]/base/[baseRevision]/index.tsx index ebbcdb5ad3..37ec2fb017 100644 --- a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/compare/[revision]/base/[baseRevision]/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/compare/[revision]/base/[baseRevision]/index.tsx @@ -14,7 +14,7 @@ * under the License. */ -import { useRouter } from 'next/router'; +import Router, { useRouter } from 'next/router'; import { useGetFilesQuery } from 'dogma/features/api/apiSlice'; import { Deferred } from 'dogma/common/components/Deferred'; import { @@ -30,13 +30,17 @@ import { } from '@chakra-ui/react'; import React, { useState } from 'react'; import { Breadcrumbs } from 'dogma/common/components/Breadcrumbs'; -import { ChakraLink } from 'dogma/common/components/ChakraLink'; import FourOhFour from 'pages/404'; -import { toFilePath } from 'dogma/util/path-util'; import { FaArrowLeftLong, FaCodeCompare } from 'react-icons/fa6'; import { GoDiff } from 'react-icons/go'; import DiffView, { DiffMode } from 'dogma/common/components/DiffView'; import DiffModeButton from 'dogma/common/components/DiffModeButton'; +import { useForm } from 'react-hook-form'; + +type FormData = { + baseRevision: number; + headRevision: number; +}; const ChangesViewPage = () => { const router = useRouter(); @@ -44,10 +48,7 @@ const ChangesViewPage = () => { const repoName = router.query.repoName as string; const headRevision = parseInt(router.query.revision as string); const baseRevision = parseInt(router.query.baseRevision as string); - let filePath = toFilePath(router.query.path); - if (filePath == '/') { - filePath = '/**'; - } + const filePath = '/**'; const { data: newData, @@ -64,7 +65,7 @@ const ChangesViewPage = () => { data: oldData, isLoading: isOldLoading, error: oldError, - }: any = useGetFilesQuery({ + } = useGetFilesQuery({ projectName, repoName, revision: baseRevision, @@ -74,8 +75,22 @@ const ChangesViewPage = () => { const { colorMode } = useColorMode(); const [diffMode, setDiffMode] = useState('Split'); - const [headRev, setHeadRev] = useState(headRevision); - const [baseRev, setBaseRev] = useState(baseRevision); + + const { + register, + handleSubmit, + formState: { isDirty }, + } = useForm({ + defaultValues: { + baseRevision: baseRevision, + headRevision: headRevision, + }, + }); + const onSubmit = (data: FormData) => { + Router.push( + `/app/projects/${projectName}/repos/${repoName}/compare/${data.headRevision}/base/${data.baseRevision}`, + ); + }; if (headRevision <= 1) { return ; @@ -97,50 +112,49 @@ const ChangesViewPage = () => { - - - - - Base - - + + + + + Base + + + + + + + + + + + From + + + + + + - - + leftIcon={} + colorScheme="green" + isDisabled={!isDirty} + > + Compare + + + + setDiffMode(value as DiffMode)} /> @@ -160,5 +174,4 @@ const ChangesViewPage = () => { ); }; - export default ChangesViewPage; diff --git a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/files/[revision]/[[...path]]/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/files/[revision]/[[...path]]/index.tsx index 9a1f648dde..1adbb62b40 100644 --- a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/files/[revision]/[[...path]]/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/files/[revision]/[[...path]]/index.tsx @@ -1,6 +1,6 @@ import { InfoIcon } from '@chakra-ui/icons'; import { Box, Flex, Heading, HStack, Tag, Tooltip } from '@chakra-ui/react'; -import { useGetFileContentQuery, useGetHistoryQuery } from 'dogma/features/api/apiSlice'; +import { useGetFileContentQuery } from 'dogma/features/api/apiSlice'; import { useRouter } from 'next/router'; import FileEditor from 'dogma/common/components/editor/FileEditor'; import { Breadcrumbs } from 'dogma/common/components/Breadcrumbs'; @@ -27,20 +27,8 @@ const FileContentPage = () => { refetchOnMountOrArgChange: true, }, ); - const { - data: historyData, - isLoading: isHistoryLoading, - error: historyError, - } = useGetHistoryQuery({ - projectName, - repoName, - revision, - to: 1, - filePath, - maxCommits: 1, - }); return ( - + {() => { return ( @@ -53,7 +41,7 @@ const FileContentPage = () => { - + {fileName} @@ -72,7 +60,7 @@ const FileContentPage = () => { originalContent={data.content} path={data.path} name={fileName} - commitRevision={historyData[0].revision} + revision={revision} /> ); diff --git a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/tree/[revision]/[[...path]]/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/tree/[revision]/[[...path]]/index.tsx index 9fbc12797d..d49cafd0dd 100644 --- a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/tree/[revision]/[[...path]]/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/tree/[revision]/[[...path]]/index.tsx @@ -18,14 +18,14 @@ import { GoRepo } from 'react-icons/go'; import { ChakraLink } from 'dogma/common/components/ChakraLink'; import { WithProjectRole } from 'dogma/features/auth/ProjectRole'; import { FaHistory } from 'react-icons/fa'; -import { makeTraversalFileLinks } from 'dogma/util/path-util'; +import { makeTraversalFileLinks, toFilePath } from 'dogma/util/path-util'; const RepositoryDetailPage = () => { const router = useRouter(); const repoName = router.query.repoName ? (router.query.repoName as string) : ''; const projectName = router.query.projectName ? (router.query.projectName as string) : ''; const revision = router.query.revision ? (router.query.revision as string) : 'head'; - const filePath = router.query.path ? `/${Array.from(router.query.path).join('/')}` : ''; + const filePath = router.query.path ? toFilePath(router.query.path) : ''; const directoryPath = router.asPath; const dispatch = useAppDispatch(); @@ -108,14 +108,16 @@ cat ${project}/${repo}${path}`; - {makeTraversalFileLinks(projectName, repoName, filePath).map(({ segment, url }) => { - return ( - - {'/'} - {segment} - - ); - })} + {makeTraversalFileLinks(projectName, repoName, 'tree/head', filePath).map( + ({ segment, url }) => { + return ( + + {'/'} + {segment} + + ); + }, + )} ) : ( @@ -138,7 +140,7 @@ cat ${project}/${repo}${path}`;