mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 09:25:43 +01:00
Show source code controls even if a file is not present (#1680)
This fix preserves the context and shows the error in the component where normally the content of the file is displayed, so that you can still change the branch and the path.
This commit is contained in:
2
gradle/changelog/file_not_found_controls.yaml
Normal file
2
gradle/changelog/file_not_found_controls.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: fixed
|
||||||
|
description: Show source code controls even if a file is not present ([#1680](https://github.com/scm-manager/scm-manager/pull/1680))
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { ReactNode } from "react";
|
import React, { FC, ReactNode, useState } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
@@ -33,21 +33,16 @@ import SourcesView from "./SourcesView";
|
|||||||
import HistoryView from "./HistoryView";
|
import HistoryView from "./HistoryView";
|
||||||
import AnnotateView from "./AnnotateView";
|
import AnnotateView from "./AnnotateView";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = {
|
||||||
file: File;
|
file: File;
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
revision: string;
|
revision: string;
|
||||||
path: string;
|
path: string;
|
||||||
breadcrumb: React.ReactNode;
|
breadcrumb: React.ReactNode;
|
||||||
|
error?: Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
const HeaderWrapper = styled.div`
|
||||||
collapsed: boolean;
|
|
||||||
selected: SourceViewSelection;
|
|
||||||
errorFromExtension?: Error;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
border-bottom: solid 1px #dbdbdb;
|
border-bottom: solid 1px #dbdbdb;
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@@ -81,50 +76,49 @@ const BorderLessDiv = styled.div`
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type SourceViewSelection = "source" | "history" | "annotations";
|
export type SourceViewSelection = "source" | "annotations" | "history";
|
||||||
|
|
||||||
class Content extends React.Component<Props, State> {
|
const Content: FC<Props> = ({ file, repository, revision, path, breadcrumb, error }) => {
|
||||||
constructor(props: Props) {
|
const [t] = useTranslation("repos");
|
||||||
super(props);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
|
const [selected, setSelected] = useState<SourceViewSelection>("source");
|
||||||
|
const [errorFromExtension, setErrorFromExtension] = useState<Error>();
|
||||||
|
|
||||||
this.state = {
|
const wrapContent = (content: ReactNode) => {
|
||||||
collapsed: true,
|
return (
|
||||||
selected: "source"
|
<>
|
||||||
};
|
<div className="panel">
|
||||||
}
|
{breadcrumb}
|
||||||
|
{content}
|
||||||
toggleCollapse = () => {
|
</div>
|
||||||
this.setState(prevState => ({
|
<ErrorNotification error={errorFromExtension} />
|
||||||
collapsed: !prevState.collapsed
|
</>
|
||||||
}));
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleExtensionError = (error: Error) => {
|
const toggleCollapse = () => {
|
||||||
this.setState({
|
setCollapsed(!collapsed);
|
||||||
errorFromExtension: error
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showHeader(content: ReactNode) {
|
const showHeader = (content: ReactNode) => {
|
||||||
const { repository, file, revision } = this.props;
|
|
||||||
const { selected, collapsed } = this.state;
|
|
||||||
const icon = collapsed ? "angle-right" : "angle-down";
|
const icon = collapsed ? "angle-right" : "angle-down";
|
||||||
|
|
||||||
const selector = file._links.history ? (
|
const selector = file._links.history ? (
|
||||||
<FileButtonAddons
|
<FileButtonAddons
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
selected={selected}
|
selected={selected}
|
||||||
showSources={() => this.setState({ selected: "source" })}
|
showSources={() => setSelected("source")}
|
||||||
showHistory={() => this.setState({ selected: "history" })}
|
showAnnotations={() => setSelected("annotations")}
|
||||||
showAnnotations={() => this.setState({ selected: "annotations" })}
|
showHistory={() => setSelected("history")}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<HeaderWrapper>
|
||||||
<div className={classNames("level", "is-flex-wrap-wrap")}>
|
<div className={classNames("level", "is-flex-wrap-wrap")}>
|
||||||
<FullWidthTitleHeader
|
<FullWidthTitleHeader
|
||||||
className={classNames("level-left", "is-flex", "has-cursor-pointer", "is-word-break", "mr-2")}
|
className={classNames("level-left", "is-flex", "has-cursor-pointer", "is-word-break", "mr-2")}
|
||||||
onClick={this.toggleCollapse}
|
onClick={toggleCollapse}
|
||||||
>
|
>
|
||||||
<Icon className={classNames("is-inline", "mr-2")} name={`${icon} fa-fw`} color="inherit" />
|
<Icon className={classNames("is-inline", "mr-2")} name={`${icon} fa-fw`} color="inherit" />
|
||||||
{file.name}
|
{file.name}
|
||||||
@@ -142,19 +136,18 @@ class Content extends React.Component<Props, State> {
|
|||||||
repository,
|
repository,
|
||||||
file,
|
file,
|
||||||
revision: revision ? encodeURIComponent(revision) : "",
|
revision: revision ? encodeURIComponent(revision) : "",
|
||||||
handleExtensionError: this.handleExtensionError
|
handleExtensionError: setErrorFromExtension,
|
||||||
}}
|
}}
|
||||||
renderAll={true}
|
renderAll={true}
|
||||||
/>
|
/>
|
||||||
</AlignRight>
|
</AlignRight>
|
||||||
</div>
|
</div>
|
||||||
|
</HeaderWrapper>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
showMoreInformation() {
|
const showMoreInformation = () => {
|
||||||
const collapsed = this.state.collapsed;
|
const fileSize = file.directory ? null : <FileSize bytes={file?.length ? file.length : 0} />;
|
||||||
const { file, revision, t, repository } = this.props;
|
|
||||||
const date = <DateFromNow date={file.commitDate} />;
|
|
||||||
const description = file.description ? (
|
const description = file.description ? (
|
||||||
<p>
|
<p>
|
||||||
{file.description.split("\n").map((item, key) => {
|
{file.description.split("\n").map((item, key) => {
|
||||||
@@ -167,7 +160,7 @@ class Content extends React.Component<Props, State> {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
) : null;
|
) : null;
|
||||||
const fileSize = file.directory ? "" : <FileSize bytes={file?.length ? file.length : 0} />;
|
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -188,7 +181,9 @@ class Content extends React.Component<Props, State> {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.commitDate")}</td>
|
<td>{t("sources.content.commitDate")}</td>
|
||||||
<td>{date}</td>
|
<td>
|
||||||
|
<DateFromNow date={file.commitDate} />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.description")}</td>
|
<td>{t("sources.content.description")}</td>
|
||||||
@@ -200,7 +195,7 @@ class Content extends React.Component<Props, State> {
|
|||||||
props={{
|
props={{
|
||||||
file,
|
file,
|
||||||
repository,
|
repository,
|
||||||
revision
|
revision,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -211,38 +206,33 @@ class Content extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!file || error) {
|
||||||
|
return wrapContent(<ErrorNotification error={error} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
let body;
|
||||||
const { file, revision, repository, path, breadcrumb } = this.props;
|
|
||||||
const { selected, errorFromExtension } = this.state;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
switch (selected) {
|
switch (selected) {
|
||||||
case "source":
|
case "source":
|
||||||
content = <SourcesView revision={revision} file={file} repository={repository} path={path} />;
|
body = <SourcesView revision={revision} file={file} repository={repository} path={path} />;
|
||||||
break;
|
|
||||||
case "history":
|
|
||||||
content = <HistoryView file={file} repository={repository} />;
|
|
||||||
break;
|
break;
|
||||||
case "annotations":
|
case "annotations":
|
||||||
content = <AnnotateView file={file} repository={repository} />;
|
body = <AnnotateView file={file} repository={repository} />;
|
||||||
|
break;
|
||||||
|
case "history":
|
||||||
|
body = <HistoryView file={file} repository={repository} />;
|
||||||
}
|
}
|
||||||
const header = this.showHeader(content);
|
const header = showHeader(body);
|
||||||
const moreInformation = this.showMoreInformation();
|
const moreInformation = showMoreInformation();
|
||||||
|
|
||||||
return (
|
return wrapContent(
|
||||||
<div>
|
<>
|
||||||
<div className="panel">
|
{header}
|
||||||
{breadcrumb}
|
|
||||||
<Header>{header}</Header>
|
|
||||||
{moreInformation}
|
{moreInformation}
|
||||||
{content}
|
{body}
|
||||||
</div>
|
</>
|
||||||
{errorFromExtension && <ErrorNotification error={errorFromExtension} />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation("repos")(Content);
|
export default Content;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { FC, useEffect } from "react";
|
import React, { FC, useEffect } from "react";
|
||||||
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, Loading, Notification } from "@scm-manager/ui-components";
|
||||||
import FileTree from "../components/FileTree";
|
import FileTree from "../components/FileTree";
|
||||||
import Content from "./Content";
|
import Content from "./Content";
|
||||||
import CodeActionBar from "../../codeSection/components/CodeActionBar";
|
import CodeActionBar from "../../codeSection/components/CodeActionBar";
|
||||||
@@ -50,7 +50,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 || "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,23 +62,25 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
// redirect to default branch is non branch selected
|
// redirect to default branch is non branch selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (branches && branches.length > 0 && !selectedBranch) {
|
if (branches && branches.length > 0 && !selectedBranch) {
|
||||||
const defaultBranch = branches?.filter(b => b.defaultBranch === true)[0];
|
const defaultBranch = branches?.filter((b) => b.defaultBranch === true)[0];
|
||||||
history.replace(`${baseUrl}/sources/${encodeURIComponent(defaultBranch.name)}/`);
|
history.replace(`${baseUrl}/sources/${encodeURIComponent(defaultBranch.name)}/`);
|
||||||
}
|
}
|
||||||
}, [branches, selectedBranch]);
|
}, [branches, selectedBranch]);
|
||||||
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 (isLoading || (!error && !file)) {
|
||||||
return <ErrorNotification error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading || !file) {
|
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ 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/`;
|
||||||
}
|
}
|
||||||
return `${baseUrl}/changesets/`;
|
return `${baseUrl}/changesets/`;
|
||||||
@@ -119,15 +121,15 @@ 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file.directory) {
|
if (file && file.directory) {
|
||||||
let body;
|
let body;
|
||||||
if (isRootFile(file) && isEmptyDirectory(file)) {
|
if (isRootFile(file) && isEmptyDirectory(file)) {
|
||||||
body = (
|
body = (
|
||||||
@@ -176,6 +178,7 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
revision={revision || file.revision}
|
revision={revision || file.revision}
|
||||||
path={path}
|
path={path}
|
||||||
breadcrumb={renderBreadcrumb()}
|
breadcrumb={renderBreadcrumb()}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user