diff --git a/gradle/changelog/render_images_in_md.yaml b/gradle/changelog/render_images_in_md.yaml new file mode 100644 index 0000000000..653f533a9a --- /dev/null +++ b/gradle/changelog/render_images_in_md.yaml @@ -0,0 +1,2 @@ +- type: added + description: Markdown component to render images from repository correctly diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 95dcca030f..729e1a99eb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -91,6 +91,9 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { private Loader getLoader(CatCommandRequest request) throws IOException { org.eclipse.jgit.lib.Repository repo = open(); ObjectId revId = getCommitOrDefault(repo, request.getRevision()); + if (revId == null) { + throw notFound(entity("Revision", request.getRevision()).in(repository)); + } logger.info("loading content for file {} for revision {} in repository {}", request.getPath(), revId, repository); return getLoader(repo, revId, request.getPath()); } diff --git a/scm-ui/ui-api/src/RepositoryContext.tsx b/scm-ui/ui-api/src/RepositoryContext.tsx new file mode 100644 index 0000000000..1223d2287c --- /dev/null +++ b/scm-ui/ui-api/src/RepositoryContext.tsx @@ -0,0 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { createContext, FC, useContext } from "react"; +import { Repository } from "@scm-manager/ui-types"; + +const Context = createContext(undefined); + +export const useRepositoryContext = () => useContext(Context); + +export const RepositoryContextProvider: FC<{ repository: Repository }> = ({ repository, children }) => ( + {children} +); diff --git a/scm-ui/ui-api/src/RepositoryRevisionContext.tsx b/scm-ui/ui-api/src/RepositoryRevisionContext.tsx new file mode 100644 index 0000000000..cba17a8d5a --- /dev/null +++ b/scm-ui/ui-api/src/RepositoryRevisionContext.tsx @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { createContext, FC, useContext } from "react"; + +const Context = createContext(undefined); + +export const useRepositoryRevisionContext = () => useContext(Context); + +export const RepositoryRevisionContextProvider: FC<{ revision?: string }> = ({ revision, children }) => ( + {children} +); diff --git a/scm-ui/ui-api/src/index.ts b/scm-ui/ui-api/src/index.ts index d113c5eb5f..8da3143010 100644 --- a/scm-ui/ui-api/src/index.ts +++ b/scm-ui/ui-api/src/index.ts @@ -71,3 +71,5 @@ export * from "./ApiProvider"; export * from "./LegacyContext"; export * from "./NamespaceAndNameContext"; +export * from "./RepositoryContext"; +export * from "./RepositoryRevisionContext"; diff --git a/scm-ui/ui-components/src/__resources__/markdown-images.md.ts b/scm-ui/ui-components/src/__resources__/markdown-images.md.ts new file mode 100644 index 0000000000..7f19e39895 --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/markdown-images.md.ts @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export default `# Images + +Show case for different possibilities to render image src links. +Please note that some of the images do not work in storybook, +the story is mostly for checking if the src links are rendered correct. + +## External + +External images should be rendered with the unaltered link: + +![external image](https://github.com/scm-manager/scm-manager/blob/develop/docs/en/logo/scm-manager_logo.png) + +## Images from repository + +Images from the repository should be resolved to an api url: + +![relative path](some_image.jpg) + +![path starting with a '.'](./some_image.jpg) + +![absolute image path](/path/with/some_image.jpg) +`; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index c96bcc5143..597f2f2f9e 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -14437,6 +14437,87 @@ and this project adheres to `; +exports[`Storyshots MarkdownView Images 1`] = ` +
+
+
+

+ Images +

+ + +

+ Show case for different possibilities to render image src links. +Please note that some of the images do not work in storybook, +the story is mostly for checking if the src links are rendered correct. +

+ + +

+ External +

+ + +

+ External images should be rendered with the unaltered link: +

+ + +

+ external image +

+ + +

+ Images from repository +

+ + +

+ Images from the repository should be resolved to an api url: +

+ + +

+ relative path +

+ + +

+ path starting with a '.' +

+ + +

+ absolute image path +

+
+
+
+`; + exports[`Storyshots MarkdownView Inline Xml 1`] = `
{ remarkRendererList.heading = createMarkdownHeadingRenderer(permalink); } + remarkRendererList.image = createMarkdownImageRenderer(basePath); + let protocolLinkRendererExtensions: ProtocolLinkRendererExtensionMap = {}; if (!remarkRendererList.link) { const extensionPoints = this.context.getExtensions( diff --git a/scm-ui/ui-components/src/markdown/MarkdownImageRenderer.tsx b/scm-ui/ui-components/src/markdown/MarkdownImageRenderer.tsx new file mode 100644 index 0000000000..6fd43b795c --- /dev/null +++ b/scm-ui/ui-components/src/markdown/MarkdownImageRenderer.tsx @@ -0,0 +1,116 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { FC } from "react"; +import { useLocation } from "react-router-dom"; +import { Link } from "@scm-manager/ui-types"; +import { + isAbsolute, + isExternalLink, + isInternalScmRepoLink, + isLinkWithProtocol, + isSubDirectoryOf, + join, + normalizePath +} from "./paths"; +import { useRepositoryContext, useRepositoryRevisionContext } from "@scm-manager/ui-api"; + +export const createLocalLink = ( + basePath: string, + contentLink: string, + revision: string, + currentPath: string, + link: string +) => { + const apiBasePath = contentLink.replace("{revision}", revision); + if (isInternalScmRepoLink(link)) { + return link; + } + if (isAbsolute(link)) { + return apiBasePath.replace("{path}", link.substring(1)); + } + if (!isSubDirectoryOf(basePath, currentPath)) { + return apiBasePath.replace("{path}", link); + } + const relativePath = currentPath.substring(basePath.length); + let path = relativePath; + if (currentPath.endsWith("/")) { + path = relativePath.substring(0, relativePath.length - 1); + } + const lastSlash = path.lastIndexOf("/"); + if (lastSlash < 0) { + path = ""; + } else { + path = path.substring(0, lastSlash); + } + return apiBasePath.replace("{path}", normalizePath(join(path, link))); +}; + +type LinkProps = { + src: string; + alt: string; +}; + +type Props = LinkProps & { + base?: string; + contentLink?: string; +}; + +const MarkdownImageRenderer: FC = ({ src = "", alt = "", base, contentLink, children, ...props }) => { + const location = useLocation(); + const repository = useRepositoryContext(); + const revision = useRepositoryRevisionContext(); + + if (isExternalLink(src) || isLinkWithProtocol(src)) { + return ( + {alt} + {children} + + ); + } else if (base && repository && revision) { + const localLink = createLocalLink(base, (repository._links.content as Link).href, revision, location.pathname, src); + return ( + {alt} + {children} + + ); + } else if (src) { + return ( + {alt} + {children} + + ); + } else { + return {children}; + } +}; + +// we use a factory method, because react-markdown does not pass +// base as prop down to our link component. +export const create = (base: string | undefined): FC => { + return (props) => { + return ; + }; +}; + +export default MarkdownImageRenderer; diff --git a/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.test.tsx b/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.test.tsx index b03672bf28..b5c630bdde 100644 --- a/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.test.tsx +++ b/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.test.tsx @@ -21,13 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { - isAnchorLink, - isExternalLink, - isLinkWithProtocol, - createLocalLink, - isInternalScmRepoLink, -} from "./MarkdownLinkRenderer"; +import { isAnchorLink, isExternalLink, isLinkWithProtocol, isInternalScmRepoLink } from "./paths"; +import { createLocalLink } from "./MarkdownLinkRenderer"; describe("test isAnchorLink", () => { it("should return true", () => { diff --git a/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.tsx b/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.tsx index de496f1967..00d4980822 100644 --- a/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.tsx +++ b/scm-ui/ui-components/src/markdown/MarkdownLinkRenderer.tsx @@ -26,59 +26,15 @@ import { Link, useLocation } from "react-router-dom"; import ExternalLink from "../navigation/ExternalLink"; import { urls } from "@scm-manager/ui-api"; import { ProtocolLinkRendererExtensionMap } from "./markdownExtensions"; - -const externalLinkRegex = new RegExp("^http(s)?://"); -export const isExternalLink = (link: string) => { - return externalLinkRegex.test(link); -}; - -export const isAnchorLink = (link: string) => { - return link.startsWith("#"); -}; - -export const isInternalScmRepoLink = (link: string) => { - return link.startsWith("/repo/"); -}; - -const linkWithProtocolRegex = new RegExp("^([a-z]+):(.+)"); -export const isLinkWithProtocol = (link: string) => { - const match = link.match(linkWithProtocolRegex); - return match && { protocol: match[1], link: match[2] }; -}; - -const join = (left: string, right: string) => { - if (left.endsWith("/") && right.startsWith("/")) { - return left + right.substring(1); - } else if (!left.endsWith("/") && !right.startsWith("/")) { - return left + "/" + right; - } - return left + right; -}; - -const normalizePath = (path: string) => { - const stack = []; - const parts = path.split("/"); - for (const part of parts) { - if (part === "..") { - stack.pop(); - } else if (part !== ".") { - stack.push(part); - } - } - const normalizedPath = stack.join("/"); - if (normalizedPath.startsWith("/")) { - return normalizedPath; - } - return "/" + normalizedPath; -}; - -const isAbsolute = (link: string) => { - return link.startsWith("/"); -}; - -const isSubDirectoryOf = (basePath: string, currentPath: string) => { - return currentPath.startsWith(basePath); -}; +import { + isAbsolute, isAnchorLink, + isExternalLink, + isInternalScmRepoLink, + isLinkWithProtocol, + isSubDirectoryOf, + join, + normalizePath +} from "./paths"; export const createLocalLink = (basePath: string, currentPath: string, link: string) => { if (isInternalScmRepoLink(link)) { @@ -100,7 +56,7 @@ export const createLocalLink = (basePath: string, currentPath: string, link: str } else { path = path.substring(0, lastSlash); } - return normalizePath(join(path, link)); + return "/" + normalizePath(join(path, link)); }; type LinkProps = { diff --git a/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx b/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx index 724d76de9a..c494063261 100644 --- a/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx +++ b/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx @@ -32,6 +32,7 @@ import MarkdownXmlCodeBlock from "../__resources__/markdown-xml-codeblock.md"; import MarkdownUmlCodeBlock from "../__resources__/markdown-uml-codeblock.md"; import MarkdownInlineXml from "../__resources__/markdown-inline-xml.md"; import MarkdownLinks from "../__resources__/markdown-links.md"; +import MarkdownImages from "../__resources__/markdown-images.md"; import MarkdownCommitLinks from "../__resources__/markdown-commit-link.md"; import MarkdownXss from "../__resources__/markdown-xss.md"; import MarkdownChangelog from "../__resources__/markdown-changelog.md"; @@ -40,6 +41,7 @@ import { Subtitle } from "../layout"; import { MemoryRouter } from "react-router-dom"; import { Binder, BinderContext, extensionPoints } from "@scm-manager/ui-extensions"; import { ProtocolLinkRendererProps } from "./markdownExtensions"; +import { RepositoryContextProvider, RepositoryRevisionContextProvider } from "@scm-manager/ui-api"; const Spacing = styled.div` padding: 2em; @@ -114,7 +116,19 @@ storiesOf("MarkdownView", module) ); }) - .add("XSS Prevention", () => ); + .add("XSS Prevention", () => ) + .add("Images", () => ( + + + + + + )); export const ProtocolLinkRenderer: FC> = ({ protocol, href, children }) => { return ( diff --git a/scm-ui/ui-components/src/markdown/paths.ts b/scm-ui/ui-components/src/markdown/paths.ts new file mode 100644 index 0000000000..ddcab93d47 --- /dev/null +++ b/scm-ui/ui-components/src/markdown/paths.ts @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const externalLinkRegex = new RegExp("^http(s)?://"); +export const isExternalLink = (link: string) => { + return externalLinkRegex.test(link); +}; + +export const isAnchorLink = (link: string) => { + return link.startsWith("#"); +}; + +export const isInternalScmRepoLink = (link: string) => { + return link.startsWith("/repo/"); +}; + +const linkWithProtocolRegex = new RegExp("^([a-z]+):(.+)"); +export const isLinkWithProtocol = (link: string) => { + const match = link.match(linkWithProtocolRegex); + return match && { protocol: match[1], link: match[2] }; +}; + +export const join = (left: string, right: string) => { + if (left.endsWith("/") && right.startsWith("/")) { + return left + right.substring(1); + } else if (!left.endsWith("/") && !right.startsWith("/")) { + return left + "/" + right; + } + return left + right; +}; + +export const normalizePath = (path: string) => { + const stack = []; + const parts = path.split("/"); + for (const part of parts) { + if (part === "..") { + stack.pop(); + } else if (part !== ".") { + stack.push(part); + } + } + const normalizedPath = stack.join("/"); + if (normalizedPath.startsWith("/")) { + return normalizedPath.substring(1); + } + return normalizedPath; +}; + +export const isAbsolute = (link: string) => { + return link.startsWith("/"); +}; + +export const isSubDirectoryOf = (basePath: string, currentPath: string) => { + return currentPath.startsWith(basePath); +}; diff --git a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx index 876fe55d1f..6f23209ea0 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx @@ -27,7 +27,7 @@ import { Changeset, Repository } from "@scm-manager/ui-types"; import { ErrorPage, Loading } from "@scm-manager/ui-components"; import ChangesetDetails from "../components/changesets/ChangesetDetails"; import { FileControlFactory } from "@scm-manager/ui-components"; -import { useChangeset } from "@scm-manager/ui-api"; +import { RepositoryRevisionContextProvider, useChangeset } from "@scm-manager/ui-api"; import { useParams } from "react-router-dom"; type Props = { @@ -53,11 +53,13 @@ const ChangesetView: FC = ({ repository, fileControlFactoryFactory }) => } return ( - + + + ); }; diff --git a/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx index dc9aecafeb..c97cebf962 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx @@ -28,6 +28,7 @@ import { Repository, Branch } from "@scm-manager/ui-types"; import CodeActionBar from "../codeSection/components/CodeActionBar"; import { urls } from "@scm-manager/ui-components"; import Changesets from "./Changesets"; +import { RepositoryRevisionContextProvider } from "@scm-manager/ui-api"; type Props = { repository: Repository; @@ -66,7 +67,7 @@ const ChangesetRoot: FC = ({ repository, baseUrl, branches, selectedBranc }; return ( - <> + = ({ repository, baseUrl, branches, selectedBranc b.name === selectedBranch)[0]} url={url} /> - + ); }; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 883a3ae45e..17e7c1b7f8 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -60,7 +60,7 @@ import SourceExtensions from "../sources/containers/SourceExtensions"; import TagsOverview from "../tags/container/TagsOverview"; import CompareRoot from "../compare/CompareRoot"; import TagRoot from "../tags/container/TagRoot"; -import { useIndexLinks, useNamespaceAndNameContext, useRepository } from "@scm-manager/ui-api"; +import { RepositoryContextProvider, useIndexLinks, useNamespaceAndNameContext, useRepository } from "@scm-manager/ui-api"; import styled from "styled-components"; import { useShortcut } from "@scm-manager/ui-shortcuts"; @@ -265,149 +265,151 @@ const RepositoryRoot = () => { return ( - - - - - - - } - > - {modal} - - - - + + + + + + + + } + > + {modal} + + + + - {/* redirect pre 2.0.0-rc2 links */} - - - - - + {/* redirect pre 2.0.0-rc2 links */} + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - name="repository.route" - props={{ - repository, - url: urls.escapeUrlForRoute(url), - indexLinks, - }} - renderAll={true} - /> - - - - - - name="repository.navigation.topLevel" - props={extensionProps} - renderAll={true} - /> - - - - - - name="repository.navigation" - props={extensionProps} - renderAll={true} - /> - - - - - name="repository.setting" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + name="repository.route" + props={{ + repository, + url: urls.escapeUrlForRoute(url), + indexLinks, + }} + renderAll={true} + /> + + + + + + name="repository.navigation.topLevel" props={extensionProps} renderAll={true} /> - - - - - + + + + + + name="repository.navigation" + props={extensionProps} + renderAll={true} + /> + + + + + name="repository.setting" + props={extensionProps} + renderAll={true} + /> + + + + + + ); }; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/content/SwitchableMarkdownViewer.tsx b/scm-ui/ui-webapp/src/repos/sources/components/content/SwitchableMarkdownViewer.tsx index 3c81a3a1a6..ea66b03c48 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/content/SwitchableMarkdownViewer.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/content/SwitchableMarkdownViewer.tsx @@ -23,7 +23,7 @@ */ import React, { FC, useState } from "react"; import MarkdownViewer from "./MarkdownViewer"; -import { File } from "@scm-manager/ui-types"; +import { File, Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, SyntaxHighlighter } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { useFileContent } from "@scm-manager/ui-api"; @@ -34,9 +34,10 @@ import classNames from "classnames"; type Props = { file: File; basePath: string; + repository: Repository; }; -const SwitchableMarkdownViewer: FC = ({ file, basePath }) => { +const SwitchableMarkdownViewer: FC = ({ file, basePath, repository }) => { const { isLoading, error, data: content } = useFileContent(file); const { t } = useTranslation("repos"); const location = useLocation(); @@ -68,7 +69,13 @@ const SwitchableMarkdownViewer: FC = ({ file, basePath }) => {
{renderMarkdown ? ( - + ) : ( )} 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 535ccaa499..1bd3f30680 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx @@ -24,7 +24,7 @@ import React, { FC, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useHistory, useLocation, useParams } from "react-router-dom"; -import { useSources } from "@scm-manager/ui-api"; +import { RepositoryRevisionContextProvider, useSources } from "@scm-manager/ui-api"; import { Branch, Repository } from "@scm-manager/ui-types"; import { Breadcrumb, ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; import FileTree from "../components/FileTree"; @@ -178,7 +178,7 @@ const Sources: FC = ({ repository, branches, selectedBranch, baseUrl }) = }; return ( - <> + {hasBranchesWhenSupporting(repository) && ( = ({ repository, branches, selectedBranch, baseUrl }) = /> )} {renderPanelContent()} - + ); }; diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx index bd9e103da0..7fe5a93345 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx @@ -66,7 +66,7 @@ const SourcesView: FC = ({ file, repository, revision }) => { if (contentType.startsWith("image/")) { sources = ; } else if (contentType.includes("markdown") || (language && language.toLowerCase() === "markdown")) { - sources = ; + sources = ; } else if (language) { sources = ; } else if (contentType.startsWith("text/")) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 4a2f925a7d..0e0a60c9a1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -170,6 +170,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper