From e55ba52ace7879bbf0ed1e1581b779b474239eeb Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 2 Jun 2021 15:27:41 +0200 Subject: [PATCH] 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. --- gradle/changelog/file_not_found_controls.yaml | 2 + .../src/repos/sources/containers/Content.tsx | 190 +++++++++--------- .../src/repos/sources/containers/Sources.tsx | 31 +-- 3 files changed, 109 insertions(+), 114 deletions(-) create mode 100644 gradle/changelog/file_not_found_controls.yaml diff --git a/gradle/changelog/file_not_found_controls.yaml b/gradle/changelog/file_not_found_controls.yaml new file mode 100644 index 0000000000..34128197d0 --- /dev/null +++ b/gradle/changelog/file_not_found_controls.yaml @@ -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)) diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx index 560bb58379..499e536e1a 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx @@ -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 { - constructor(props: Props) { - super(props); +const Content: FC = ({ file, repository, revision, path, breadcrumb, error }) => { + const [t] = useTranslation("repos"); + const [collapsed, setCollapsed] = useState(true); + const [selected, setSelected] = useState("source"); + const [errorFromExtension, setErrorFromExtension] = useState(); - this.state = { - collapsed: true, - selected: "source" - }; - } - - toggleCollapse = () => { - this.setState(prevState => ({ - collapsed: !prevState.collapsed - })); + const wrapContent = (content: ReactNode) => { + return ( + <> +
+ {breadcrumb} + {content} +
+ + + ); }; - 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 ? ( 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 ( -
- - - {file.name} - - - {selector} - {content}} - tooltipStyle="htmlTitle" - /> - - -
+ +
+ + + {file.name} + + + {selector} + {content}} + tooltipStyle="htmlTitle" + /> + + +
+
); - } + }; - showMoreInformation() { - const collapsed = this.state.collapsed; - const { file, revision, t, repository } = this.props; - const date = ; + const showMoreInformation = () => { + const fileSize = file.directory ? null : ; const description = file.description ? (

{file.description.split("\n").map((item, key) => { @@ -167,7 +160,7 @@ class Content extends React.Component { })}

) : null; - const fileSize = file.directory ? "" : ; + if (!collapsed) { return ( <> @@ -188,7 +181,9 @@ class Content extends React.Component { {t("sources.content.commitDate")} - {date} + + + {t("sources.content.description")} @@ -200,7 +195,7 @@ class Content extends React.Component { props={{ file, repository, - revision + revision, }} /> @@ -211,38 +206,33 @@ class Content extends React.Component { ); } return null; + }; + + if (!file || error) { + return wrapContent(); } - render() { - const { file, revision, repository, path, breadcrumb } = this.props; - const { selected, errorFromExtension } = this.state; - - let content; - switch (selected) { - case "source": - content = ; - break; - case "history": - content = ; - break; - case "annotations": - content = ; - } - const header = this.showHeader(content); - const moreInformation = this.showMoreInformation(); - - return ( -
-
- {breadcrumb} -
{header}
- {moreInformation} - {content} -
- {errorFromExtension && } -
- ); + let body; + switch (selected) { + case "source": + body = ; + break; + case "annotations": + body = ; + break; + case "history": + body = ; } -} + const header = showHeader(body); + const moreInformation = showMoreInformation(); -export default withTranslation("repos")(Content); + return wrapContent( + <> + {header} + {moreInformation} + {body} + + ); +}; + +export default Content; diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx index a4330d7cea..84323e6af3 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx @@ -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(); return { revision: revision ? decodeURIComponent(revision) : undefined, - path: path || "" + path: path || "", }; }; @@ -62,23 +62,25 @@ const Sources: FC = ({ 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 ; - } - - if (isLoading || !file) { + if (isLoading || (!error && !file)) { return ; } @@ -98,7 +100,7 @@ const Sources: FC = ({ 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 = ({ 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 = ({ repository, branches, selectedBranch, baseUrl }) = revision={revision || file.revision} path={path} breadcrumb={renderBreadcrumb()} + error={error} /> );