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<thomas.zerr@cloudogu.com>
Co-authored-by: Thomas Zerr<thomas.zerr@cloudogu.com>
Pushed-by: Thomas Zerr<thomas.zerr@cloudogu.com>
This commit is contained in:
Thomas Zerr
2024-03-06 12:10:56 +01:00
parent a8d250c13f
commit 1a6e202384
8 changed files with 142 additions and 26 deletions

View File

@@ -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.

View File

@@ -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");
});
});

View File

@@ -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)}` : "";
};

View File

@@ -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<Props> = ({ baseUrl, revision }) => {
const FileSearchButton: FC<Props> = ({ baseUrl, revision, currentSource, repository }) => {
const [t] = useTranslation("repos");
const currentSourcePath =
repository.type === "svn"
? urls.createPrevSourcePathQuery(`${revision}/${currentSource.path}`)
: urls.createPrevSourcePathQuery(currentSource.path);
return (
<Link
to={`${baseUrl}/search/${encodeURIComponent(revision)}`}
to={`${baseUrl}/search/${encodeURIComponent(revision)}/?${currentSourcePath}`}
aria-label={t("fileSearch.button.title")}
data-testid="file_search_button"
>

View File

@@ -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<Props> = ({ repository, baseUrl, branches, selectedBranch }
const { isLoading, error, data } = usePaths(repository, revision);
const [result, setResult] = useState<string[]>([]);
const query = urls.getQueryStringFromLocation(location) || "";
const prevSourcePath = urls.getPrevSourcePathFromLocation(location) || "";
const [t] = useTranslation("repos");
const [firstSelectedBranch, setBranchChanged] = useState<string | undefined>(selectedBranch);
useEffect(() => {
if (query.length > 1 && data) {
setResult(filepathSearch(data.paths, query));
@@ -74,20 +78,35 @@ const FileSearch: FC<Props> = ({ 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<Props> = ({ repository, baseUrl, branches, selectedBranch }
"pb-0"
)}
>
<HomeLink className={classNames("mr-3", "pr-3")} to={contentBaseUrl}>
<HomeIcon title={t("fileSearch.home")} name="home" color="inherit" />
<HomeLink
className={classNames("mr-3", "pr-3")}
to={firstSelectedBranch !== selectedBranch ? contentBaseUrl : () => evaluateSwitchViewLink("sources")}
>
<HomeIcon
title={t("fileSearch.home")}
name={firstSelectedBranch !== selectedBranch ? "home" : "arrow-left"}
color="inherit"
/>
</HomeLink>
<FilterInput
className="is-full-width pr-2"

View File

@@ -23,13 +23,13 @@
*/
import React, { FC } from "react";
import { Route, useRouteMatch, useHistory } from "react-router-dom";
import { Route, useRouteMatch, useHistory, useLocation } from "react-router-dom";
import { Repository, Branch } from "@scm-manager/ui-types";
import CodeActionBar from "../codeSection/components/CodeActionBar";
import { urls } from "@scm-manager/ui-components";
import Changesets from "./Changesets";
import { RepositoryRevisionContextProvider } from "@scm-manager/ui-api";
import { encodePart } from "../sources/components/content/FileLink";
import { encodeFilePath, encodePart } from "../sources/components/content/FileLink";
type Props = {
repository: Repository;
@@ -41,29 +41,37 @@ type Props = {
const ChangesetRoot: FC<Props> = ({ 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<Props> = ({ repository, baseUrl, branches, selectedBranc
switchViewLink={evaluateSwitchViewLink()}
/>
<Route path={`${url}/:page?`}>
<Changesets repository={repository} branch={branches?.filter(b => b.name === selectedBranch)[0]} url={url} />
<Changesets repository={repository} branch={branches?.filter((b) => b.name === selectedBranch)[0]} url={url} />
</Route>
</RepositoryRevisionContextProvider>
);

View File

@@ -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,

View File

@@ -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<Params>();
return {
revision: revision ? decodeURIComponent(revision) : undefined,
path: path || ""
path: path || "",
};
};
@@ -70,12 +70,18 @@ const Sources: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
const buttons = [];
if (repository._links.paths) {
buttons.push(<FileSearchButton baseUrl={baseUrl} revision={revision || file.revision} />);
buttons.push(
<FileSearchButton
baseUrl={baseUrl}
revision={revision || file.revision}
currentSource={file}
repository={repository}
/>
);
}
return (
@@ -127,8 +147,8 @@ const Sources: FC<Props> = ({ 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}
/>