mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 06:25:45 +01:00
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:
2
gradle/remember_path_when_switching_view.yaml
Normal file
2
gradle/remember_path_when_switching_view.yaml
Normal 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.
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
concat,
|
concat,
|
||||||
|
createPrevSourcePathQuery,
|
||||||
getNamespaceAndPageFromMatch,
|
getNamespaceAndPageFromMatch,
|
||||||
|
getPrevSourcePathFromLocation,
|
||||||
getQueryStringFromLocation,
|
getQueryStringFromLocation,
|
||||||
getValueStringFromLocationByKey,
|
getValueStringFromLocationByKey,
|
||||||
withEndingSlash,
|
withEndingSlash,
|
||||||
@@ -143,3 +145,32 @@ describe("tests for getValueStringFromLocationByKey", () => {
|
|||||||
expect(getValueStringFromLocationByKey(location, "namespace")).toBeUndefined();
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -125,3 +125,18 @@ export function escapeUrlForRoute(url: string) {
|
|||||||
export function unescapeUrlForRoute(url: string) {
|
export function unescapeUrlForRoute(url: string) {
|
||||||
return url.replace(/\\/g, "");
|
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)}` : "";
|
||||||
|
};
|
||||||
|
|||||||
@@ -23,24 +23,33 @@
|
|||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { File, Repository } from "@scm-manager/ui-types";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Icon } from "@scm-manager/ui-components";
|
import { Icon } from "@scm-manager/ui-components";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { urls } from "@scm-manager/ui-api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
repository: Repository;
|
||||||
revision: string;
|
revision: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
currentSource: File;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchIcon = styled(Icon)`
|
const SearchIcon = styled(Icon)`
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FileSearchButton: FC<Props> = ({ baseUrl, revision }) => {
|
const FileSearchButton: FC<Props> = ({ baseUrl, revision, currentSource, repository }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
const currentSourcePath =
|
||||||
|
repository.type === "svn"
|
||||||
|
? urls.createPrevSourcePathQuery(`${revision}/${currentSource.path}`)
|
||||||
|
: urls.createPrevSourcePathQuery(currentSource.path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`${baseUrl}/search/${encodeURIComponent(revision)}`}
|
to={`${baseUrl}/search/${encodeURIComponent(revision)}/?${currentSourcePath}`}
|
||||||
aria-label={t("fileSearch.button.title")}
|
aria-label={t("fileSearch.button.title")}
|
||||||
data-testid="file_search_button"
|
data-testid="file_search_button"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { createA11yId, ErrorNotification, FilterInput, Help, Icon, Loading } fro
|
|||||||
import CodeActionBar from "../components/CodeActionBar";
|
import CodeActionBar from "../components/CodeActionBar";
|
||||||
import FileSearchResults from "../components/FileSearchResults";
|
import FileSearchResults from "../components/FileSearchResults";
|
||||||
import { filepathSearch } from "../utils/filepathSearch";
|
import { filepathSearch } from "../utils/filepathSearch";
|
||||||
|
import { encodeFilePath } from "../../sources/components/content/FileLink";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -64,7 +65,10 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
const { isLoading, error, data } = usePaths(repository, revision);
|
const { isLoading, error, data } = usePaths(repository, revision);
|
||||||
const [result, setResult] = useState<string[]>([]);
|
const [result, setResult] = useState<string[]>([]);
|
||||||
const query = urls.getQueryStringFromLocation(location) || "";
|
const query = urls.getQueryStringFromLocation(location) || "";
|
||||||
|
const prevSourcePath = urls.getPrevSourcePathFromLocation(location) || "";
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
const [firstSelectedBranch, setBranchChanged] = useState<string | undefined>(selectedBranch);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.length > 1 && data) {
|
if (query.length > 1 && data) {
|
||||||
setResult(filepathSearch(data.paths, query));
|
setResult(filepathSearch(data.paths, query));
|
||||||
@@ -74,20 +78,35 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
}, [data, query]);
|
}, [data, query]);
|
||||||
|
|
||||||
const search = (query: string) => {
|
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) => {
|
const onSelectBranch = (branch?: Branch) => {
|
||||||
if (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) => {
|
const evaluateSwitchViewLink = (type: string) => {
|
||||||
if (type === "sources") {
|
if (type === "sources" && repository.type !== "svn") {
|
||||||
return `${baseUrl}/sources/${revision}/`;
|
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}/`;
|
const contentBaseUrl = `${baseUrl}/sources/${revision}/`;
|
||||||
@@ -113,8 +132,15 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
"pb-0"
|
"pb-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HomeLink className={classNames("mr-3", "pr-3")} to={contentBaseUrl}>
|
<HomeLink
|
||||||
<HomeIcon title={t("fileSearch.home")} name="home" color="inherit" />
|
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>
|
</HomeLink>
|
||||||
<FilterInput
|
<FilterInput
|
||||||
className="is-full-width pr-2"
|
className="is-full-width pr-2"
|
||||||
|
|||||||
@@ -23,13 +23,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from "react";
|
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 { Repository, Branch } from "@scm-manager/ui-types";
|
||||||
import CodeActionBar from "../codeSection/components/CodeActionBar";
|
import CodeActionBar from "../codeSection/components/CodeActionBar";
|
||||||
import { urls } from "@scm-manager/ui-components";
|
import { urls } from "@scm-manager/ui-components";
|
||||||
import Changesets from "./Changesets";
|
import Changesets from "./Changesets";
|
||||||
import { RepositoryRevisionContextProvider } from "@scm-manager/ui-api";
|
import { RepositoryRevisionContextProvider } from "@scm-manager/ui-api";
|
||||||
import { encodePart } from "../sources/components/content/FileLink";
|
import { encodeFilePath, encodePart } from "../sources/components/content/FileLink";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -41,29 +41,37 @@ type Props = {
|
|||||||
const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }) => {
|
const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }) => {
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = urls.stripEndingSlash(urls.escapeUrlForRoute(match.url));
|
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 = () => {
|
const isBranchAvailable = () => {
|
||||||
return branches?.filter(b => b.name === selectedBranch).length === 0;
|
return branches?.filter((b) => b.name === selectedBranch).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateSwitchViewLink = () => {
|
const evaluateSwitchViewLink = () => {
|
||||||
|
const sourcePath = encodeFilePath(urls.getPrevSourcePathFromLocation(location) || "");
|
||||||
|
|
||||||
if (selectedBranch) {
|
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/`;
|
return `${baseUrl}/sources/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectBranch = (branch?: Branch) => {
|
const onSelectBranch = (branch?: Branch) => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
history.push(`${baseUrl}/branch/${encodePart(branch.name)}/changesets/`);
|
history.push(`${baseUrl}/branch/${encodePart(branch.name)}/changesets/${location.search}`);
|
||||||
} else {
|
} 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()}
|
switchViewLink={evaluateSwitchViewLink()}
|
||||||
/>
|
/>
|
||||||
<Route path={`${url}/:page?`}>
|
<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>
|
</Route>
|
||||||
</RepositoryRevisionContextProvider>
|
</RepositoryRevisionContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ export const encodePart = (part: string) => {
|
|||||||
return encodeURIComponent(part);
|
return encodeURIComponent(part);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const encodeFilePath = (filePath: string) => {
|
||||||
|
const encodedUri = encodePart(filePath);
|
||||||
|
return encodedUri.replace(/%2F/g, "/");
|
||||||
|
};
|
||||||
|
|
||||||
export const createRelativeLink = (
|
export const createRelativeLink = (
|
||||||
repositoryUrl: string,
|
repositoryUrl: string,
|
||||||
contextPath: string,
|
contextPath: string,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
import React, { FC, useEffect } from "react";
|
import React, { FC, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory, useLocation, useParams } from "react-router-dom";
|
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 { Branch, Repository } from "@scm-manager/ui-types";
|
||||||
import { Breadcrumb, ErrorNotification, Loading, Notification } from "@scm-manager/ui-components";
|
import { Breadcrumb, ErrorNotification, Loading, Notification } from "@scm-manager/ui-components";
|
||||||
import FileTree from "../components/FileTree";
|
import FileTree from "../components/FileTree";
|
||||||
@@ -52,7 +52,7 @@ const useUrlParams = () => {
|
|||||||
const { revision, path } = useParams<Params>();
|
const { revision, path } = useParams<Params>();
|
||||||
return {
|
return {
|
||||||
revision: revision ? decodeURIComponent(revision) : undefined,
|
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]);
|
}, [branches, selectedBranch, history, baseUrl]);
|
||||||
const { isLoading, error, data: file, isFetchingNextPage, fetchNextPage } = useSources(repository, {
|
const {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
data: file,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useSources(repository, {
|
||||||
revision,
|
revision,
|
||||||
path,
|
path,
|
||||||
// we have to wait until a branch is selected,
|
// we have to wait until a branch is selected,
|
||||||
// expect if we have no branches (svn)
|
// expect if we have no branches (svn)
|
||||||
enabled: !branches || !!selectedBranch
|
enabled: !branches || !!selectedBranch,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -106,9 +112,16 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const evaluateSwitchViewLink = () => {
|
const evaluateSwitchViewLink = () => {
|
||||||
if (branches && selectedBranch && branches?.filter(b => b.name === selectedBranch).length !== 0) {
|
if (branches && selectedBranch && branches?.filter((b) => b.name === selectedBranch).length !== 0) {
|
||||||
return `${baseUrl}/branch/${encodeURIComponent(selectedBranch)}/changesets/`;
|
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/`;
|
return `${baseUrl}/changesets/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +130,14 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
|
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
if (repository._links.paths) {
|
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 (
|
return (
|
||||||
@@ -127,8 +147,8 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
revision={revision || file.revision}
|
revision={revision || file.revision}
|
||||||
path={path || ""}
|
path={path || ""}
|
||||||
baseUrl={baseUrl + "/sources"}
|
baseUrl={baseUrl + "/sources"}
|
||||||
branch={branches?.filter(b => b.name === selectedBranch)[0]}
|
branch={branches?.filter((b) => b.name === selectedBranch)[0]}
|
||||||
defaultBranch={branches?.filter(b => b.defaultBranch === true)[0]}
|
defaultBranch={branches?.filter((b) => b.defaultBranch === true)[0]}
|
||||||
sources={file}
|
sources={file}
|
||||||
permalink={permalink}
|
permalink={permalink}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user