mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 23:45:44 +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:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user