diff --git a/CHANGELOG.md b/CHANGELOG.md index 886f83007c..5d629bcb0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278)) +- add link to source file in diff sections ([#1267](https://github.com/scm-manager/scm-manager/pull/1267)) - Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283)) ### Fixed @@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Avoid stacktrace logging when protocol url is accessed outside of request scope ([#1276](https://github.com/scm-manager/scm-manager/pull/1276)) ## [2.3.0] - 2020-07-23 + ### Added - Add branch link provider to access branch links in plugins ([#1243](https://github.com/scm-manager/scm-manager/pull/1243)) - Add key value input field component ([#1246](https://github.com/scm-manager/scm-manager/pull/1246)) diff --git a/docs/de/user/repo/assets/repository-code-changesetDetails.png b/docs/de/user/repo/assets/repository-code-changesetDetails.png index f576e0736a..b3bce461c0 100644 Binary files a/docs/de/user/repo/assets/repository-code-changesetDetails.png and b/docs/de/user/repo/assets/repository-code-changesetDetails.png differ diff --git a/docs/en/user/repo/assets/repository-code-changesetDetails.png b/docs/en/user/repo/assets/repository-code-changesetDetails.png index c157813470..cfcdd88939 100644 Binary files a/docs/en/user/repo/assets/repository-code-changesetDetails.png and b/docs/en/user/repo/assets/repository-code-changesetDetails.png differ 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 05297adf93..4b0a0e08ea 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -3093,6 +3093,13 @@ exports[`Storyshots Diff Binaries 1`] = ` add +
+
+
+
+ + + + + +
+
+ + + + + +
@@ -3230,6 +3275,44 @@ exports[`Storyshots Diff Collapsed 1`] = ` +
+ + + + + +
+
+ + + + + +
@@ -3287,6 +3370,44 @@ exports[`Storyshots Diff Collapsed 1`] = ` +
+ + + + + +
+
+ + + + + +
@@ -3344,6 +3465,44 @@ exports[`Storyshots Diff Collapsed 1`] = ` +
+ + + + + +
+
+ + + + + +
@@ -3401,6 +3560,44 @@ exports[`Storyshots Diff Collapsed 1`] = ` +
+ + + + + +
+
+ + + + + +
@@ -3458,6 +3655,44 @@ exports[`Storyshots Diff Collapsed 1`] = ` +
+ + + + + +
+
+ + + + + +
@@ -38310,6 +38545,4312 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` `; +exports[`Storyshots Diff WithLinkToFile 1`] = ` +
+
+
+
+
+ + + src/main/java/com/cloudogu/scm/review/events/EventListener.java + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1 + + 1 + + package com.cloudogu.scm.review.events; +
+ 2 + + 2 + + +
+ 3 + + + import com.cloudogu.scm.review.comment.service.BasicComment; +
+ 4 + + + import com.cloudogu.scm.review.comment.service.BasicCommentEvent; +
+ 5 + + + import com.cloudogu.scm.review.comment.service.CommentEvent; +
+ 6 + + + import com.cloudogu.scm.review.comment.service.ReplyEvent; +
+ 7 + + 3 + + import com.cloudogu.scm.review.pullrequest.service.BasicPullRequestEvent; +
+ 8 + + 4 + + import com.cloudogu.scm.review.pullrequest.service.PullRequest; +
+ 9 + + + import com.cloudogu.scm.review.pullrequest.service.PullRequestEvent; +
+ 10 + + 5 + + import com.github.legman.Subscribe; +
+ 11 + + + import lombok.Data; +
+ 12 + + 6 + + import org.apache.shiro.SecurityUtils; +
+ 13 + + 7 + + import org.apache.shiro.subject.PrincipalCollection; +
+ 14 + + 8 + + import org.apache.shiro.subject.Subject; +
+ 15 + + 9 + + import sonia.scm.EagerSingleton; +
+ 16 + + + import sonia.scm.HandlerEventType; +
+ 17 + + + import sonia.scm.event.HandlerEvent; +
+ 18 + + 10 + + import sonia.scm.plugin.Extension; +
+ 19 + + 11 + + import sonia.scm.repository.Repository; +
+ 20 + + 12 + + import sonia.scm.security.SessionId; +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/main/js/ChangeNotification.tsx + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandComplete + +
+
+ 2 + + 2 + + import { Link } from "@scm-manager/ui-types"; +
+ 3 + + 3 + + import { apiClient, Toast, ToastButtons, ToastButton } from "@scm-manager/ui-components"; +
+ 4 + + 4 + + import { PullRequest } from "./types/PullRequest"; +
+ + 5 + + import { useTranslation } from "react-i18next"; +
+ 5 + + 6 + + +
+ 6 + + 7 + + type HandlerProps = { +
+ 7 + + 8 + + url: string; +
+
+ + + + diff.expandComplete + +
+
+
+ + + + diff.expandComplete + +
+
+ 15 + + 16 + + pullRequest: setEvent +
+ 16 + + 17 + + }); +
+ 17 + + 18 + + }, [url]); +
+ + 19 + + const { t } = useTranslation("plugins"); +
+ 18 + + 20 + + if (event) { +
+ 19 + + 21 + + return ( +
+ 20 + + + <Toast type="warning" title="New Changes"> +
+ 21 + + + <p>The underlying Pull-Request has changed. Press reload to see the changes.</p> +
+ 22 + + + <p>Warning: Non saved modification will be lost.</p> +
+ + 22 + + <Toast type="warning" title={t("scm-review-plugin.changeNotification.title")}> +
+ + 23 + + <p>{t("scm-review-plugin.changeNotification.description")}</p> +
+ + 24 + + <p>{t("scm-review-plugin.changeNotification.modificationWarning")}</p> +
+ 23 + + 25 + + <ToastButtons> +
+ 24 + + + <ToastButton icon="redo" onClick={reload}>Reload</ToastButton> +
+ 25 + + + <ToastButton icon="times" onClick={() => setEvent(undefined)}>Ignore</ToastButton> +
+ + 26 + + <ToastButton icon="redo" onClick={reload}> +
+ + 27 + + {t("scm-review-plugin.changeNotification.buttons.reload")} +
+ + 28 + + </ToastButton> +
+ + 29 + + <ToastButton icon="times" onClick={() => setEvent(undefined)}> +
+ + 30 + + {t("scm-review-plugin.changeNotification.buttons.ignore")} +
+ + 31 + + </ToastButton> +
+ 26 + + 32 + + </ToastButtons> +
+ 27 + + 33 + + </Toast> +
+ 28 + + 34 + + ); +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/main/resources/locales/de/plugins.json + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+ 181 + + 181 + + "titleClickable": "Der Kommentar bezieht sich auf eine ältere Version des Source- oder Target-Branches. Klicken Sie hier, um den ursprünglichen Kontext zu sehen." +
+ 182 + + 182 + + } +
+ 183 + + 183 + + } +
+ + 184 + + }, +
+ + 185 + + "changeNotification": { +
+ + 186 + + "title": "Neue Änderungen", +
+ + 187 + + "description": "An diesem Pull Request wurden Änderungen vorgenommen. Laden Sie die Seite neu um diese anzuzeigen.", +
+ + 188 + + "modificationWarning": "Warnung: Nicht gespeicherte Eingaben gehen verloren.", +
+ + 189 + + "buttons": { +
+ + 190 + + "reload": "Neu laden", +
+ + 191 + + "ignore": "Ignorieren" +
+ + 192 + + } +
+ 184 + + 193 + + } +
+ 185 + + 194 + + }, +
+ 186 + + 195 + + "permissions": { +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/main/resources/locales/en/plugins.json + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+ 181 + + 181 + + "titleClickable": "The comment is related to an older of the source or target branch. Click here to see the original context." +
+ 182 + + 182 + + } +
+ 183 + + 183 + + } +
+ + 184 + + }, +
+ + 185 + + "changeNotification": { +
+ + 186 + + "title": "New Changes", +
+ + 187 + + "description": "The underlying Pull-Request has changed. Press reload to see the changes.", +
+ + 188 + + "modificationWarning": "Warning: Non saved modification will be lost.", +
+ + 189 + + "buttons": { +
+ + 190 + + "reload": "Reload", +
+ + 191 + + "ignore": "Ignore" +
+ + 192 + + } +
+ 184 + + 193 + + } +
+ 185 + + 194 + + }, +
+ 186 + + 195 + + "permissions": { +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/test/java/com/cloudogu/scm/review/events/ClientTest.java + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandComplete + +
+
+ 7 + + 7 + + import org.mockito.Mock; +
+ 8 + + 8 + + import org.mockito.junit.jupiter.MockitoExtension; +
+ 9 + + 9 + + import sonia.scm.security.SessionId; +
+ + 10 + + +
+ 10 + + 11 + + import javax.ws.rs.sse.OutboundSseEvent; +
+ 11 + + 12 + + import javax.ws.rs.sse.SseEventSink; +
+ 12 + + + +
+ 13 + + 13 + + import java.time.Clock; +
+ 14 + + 14 + + import java.time.Instant; +
+ 15 + + 15 + + import java.time.LocalDateTime; +
+ 16 + + 16 + + import java.time.ZoneOffset; +
+ 17 + + 17 + + import java.time.temporal.ChronoField; +
+ 18 + + + import java.time.temporal.ChronoUnit; +
+ 19 + + + import java.time.temporal.TemporalField; +
+ 20 + + 18 + + import java.util.concurrent.CompletableFuture; +
+ 21 + + 19 + + import java.util.concurrent.CompletionStage; +
+ 22 + + + import java.util.concurrent.atomic.AtomicLong; +
+ 23 + + 20 + + import java.util.concurrent.atomic.AtomicReference; +
+ 24 + + 21 + + +
+ 25 + + 22 + + import static java.time.temporal.ChronoUnit.MINUTES; +
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+ 83 + + 80 + + +
+ 84 + + 81 + + @Test +
+ 85 + + 82 + + @SuppressWarnings("unchecked") +
+ 86 + + + void shouldCloseEventSinkOnFailure() throws InterruptedException { +
+ + 83 + + void shouldCloseEventSinkOnFailure() { +
+ 87 + + 84 + + CompletionStage future = CompletableFuture.supplyAsync(() -> { +
+ 88 + + 85 + + throw new RuntimeException("failed to send message"); +
+ 89 + + 86 + + }); +
+
+ + + + diff.expandComplete + +
+
+
+ + + + diff.expandComplete + +
+
+ 91 + + 88 + + +
+ 92 + + 89 + + client.send(message); +
+ 93 + + 90 + + +
+ 94 + + + Thread.sleep(50L); +
+ 95 + + + +
+ 96 + + + verify(eventSink).close(); +
+ + 91 + + verify(eventSink, timeout(50L)).close(); +
+ 97 + + 92 + + } +
+ 98 + + 93 + + +
+ 99 + + 94 + + @Test +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + Main.java + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 1 + + import java.io.PrintStream; +
+ 1 + + 2 + + import java.util.Arrays; +
+ 2 + + 3 + + +
+ 3 + + 4 + + class Main { +
+ + 5 + + private static final PrintStream OUT = System.out; +
+ + 6 + + +
+ 4 + + 7 + + public static void main(String[] args) { +
+ + 8 + + <<<<<<< HEAD +
+ 5 + + 9 + + System.out.println("Expect nothing more to happen."); +
+ 6 + + 10 + + System.out.println("The command line parameters are:"); +
+ 7 + + 11 + + Arrays.stream(args).map(arg -> "- " + arg).forEach(System.out::println); +
+ + 12 + + ======= +
+ + 13 + + OUT.println("Expect nothing more to happen."); +
+ + 14 + + OUT.println("Parameters:"); +
+ + 15 + + Arrays.stream(args).map(arg -> "- " + arg).forEach(OUT::println); +
+ + 16 + + >>>>>>> feature/use_constant +
+ 8 + + 17 + + } +
+ 9 + + 18 + + } +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+`; + exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = `
ReactNode) => {story()}; + +const fileControlFactory: (changeset: Changeset) => FileControlFactory = changeset => file => { + const baseUrl = "/repo/hitchhiker/heartOfGold/code/changeset"; + const sourceLink = { + url: `${baseUrl}/${changeset.id}/${file.newPath}/`, + label: "Jump to source" + }; + const targetLink = changeset._embedded?.parents?.length === 1 && { + url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`, + label: "Jump to target" + }; + + const links = []; + switch (file.type) { + case "add": + links.push(sourceLink); + break; + case "delete": + if (targetLink) { + links.push(targetLink); + } + break; + default: + if (targetLink) { + links.push(sourceLink, targetLink); + } else { + links.push(sourceLink); + } + } + + return links.map(({ url, label }) => ); +}; + storiesOf("Diff", module) + .addDecorator(RoutingDecorator) .addDecorator(storyFn => {storyFn()}) .add("Default", () => ) .add("Side-By-Side", () => ) - .add("Collapsed", () => ) + .add("Collapsed", () => ) .add("File Controls", () => ( ; - }); + }) + .add("WithLinkToFile", () => ); diff --git a/scm-ui/ui-components/src/repos/Diff.tsx b/scm-ui/ui-components/src/repos/Diff.tsx index 17a165d877..5a0571e95f 100644 --- a/scm-ui/ui-components/src/repos/Diff.tsx +++ b/scm-ui/ui-components/src/repos/Diff.tsx @@ -23,13 +23,14 @@ */ import React from "react"; import DiffFile from "./DiffFile"; -import { DiffObjectProps, File } from "./DiffTypes"; +import { DiffObjectProps, File, FileControlFactory } from "./DiffTypes"; import Notification from "../Notification"; import { WithTranslation, withTranslation } from "react-i18next"; type Props = WithTranslation & DiffObjectProps & { diff: File[]; + fileControlFactory?: FileControlFactory; }; class Diff extends React.Component { diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 98fa9959e3..637128fae9 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -309,7 +309,11 @@ class DiffFile extends React.Component { if (file._links?.lines) { items.push(this.createHunkHeader(expandableHunk)); } else if (i > 0) { - items.push(); + items.push( + + + + ); } items.push( @@ -411,29 +415,31 @@ class DiffFile extends React.Component { } const collapseIcon = this.hasContent(file) ? : null; const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null; - const sideBySideToggle = - file.hunks && file.hunks.length > 0 ? ( - - - - {({ setCollapsed }) => ( - - this.toggleSideBySide(() => { - if (this.state.sideBySide) { - setCollapsed(true); - } - }) - } - /> - )} - - {fileControls} - - - ) : null; + const sideBySideToggle = file.hunks && file.hunks.length && ( + + {({ setCollapsed }) => ( + + this.toggleSideBySide(() => { + if (this.state.sideBySide) { + setCollapsed(true); + } + }) + } + /> + )} + + ); + const headerButtons = ( + + + {sideBySideToggle} + {fileControls} + + + ); let errorModal; if (expansionError) { @@ -463,7 +469,7 @@ class DiffFile extends React.Component { {this.renderChangeTag(file)} - {sideBySideToggle} + {headerButtons}
{body} diff --git a/scm-ui/ui-components/src/repos/JumpToFileButton.tsx b/scm-ui/ui-components/src/repos/JumpToFileButton.tsx new file mode 100644 index 0000000000..59052af3df --- /dev/null +++ b/scm-ui/ui-components/src/repos/JumpToFileButton.tsx @@ -0,0 +1,54 @@ +/* + * 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 styled from "styled-components"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import Tooltip from "../Tooltip"; +import Icon from "../Icon"; + +const Button = styled(Link)` + width: 50px; + cursor: pointer; + &:hover { + color: #33b2e8; + } +`; + +type Props = { + link: string; + tooltip: string; +}; + +const JumpToFileButton: FC = ({ link, tooltip }) => { + return ( + + + + ); +}; + +export default JumpToFileButton; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx index da34591fcd..5bffd9adc3 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx @@ -22,14 +22,16 @@ * SOFTWARE. */ import React from "react"; -import { Changeset, Link, Collection } from "@scm-manager/ui-types"; +import { Changeset, Collection, Link } from "@scm-manager/ui-types"; import LoadingDiff from "../LoadingDiff"; import Notification from "../../Notification"; import { WithTranslation, withTranslation } from "react-i18next"; +import { FileControlFactory } from "../DiffTypes"; type Props = WithTranslation & { changeset: Changeset; defaultCollapse?: boolean; + fileControlFactory?: FileControlFactory; }; export const isDiffSupported = (changeset: Collection) => { @@ -47,12 +49,19 @@ export const createUrl = (changeset: Collection) => { class ChangesetDiff extends React.Component { render() { - const { changeset, defaultCollapse, t } = this.props; + const { changeset, fileControlFactory, defaultCollapse, t } = this.props; if (!isDiffSupported(changeset)) { return {t("changeset.diffNotSupported")}; } else { const url = createUrl(changeset); - return ; + return ( + + ); } } } diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx index 46a30a6fd4..543de073db 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx @@ -101,7 +101,7 @@ class ChangesetRow extends React.Component { - + @@ -119,24 +119,24 @@ class ChangesetRow extends React.Component {

- +

- +

- + - + - + = ({ changeset }) => { ); } + return ( <> setOpen(!open)}> @@ -157,7 +160,7 @@ class ChangesetDetails extends React.Component { } render() { - const { changeset, repository, t } = this.props; + const { changeset, repository, fileControlFactory, t } = this.props; const { collapsed } = this.state; const description = changesets.parseDescription(changeset.description); @@ -238,7 +241,7 @@ class ChangesetDetails extends React.Component { /> } /> - + ); diff --git a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx index a57f2dfd22..fd9c55d32b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx @@ -35,11 +35,13 @@ import { isFetchChangesetPending } from "../modules/changesets"; import ChangesetDetails from "../components/changesets/ChangesetDetails"; +import { FileControlFactory } from "@scm-manager/ui-components"; type Props = WithTranslation & { id: string; changeset: Changeset; repository: Repository; + fileControlFactoryFactory?: (changeset: Changeset) => FileControlFactory; loading: boolean; error: Error; fetchChangesetIfNeeded: (repository: Repository, id: string) => void; @@ -60,7 +62,7 @@ class ChangesetView extends React.Component { } render() { - const { changeset, loading, error, t, repository } = this.props; + const { changeset, loading, error, t, repository, fileControlFactoryFactory } = this.props; if (error) { return ; @@ -68,7 +70,13 @@ class ChangesetView extends React.Component { if (!changeset || loading) return ; - return ; + return ( + + ); } } diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index f3d1aa6ac1..2981ecad28 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -23,21 +23,21 @@ */ import React from "react"; import { connect } from "react-redux"; -import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom"; +import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; -import { Repository } from "@scm-manager/ui-types"; +import { Changeset, Repository } from "@scm-manager/ui-types"; import { + CustomQueryFlexWrappedColumns, ErrorPage, Loading, NavLink, Page, - CustomQueryFlexWrappedColumns, PrimaryContentColumn, - SecondaryNavigationColumn, SecondaryNavigation, - SubNavigation, - StateMenuContextProvider + SecondaryNavigationColumn, + StateMenuContextProvider, + SubNavigation } from "@scm-manager/ui-components"; import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; import RepositoryDetails from "../components/RepositoryDetails"; @@ -53,6 +53,7 @@ import { getLinks, getRepositoriesLink } from "../../modules/indexResource"; import CodeOverview from "../codeSection/containers/CodeOverview"; import ChangesetView from "./ChangesetView"; import SourceExtensions from "../sources/containers/SourceExtensions"; +import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components"; type Props = RouteComponentProps & WithTranslation & { @@ -117,7 +118,7 @@ class RepositoryRoot extends React.Component { evaluateDestinationForCodeLink = () => { const { repository } = this.props; - let url = `${this.matchedUrl()}/code`; + const url = `${this.matchedUrl()}/code`; if (repository?._links?.sources) { return `${url}/sources/`; } @@ -153,6 +154,38 @@ class RepositoryRoot extends React.Component { redirectedUrl = url + "/info"; } + const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = changeset => file => { + const baseUrl = `${url}/code/sources`; + const sourceLink = { + url: `${baseUrl}/${changeset.id}/${file.newPath}/`, + label: t("diff.jumpToSource") + }; + const targetLink = changeset._embedded?.parents?.length === 1 && { + url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`, + label: t("diff.jumpToTarget") + }; + + const links = []; + switch (file.type) { + case "add": + links.push(sourceLink); + break; + case "delete": + if (targetLink) { + links.push(targetLink); + } + break; + default: + if (targetLink) { + links.push(targetLink, sourceLink); // Target link first because its the previous file + } else { + links.push(sourceLink); + } + } + + return links.map(({ url, label }) => ); + }; + return ( { } + render={() => ( + + )} />