From 1a6e202384109ade9c4f92d4c0c8ab693dbcd2b4 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 6 Mar 2024 12:10:56 +0100 Subject: [PATCH] Remember Path when switching to commits or file search The path gets remembered by a query parameter. Using React state to remember the current path has two downsides. First you would need to wrap the components in a context and store the current state there. Second the remembered state gets lost by refreshing the state. By using a query parameter those two downside get avoided. Committed-by: Thomas Zerr Co-authored-by: Thomas Zerr Pushed-by: Thomas Zerr --- gradle/remember_path_when_switching_view.yaml | 2 + scm-ui/ui-api/src/urls.test.ts | 31 ++++++++++++++ scm-ui/ui-api/src/urls.ts | 15 +++++++ .../components/FileSearchButton.tsx | 13 +++++- .../codeSection/containers/FileSearch.tsx | 40 +++++++++++++++---- .../src/repos/containers/ChangesetsRoot.tsx | 24 +++++++---- .../sources/components/content/FileLink.tsx | 5 +++ .../src/repos/sources/containers/Sources.tsx | 38 +++++++++++++----- 8 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 gradle/remember_path_when_switching_view.yaml diff --git a/gradle/remember_path_when_switching_view.yaml b/gradle/remember_path_when_switching_view.yaml new file mode 100644 index 0000000000..fb512ea7f8 --- /dev/null +++ b/gradle/remember_path_when_switching_view.yaml @@ -0,0 +1,2 @@ +- type: changed + description: The currently opened path is now remembered, when switching back and forth between Source, Commit and File Search view. diff --git a/scm-ui/ui-api/src/urls.test.ts b/scm-ui/ui-api/src/urls.test.ts index 9fc043f8bf..33a79ca795 100644 --- a/scm-ui/ui-api/src/urls.test.ts +++ b/scm-ui/ui-api/src/urls.test.ts @@ -24,7 +24,9 @@ import { concat, + createPrevSourcePathQuery, getNamespaceAndPageFromMatch, + getPrevSourcePathFromLocation, getQueryStringFromLocation, getValueStringFromLocationByKey, withEndingSlash, @@ -143,3 +145,32 @@ describe("tests for getValueStringFromLocationByKey", () => { expect(getValueStringFromLocationByKey(location, "namespace")).toBeUndefined(); }); }); + +describe("tests for getPrevSourcePathFromLocation", () => { + it("should return the value string", () => { + const location = { search: "?prevSourcePath=src%2Fsub%25%2Ffile%26%252F.abc" }; + expect(getPrevSourcePathFromLocation(location)).toBe("src/sub%/file&%2F.abc"); + }); + + it("should return undefined, because query parameter is missing", () => { + const location = { search: "?q=abc" }; + expect(getPrevSourcePathFromLocation(location)).toBeUndefined(); + }); + + it("should return undefined, because query parameter is missing", () => { + const location = { search: "?q=abc" }; + expect(getPrevSourcePathFromLocation(location)).toBeUndefined(); + }); +}); + +describe("tests for createPrevSourcePathQuery", () => { + it("should return empty string if file path is empty", () => { + const encodedPath = createPrevSourcePathQuery(""); + expect(encodedPath).toBe(""); + }); + + it("should return the encoded path as query parameter", () => { + const encodedPath = createPrevSourcePathQuery("src/sub%/file&%2F.abc"); + expect(encodedPath).toBe("prevSourcePath=src%2Fsub%25%2Ffile%26%252F.abc"); + }); +}); diff --git a/scm-ui/ui-api/src/urls.ts b/scm-ui/ui-api/src/urls.ts index 485d0a1aa9..6d4ecce0f8 100644 --- a/scm-ui/ui-api/src/urls.ts +++ b/scm-ui/ui-api/src/urls.ts @@ -125,3 +125,18 @@ export function escapeUrlForRoute(url: string) { export function unescapeUrlForRoute(url: string) { return url.replace(/\\/g, ""); } + +const prevSourcePathQueryName = "prevSourcePath"; + +export function getPrevSourcePathFromLocation(location: { search?: string }): string | undefined { + if (location.search) { + const prevSourcePath = queryString.parse(location.search)[prevSourcePathQueryName]; + if (prevSourcePath && !Array.isArray(prevSourcePath)) { + return prevSourcePath; + } + } +} + +export const createPrevSourcePathQuery = (filePath: string) => { + return filePath ? `${prevSourcePathQueryName}=${encodeURIComponent(filePath)}` : ""; +}; diff --git a/scm-ui/ui-webapp/src/repos/codeSection/components/FileSearchButton.tsx b/scm-ui/ui-webapp/src/repos/codeSection/components/FileSearchButton.tsx index 53ade6fc2f..02a4b22796 100644 --- a/scm-ui/ui-webapp/src/repos/codeSection/components/FileSearchButton.tsx +++ b/scm-ui/ui-webapp/src/repos/codeSection/components/FileSearchButton.tsx @@ -23,24 +23,33 @@ */ import React, { FC } from "react"; import { useTranslation } from "react-i18next"; +import { File, Repository } from "@scm-manager/ui-types"; import { Link } from "react-router-dom"; import { Icon } from "@scm-manager/ui-components"; import styled from "styled-components"; +import { urls } from "@scm-manager/ui-api"; type Props = { + repository: Repository; revision: string; baseUrl: string; + currentSource: File; }; const SearchIcon = styled(Icon)` line-height: 1.5rem; `; -const FileSearchButton: FC = ({ baseUrl, revision }) => { +const FileSearchButton: FC = ({ baseUrl, revision, currentSource, repository }) => { const [t] = useTranslation("repos"); + const currentSourcePath = + repository.type === "svn" + ? urls.createPrevSourcePathQuery(`${revision}/${currentSource.path}`) + : urls.createPrevSourcePathQuery(currentSource.path); + return ( diff --git a/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx b/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx index 9a763357d4..a4117f745c 100644 --- a/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx +++ b/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx @@ -32,6 +32,7 @@ import { createA11yId, ErrorNotification, FilterInput, Help, Icon, Loading } fro import CodeActionBar from "../components/CodeActionBar"; import FileSearchResults from "../components/FileSearchResults"; import { filepathSearch } from "../utils/filepathSearch"; +import { encodeFilePath } from "../../sources/components/content/FileLink"; type Props = { repository: Repository; @@ -64,7 +65,10 @@ const FileSearch: FC = ({ repository, baseUrl, branches, selectedBranch } const { isLoading, error, data } = usePaths(repository, revision); const [result, setResult] = useState([]); const query = urls.getQueryStringFromLocation(location) || ""; + const prevSourcePath = urls.getPrevSourcePathFromLocation(location) || ""; const [t] = useTranslation("repos"); + const [firstSelectedBranch, setBranchChanged] = useState(selectedBranch); + useEffect(() => { if (query.length > 1 && data) { setResult(filepathSearch(data.paths, query)); @@ -74,20 +78,35 @@ const FileSearch: FC = ({ repository, baseUrl, branches, selectedBranch } }, [data, query]); const search = (query: string) => { - history.push(`${location.pathname}?q=${encodeURIComponent(query)}`); + const prevSourceQuery = urls.createPrevSourcePathQuery(prevSourcePath); + history.push(`${location.pathname}?q=${encodeURIComponent(query)}${prevSourceQuery ? `&${prevSourceQuery}` : ""}`); }; const onSelectBranch = (branch?: Branch) => { if (branch) { - history.push(`${baseUrl}/search/${encodeURIComponent(branch.name)}?q=${query}`); + const prevSourceQuery = urls.createPrevSourcePathQuery(prevSourcePath); + history.push( + `${baseUrl}/search/${encodeURIComponent(branch.name)}?q=${query}${prevSourceQuery ? `&${prevSourceQuery}` : ""}` + ); } }; const evaluateSwitchViewLink = (type: string) => { - if (type === "sources") { - return `${baseUrl}/sources/${revision}/`; + if (type === "sources" && repository.type !== "svn") { + return `${baseUrl}/sources/${revision}/${encodeFilePath(prevSourcePath)}`; } - return `${baseUrl}/changesets/${revision}/`; + + if (type === "sources" && repository.type === "svn") { + return `${baseUrl}/sources/${encodeFilePath(prevSourcePath)}`; + } + + if (repository.type !== "svn") { + return `${baseUrl}/branch/${revision}/changesets/${ + prevSourcePath ? `?${urls.createPrevSourcePathQuery(prevSourcePath)}` : "" + }`; + } + + return `${baseUrl}/changesets/${prevSourcePath ? `?${urls.createPrevSourcePathQuery(prevSourcePath)}` : ""}`; }; const contentBaseUrl = `${baseUrl}/sources/${revision}/`; @@ -113,8 +132,15 @@ const FileSearch: FC = ({ repository, baseUrl, branches, selectedBranch } "pb-0" )} > - - + evaluateSwitchViewLink("sources")} + > + = ({ repository, baseUrl, branches, selectedBranch }) => { const match = useRouteMatch(); const history = useHistory(); + const location = useLocation(); if (!repository) { return null; } const url = urls.stripEndingSlash(urls.escapeUrlForRoute(match.url)); - const defaultBranch = branches?.find(b => b.defaultBranch === true); + const defaultBranch = branches?.find((b) => b.defaultBranch === true); const isBranchAvailable = () => { - return branches?.filter(b => b.name === selectedBranch).length === 0; + return branches?.filter((b) => b.name === selectedBranch).length === 0; }; const evaluateSwitchViewLink = () => { + const sourcePath = encodeFilePath(urls.getPrevSourcePathFromLocation(location) || ""); + if (selectedBranch) { - return `${baseUrl}/sources/${encodePart(selectedBranch)}/`; + return `${baseUrl}/sources/${encodePart(selectedBranch)}/${sourcePath}`; } + + if (repository.type === "svn") { + return `${baseUrl}/sources/${sourcePath !== "/" ? sourcePath : ""}`; + } + return `${baseUrl}/sources/`; }; const onSelectBranch = (branch?: Branch) => { if (branch) { - history.push(`${baseUrl}/branch/${encodePart(branch.name)}/changesets/`); + history.push(`${baseUrl}/branch/${encodePart(branch.name)}/changesets/${location.search}`); } else { - history.push(`${baseUrl}/changesets/`); + history.push(`${baseUrl}/changesets/${location.search}`); } }; @@ -76,7 +84,7 @@ const ChangesetRoot: FC = ({ repository, baseUrl, branches, selectedBranc switchViewLink={evaluateSwitchViewLink()} /> - b.name === selectedBranch)[0]} url={url} /> + b.name === selectedBranch)[0]} url={url} /> ); diff --git a/scm-ui/ui-webapp/src/repos/sources/components/content/FileLink.tsx b/scm-ui/ui-webapp/src/repos/sources/components/content/FileLink.tsx index 6b1eb124df..f6e32c4cbc 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/content/FileLink.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/content/FileLink.tsx @@ -62,6 +62,11 @@ export const encodePart = (part: string) => { return encodeURIComponent(part); }; +export const encodeFilePath = (filePath: string) => { + const encodedUri = encodePart(filePath); + return encodedUri.replace(/%2F/g, "/"); +}; + export const createRelativeLink = ( repositoryUrl: string, contextPath: string, diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx index 0e26258c89..17f31bc62e 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx @@ -24,7 +24,7 @@ import React, { FC, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useHistory, useLocation, useParams } from "react-router-dom"; -import { RepositoryRevisionContextProvider, useSources } from "@scm-manager/ui-api"; +import { RepositoryRevisionContextProvider, urls, useSources } from "@scm-manager/ui-api"; import { Branch, Repository } from "@scm-manager/ui-types"; import { Breadcrumb, ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; import FileTree from "../components/FileTree"; @@ -52,7 +52,7 @@ const useUrlParams = () => { const { revision, path } = useParams(); return { revision: revision ? decodeURIComponent(revision) : undefined, - path: path || "" + path: path || "", }; }; @@ -70,12 +70,18 @@ const Sources: FC = ({ repository, branches, selectedBranch, baseUrl }) = ); } }, [branches, selectedBranch, history, baseUrl]); - const { isLoading, error, data: file, isFetchingNextPage, fetchNextPage } = useSources(repository, { + const { + isLoading, + error, + data: file, + isFetchingNextPage, + fetchNextPage, + } = useSources(repository, { revision, path, // we have to wait until a branch is selected, // expect if we have no branches (svn) - enabled: !branches || !!selectedBranch + enabled: !branches || !!selectedBranch, }); if (error) { @@ -106,9 +112,16 @@ const Sources: FC = ({ repository, branches, selectedBranch, baseUrl }) = }; const evaluateSwitchViewLink = () => { - if (branches && selectedBranch && branches?.filter(b => b.name === selectedBranch).length !== 0) { - return `${baseUrl}/branch/${encodeURIComponent(selectedBranch)}/changesets/`; + if (branches && selectedBranch && branches?.filter((b) => b.name === selectedBranch).length !== 0) { + return `${baseUrl}/branch/${encodeURIComponent(selectedBranch)}/changesets/?${urls.createPrevSourcePathQuery( + file.path + )}`; } + + if (repository.type === "svn") { + return `${baseUrl}/changesets/?${urls.createPrevSourcePathQuery(`${file.revision}/${file.path}`)}`; + } + return `${baseUrl}/changesets/`; }; @@ -117,7 +130,14 @@ const Sources: FC = ({ repository, branches, selectedBranch, baseUrl }) = const buttons = []; if (repository._links.paths) { - buttons.push(); + buttons.push( + + ); } return ( @@ -127,8 +147,8 @@ const Sources: FC = ({ repository, branches, selectedBranch, baseUrl }) = revision={revision || file.revision} path={path || ""} baseUrl={baseUrl + "/sources"} - branch={branches?.filter(b => b.name === selectedBranch)[0]} - defaultBranch={branches?.filter(b => b.defaultBranch === true)[0]} + branch={branches?.filter((b) => b.name === selectedBranch)[0]} + defaultBranch={branches?.filter((b) => b.defaultBranch === true)[0]} sources={file} permalink={permalink} />