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:
Florian Scholdei
2021-06-02 15:27:41 +02:00
committed by GitHub
parent 5cb2f077d9
commit e55ba52ace
3 changed files with 109 additions and 114 deletions

View File

@@ -21,8 +21,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ReactNode } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC, ReactNode, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import styled from "styled-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
@@ -33,21 +33,16 @@ import SourcesView from "./SourcesView";
import HistoryView from "./HistoryView";
import AnnotateView from "./AnnotateView";
type Props = WithTranslation & {
type Props = {
file: File;
repository: Repository;
revision: string;
path: string;
breadcrumb: React.ReactNode;
error?: Error;
};
type State = {
collapsed: boolean;
selected: SourceViewSelection;
errorFromExtension?: Error;
};
const Header = styled.div`
const HeaderWrapper = styled.div`
border-bottom: solid 1px #dbdbdb;
font-size: 1.25em;
font-weight: 300;
@@ -81,80 +76,78 @@ const BorderLessDiv = styled.div`
box-shadow: none;
`;
export type SourceViewSelection = "source" | "history" | "annotations";
export type SourceViewSelection = "source" | "annotations" | "history";
class Content extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const Content: FC<Props> = ({ file, repository, revision, path, breadcrumb, error }) => {
const [t] = useTranslation("repos");
const [collapsed, setCollapsed] = useState(true);
const [selected, setSelected] = useState<SourceViewSelection>("source");
const [errorFromExtension, setErrorFromExtension] = useState<Error>();
this.state = {
collapsed: true,
selected: "source"
};
}
toggleCollapse = () => {
this.setState(prevState => ({
collapsed: !prevState.collapsed
}));
const wrapContent = (content: ReactNode) => {
return (
<>
<div className="panel">
{breadcrumb}
{content}
</div>
<ErrorNotification error={errorFromExtension} />
</>
);
};
handleExtensionError = (error: Error) => {
this.setState({
errorFromExtension: error
});
const toggleCollapse = () => {
setCollapsed(!collapsed);
};
showHeader(content: ReactNode) {
const { repository, file, revision } = this.props;
const { selected, collapsed } = this.state;
const showHeader = (content: ReactNode) => {
const icon = collapsed ? "angle-right" : "angle-down";
const selector = file._links.history ? (
<FileButtonAddons
className="mr-2"
selected={selected}
showSources={() => this.setState({ selected: "source" })}
showHistory={() => this.setState({ selected: "history" })}
showAnnotations={() => this.setState({ selected: "annotations" })}
showSources={() => setSelected("source")}
showAnnotations={() => setSelected("annotations")}
showHistory={() => setSelected("history")}
/>
) : null;
return (
<div className={classNames("level", "is-flex-wrap-wrap")}>
<FullWidthTitleHeader
className={classNames("level-left", "is-flex", "has-cursor-pointer", "is-word-break", "mr-2")}
onClick={this.toggleCollapse}
>
<Icon className={classNames("is-inline", "mr-2")} name={`${icon} fa-fw`} color="inherit" />
{file.name}
</FullWidthTitleHeader>
<AlignRight className={classNames("level-right", "buttons")}>
{selector}
<OpenInFullscreenButton
modalTitle={file?.name}
modalBody={<BorderLessDiv className="panel">{content}</BorderLessDiv>}
tooltipStyle="htmlTitle"
/>
<ExtensionPoint
name="repos.sources.content.actionbar"
props={{
repository,
file,
revision: revision ? encodeURIComponent(revision) : "",
handleExtensionError: this.handleExtensionError
}}
renderAll={true}
/>
</AlignRight>
</div>
<HeaderWrapper>
<div className={classNames("level", "is-flex-wrap-wrap")}>
<FullWidthTitleHeader
className={classNames("level-left", "is-flex", "has-cursor-pointer", "is-word-break", "mr-2")}
onClick={toggleCollapse}
>
<Icon className={classNames("is-inline", "mr-2")} name={`${icon} fa-fw`} color="inherit" />
{file.name}
</FullWidthTitleHeader>
<AlignRight className={classNames("level-right", "buttons")}>
{selector}
<OpenInFullscreenButton
modalTitle={file?.name}
modalBody={<BorderLessDiv className="panel">{content}</BorderLessDiv>}
tooltipStyle="htmlTitle"
/>
<ExtensionPoint
name="repos.sources.content.actionbar"
props={{
repository,
file,
revision: revision ? encodeURIComponent(revision) : "",
handleExtensionError: setErrorFromExtension,
}}
renderAll={true}
/>
</AlignRight>
</div>
</HeaderWrapper>
);
}
};
showMoreInformation() {
const collapsed = this.state.collapsed;
const { file, revision, t, repository } = this.props;
const date = <DateFromNow date={file.commitDate} />;
const showMoreInformation = () => {
const fileSize = file.directory ? null : <FileSize bytes={file?.length ? file.length : 0} />;
const description = file.description ? (
<p>
{file.description.split("\n").map((item, key) => {
@@ -167,7 +160,7 @@ class Content extends React.Component<Props, State> {
})}
</p>
) : null;
const fileSize = file.directory ? "" : <FileSize bytes={file?.length ? file.length : 0} />;
if (!collapsed) {
return (
<>
@@ -188,7 +181,9 @@ class Content extends React.Component<Props, State> {
</tr>
<tr>
<td>{t("sources.content.commitDate")}</td>
<td>{date}</td>
<td>
<DateFromNow date={file.commitDate} />
</td>
</tr>
<tr>
<td>{t("sources.content.description")}</td>
@@ -200,7 +195,7 @@ class Content extends React.Component<Props, State> {
props={{
file,
repository,
revision
revision,
}}
/>
</tbody>
@@ -211,38 +206,33 @@ class Content extends React.Component<Props, State> {
);
}
return null;
};
if (!file || error) {
return wrapContent(<ErrorNotification error={error} />);
}
render() {
const { file, revision, repository, path, breadcrumb } = this.props;
const { selected, errorFromExtension } = this.state;
let content;
switch (selected) {
case "source":
content = <SourcesView revision={revision} file={file} repository={repository} path={path} />;
break;
case "history":
content = <HistoryView file={file} repository={repository} />;
break;
case "annotations":
content = <AnnotateView file={file} repository={repository} />;
}
const header = this.showHeader(content);
const moreInformation = this.showMoreInformation();
return (
<div>
<div className="panel">
{breadcrumb}
<Header>{header}</Header>
{moreInformation}
{content}
</div>
{errorFromExtension && <ErrorNotification error={errorFromExtension} />}
</div>
);
let body;
switch (selected) {
case "source":
body = <SourcesView revision={revision} file={file} repository={repository} path={path} />;
break;
case "annotations":
body = <AnnotateView file={file} repository={repository} />;
break;
case "history":
body = <HistoryView file={file} repository={repository} />;
}
}
const header = showHeader(body);
const moreInformation = showMoreInformation();
export default withTranslation("repos")(Content);
return wrapContent(
<>
{header}
{moreInformation}
{body}
</>
);
};
export default Content;

View File

@@ -23,7 +23,7 @@
*/
import React, { FC, useEffect } from "react";
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 Content from "./Content";
import CodeActionBar from "../../codeSection/components/CodeActionBar";
@@ -50,7 +50,7 @@ const useUrlParams = () => {
const { revision, path } = useParams<Params>();
return {
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
useEffect(() => {
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)}/`);
}
}, [branches, selectedBranch]);
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) {
return <ErrorNotification error={error} />;
}
if (isLoading || !file) {
if (isLoading || (!error && !file)) {
return <Loading />;
}
@@ -98,7 +100,7 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
};
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}/changesets/`;
@@ -119,15 +121,15 @@ 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}
/>
);
};
if (file.directory) {
if (file && file.directory) {
let body;
if (isRootFile(file) && isEmptyDirectory(file)) {
body = (
@@ -176,6 +178,7 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
revision={revision || file.revision}
path={path}
breadcrumb={renderBreadcrumb()}
error={error}
/>
</>
);