From b9458f47e9550901ed6c1f852b7960d0ea44915c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 20 Nov 2018 11:04:30 +0100 Subject: [PATCH 01/27] Add support for custom violation exceptions --- .../scm/ScmConstraintViolationException.java | 76 +++++++++++++++++++ ...=> ResteasyValidationExceptionMapper.java} | 8 +- ...cmConstraintValidationExceptionMapper.java | 30 ++++++++ .../scm/api/v2/resources/MapperModule.java | 3 +- ...syViolationExceptionToErrorDtoMapper.java} | 2 +- ...ScmViolationExceptionToErrorDtoMapper.java | 53 +++++++++++++ 6 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java rename scm-webapp/src/main/java/sonia/scm/api/v2/{ValidationExceptionMapper.java => ResteasyValidationExceptionMapper.java} (62%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{ViolationExceptionToErrorDtoMapper.java => ResteasyViolationExceptionToErrorDtoMapper.java} (96%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java new file mode 100644 index 0000000000..1fb31dc0fc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -0,0 +1,76 @@ +package sonia.scm; + +import java.util.ArrayList; +import java.util.Collection; + +import static java.util.Collections.unmodifiableCollection; + +public class ScmConstraintViolationException extends RuntimeException { + + private final Collection violations; + + private final String furtherInformations; + + private ScmConstraintViolationException(Collection violations, String furtherInformations) { + this.violations = violations; + this.furtherInformations = furtherInformations; + } + + public Collection getViolations() { + return unmodifiableCollection(violations); + } + + public String getUrl() { + return furtherInformations; + } + + public static class Builder { + private final Collection violations = new ArrayList<>(); + private String furtherInformations; + + public static Builder doThrow() { + Builder builder = new Builder(); + return builder; + } + + public Builder andThrow() { + this.violations.clear(); + this.furtherInformations = null; + return this; + } + + public Builder violation(String message, String... pathElements) { + this.violations.add(new ScmConstraintViolation(message, pathElements)); + return this; + } + + public Builder withFurtherInformations(String furtherInformations) { + this.furtherInformations = furtherInformations; + return this; + } + + public void when(boolean condition) { + if (condition && !this.violations.isEmpty()) { + throw new ScmConstraintViolationException(violations, furtherInformations); + } + } + } + + public static class ScmConstraintViolation { + private final String message; + private final String path; + + private ScmConstraintViolation(String message, String... pathElements) { + this.message = message; + this.path = String.join(".", pathElements); + } + + public String getMessage() { + return message; + } + + public String getPropertyPath() { + return path; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java similarity index 62% rename from scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java index 6fadce8500..63582e10b8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2; import org.jboss.resteasy.api.validation.ResteasyViolationException; -import sonia.scm.api.v2.resources.ViolationExceptionToErrorDtoMapper; +import sonia.scm.api.v2.resources.ResteasyViolationExceptionToErrorDtoMapper; import javax.inject.Inject; import javax.ws.rs.core.MediaType; @@ -10,12 +10,12 @@ import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class ValidationExceptionMapper implements ExceptionMapper { +public class ResteasyValidationExceptionMapper implements ExceptionMapper { - private final ViolationExceptionToErrorDtoMapper mapper; + private final ResteasyViolationExceptionToErrorDtoMapper mapper; @Inject - public ValidationExceptionMapper(ViolationExceptionToErrorDtoMapper mapper) { + public ResteasyValidationExceptionMapper(ResteasyViolationExceptionToErrorDtoMapper mapper) { this.mapper = mapper; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java new file mode 100644 index 0000000000..991aeedaeb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java @@ -0,0 +1,30 @@ +package sonia.scm.api.v2; + +import sonia.scm.ScmConstraintViolationException; +import sonia.scm.api.v2.resources.ScmViolationExceptionToErrorDtoMapper; + +import javax.inject.Inject; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class ScmConstraintValidationExceptionMapper implements ExceptionMapper { + + private final ScmViolationExceptionToErrorDtoMapper mapper; + + @Inject + public ScmConstraintValidationExceptionMapper(ScmViolationExceptionToErrorDtoMapper mapper) { + this.mapper = mapper; + } + + @Override + public Response toResponse(ScmConstraintViolationException exception) { + return Response + .status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(mapper.map(exception)) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 35f58cef90..e6cf6721a5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -39,7 +39,8 @@ public class MapperModule extends AbstractModule { bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); - bind(ViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ViolationExceptionToErrorDtoMapper.class).getClass()); + bind(ResteasyViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ResteasyViolationExceptionToErrorDtoMapper.class).getClass()); + bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass()); bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); // no mapstruct required diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResteasyViolationExceptionToErrorDtoMapper.java similarity index 96% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResteasyViolationExceptionToErrorDtoMapper.java index e713b031f7..7bab2d4272 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResteasyViolationExceptionToErrorDtoMapper.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.stream.Collectors; @Mapper -public abstract class ViolationExceptionToErrorDtoMapper { +public abstract class ResteasyViolationExceptionToErrorDtoMapper { @Mapping(target = "errorCode", ignore = true) @Mapping(target = "transactionId", ignore = true) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java new file mode 100644 index 0000000000..3828134cc1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java @@ -0,0 +1,53 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.slf4j.MDC; +import sonia.scm.ScmConstraintViolationException; +import sonia.scm.ScmConstraintViolationException.ScmConstraintViolation; + +import java.util.List; +import java.util.stream.Collectors; + +@Mapper +public abstract class ScmViolationExceptionToErrorDtoMapper { + + @Mapping(target = "errorCode", ignore = true) + @Mapping(target = "transactionId", ignore = true) + @Mapping(target = "context", ignore = true) + public abstract ErrorDto map(ScmConstraintViolationException exception); + + @AfterMapping + void setTransactionId(@MappingTarget ErrorDto dto) { + dto.setTransactionId(MDC.get("transaction_id")); + } + + @AfterMapping + void mapViolations(ScmConstraintViolationException exception, @MappingTarget ErrorDto dto) { + List violations = + exception.getViolations() + .stream() + .map(this::createViolationDto) + .collect(Collectors.toList()); + dto.setViolations(violations); + } + + private ErrorDto.ConstraintViolationDto createViolationDto(ScmConstraintViolation violation) { + ErrorDto.ConstraintViolationDto constraintViolationDto = new ErrorDto.ConstraintViolationDto(); + constraintViolationDto.setMessage(violation.getMessage()); + constraintViolationDto.setPath(violation.getPropertyPath()); + return constraintViolationDto; + } + + @AfterMapping + void setErrorCode(@MappingTarget ErrorDto dto) { + dto.setErrorCode("3zR9vPNIE1"); + } + + @AfterMapping + void setMessage(@MappingTarget ErrorDto dto) { + dto.setMessage("input violates conditions (see violation list)"); + } +} From 3027ccb97569af23c1226eb281dee6a9fad562a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Wed, 21 Nov 2018 16:55:13 +0100 Subject: [PATCH 02/27] added translation + dummy historylink --- scm-ui/public/locales/en/repos.json | 8 ++++- .../src/repos/sources/containers/Content.js | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index d4fd950c45..509c0f5565 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -55,7 +55,13 @@ "branch": "Branch" }, "content": { - "downloadButton": "Download" + "historyLink": "History", + "downloadButton": "Download", + "path": "Path", + "branch": "Branch", + "lastModified": "Last modified", + "description": "Description", + "size": "Size" } }, "changesets": { diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index 0b764aa6a1..a03d801f55 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -2,11 +2,11 @@ import React from "react"; import { translate } from "react-i18next"; import { getSources } from "../modules/sources"; -import type { Repository, File } from "@scm-manager/ui-types"; +import type { File, Repository } from "@scm-manager/ui-types"; import { + DateFromNow, ErrorNotification, - Loading, - DateFromNow + Loading } from "@scm-manager/ui-components"; import { connect } from "react-redux"; import ImageViewer from "../components/content/ImageViewer"; @@ -87,10 +87,9 @@ class Content extends React.Component { }; showHeader() { - const { file, classes } = this.props; + const { file, classes, t } = this.props; const collapsed = this.state.collapsed; const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; - const fileSize = file.directory ? "" : ; return ( @@ -101,7 +100,11 @@ class Content extends React.Component {
{file.name}
-

{fileSize}

+

+ + {t("sources.content.historyLink")} + +

); @@ -109,7 +112,7 @@ class Content extends React.Component { showMoreInformation() { const collapsed = this.state.collapsed; - const { classes, file, revision } = this.props; + const { classes, file, revision, t } = this.props; const date = ; const description = file.description ? (

@@ -123,25 +126,30 @@ class Content extends React.Component { })}

) : null; + const fileSize = file.directory ? "" : ; if (!collapsed) { return (
- + - + - + + + + + - + From c5ce7b049b4d5b15de7e0beba880a12ea21f1c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 22 Nov 2018 09:57:56 +0100 Subject: [PATCH 03/27] add icon --- scm-ui/src/repos/sources/containers/Content.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index a03d801f55..45c81cb54d 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -95,13 +95,17 @@ class Content extends React.Component {
- +
{file.name}

+ + + + {t("sources.content.historyLink")}

From 6357ec77e27ce68d30791b3b89a670246f0f448a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 22 Nov 2018 10:26:13 +0100 Subject: [PATCH 04/27] addHistory --- scm-ui/src/repos/containers/RepositoryRoot.js | 26 ++++++++++---- .../src/repos/sources/containers/Content.js | 13 +++---- .../repos/sources/containers/FileHistory.js | 36 +++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 scm-ui/src/repos/sources/containers/FileHistory.js diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 94768d2b6a..6831fde68f 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -29,13 +29,14 @@ import Permissions from "../permissions/containers/Permissions"; import type { History } from "history"; import EditNavLink from "../components/EditNavLink"; +import FileHistory from "../sources/containers/FileHistory"; import BranchRoot from "./ChangesetsRoot"; import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; import { getRepositoriesLink } from "../../modules/indexResource"; -import {ExtensionPoint} from "@scm-manager/ui-extensions"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { namespace: string, @@ -152,6 +153,15 @@ class RepositoryRoot extends React.Component { )} /> + ( + + )} + /> ( @@ -172,9 +182,10 @@ class RepositoryRoot extends React.Component { /> )} /> - @@ -197,9 +208,10 @@ class RepositoryRoot extends React.Component { label={t("repository-root.sources")} activeOnlyWhenExact={false} /> - { diff --git a/scm-ui/src/repos/sources/containers/FileHistory.js b/scm-ui/src/repos/sources/containers/FileHistory.js new file mode 100644 index 0000000000..c5f4c66936 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/FileHistory.js @@ -0,0 +1,36 @@ +//@flow + +import React from "react"; +import { translate } from "react-i18next"; +import type { File, Repository } from "@scm-manager/ui-types"; +import { + DateFromNow, + ErrorNotification, + Loading +} from "@scm-manager/ui-components"; +import { connect } from "react-redux"; +import ImageViewer from "../components/content/ImageViewer"; +import SourcecodeViewer from "../components/content/SourcecodeViewer"; +import DownloadViewer from "../components/content/DownloadViewer"; +import FileSize from "../components/FileSize"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { getContentType } from "./contentType"; + +type Props = { + classes: any, + t: string => string +}; + +class FileHistory extends React.Component { + componentDidMount() {} + + render() { + return "History"; + } +} + +const mapStateToProps = (state: any, ownProps: Props) => {}; + +export default connect(mapStateToProps)(translate("repos")(FileHistory)); From 2f9d0f279304c7dc2a20051dbd240807febae480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 26 Nov 2018 15:20:44 +0100 Subject: [PATCH 05/27] add history and sources button to chose between view --- scm-ui/public/locales/en/repos.json | 3 +- .../sources/components/content/ButtonGroup.js | 72 ++++++++++ .../src/repos/sources/containers/Content.js | 136 ++++++------------ .../repos/sources/containers/HistoryView.js | 75 ++++++++++ .../repos/sources/containers/SourcesView.js | 100 +++++++++++++ 5 files changed, 296 insertions(+), 90 deletions(-) create mode 100644 scm-ui/src/repos/sources/components/content/ButtonGroup.js create mode 100644 scm-ui/src/repos/sources/containers/HistoryView.js create mode 100644 scm-ui/src/repos/sources/containers/SourcesView.js diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 509c0f5565..7e421e7b99 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -55,7 +55,8 @@ "branch": "Branch" }, "content": { - "historyLink": "History", + "historyButton": "History", + "sourcesButton": "Sources", "downloadButton": "Download", "path": "Path", "branch": "Branch", diff --git a/scm-ui/src/repos/sources/components/content/ButtonGroup.js b/scm-ui/src/repos/sources/components/content/ButtonGroup.js new file mode 100644 index 0000000000..5befbd94d5 --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/ButtonGroup.js @@ -0,0 +1,72 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Button } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + historyIsSelected: boolean, + showHistory: boolean => void +}; + +class ButtonGroup extends React.Component { + showHistory = () => { + this.props.showHistory(true); + }; + + showSources = () => { + this.props.showHistory(false); + }; + + render() { + const { t, historyIsSelected } = this.props; + + let sourcesColor = ""; + let historyColor = ""; + + if (historyIsSelected) { + historyColor = "info is-selected"; + } else { + sourcesColor = "info is-selected"; + } + + const sourcesLabel = ( + <> + + + + + {t("sources.content.sourcesButton")} + + + ); + + const historyLabel = ( + <> + + + + + {t("sources.content.historyButton")} + + + ); + + return ( +
+
+ ); + } +} + +export default translate("repos")(ButtonGroup); diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index e474c111f0..7202dbe928 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -1,22 +1,16 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { getSources } from "../modules/sources"; import type { File, Repository } from "@scm-manager/ui-types"; -import { - DateFromNow, - ErrorNotification, - Loading -} from "@scm-manager/ui-components"; -import { connect } from "react-redux"; -import ImageViewer from "../components/content/ImageViewer"; -import SourcecodeViewer from "../components/content/SourcecodeViewer"; -import DownloadViewer from "../components/content/DownloadViewer"; +import { DateFromNow } from "@scm-manager/ui-components"; import FileSize from "../components/FileSize"; import injectSheet from "react-jss"; import classNames from "classnames"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; -import { getContentType } from "./contentType"; +import ButtonGroup from "../components/content/ButtonGroup"; +import SourcesView from "./SourcesView"; +import HistoryView from "./HistoryView"; +import { getSources } from "../modules/sources"; +import { connect } from "react-redux"; type Props = { loading: boolean, @@ -30,11 +24,8 @@ type Props = { }; type State = { - contentType: string, - language: string, - loaded: boolean, collapsed: boolean, - error?: Error + showHistory: boolean }; const styles = { @@ -43,6 +34,9 @@ const styles = { }, pointer: { cursor: "pointer" + }, + marginInHeader: { + marginRight: "0.5em" } }; @@ -51,65 +45,50 @@ class Content extends React.Component { super(props); this.state = { - contentType: "", - language: "", - loaded: false, - collapsed: true + collapsed: true, + showHistory: false }; } - componentDidMount() { - const { file } = this.props; - getContentType(file._links.self.href) - .then(result => { - if (result.error) { - this.setState({ - ...this.state, - error: result.error, - loaded: true - }); - } else { - this.setState({ - ...this.state, - contentType: result.type, - language: result.language, - loaded: true - }); - } - }) - .catch(err => {}); - } - toggleCollapse = () => { this.setState(prevState => ({ collapsed: !prevState.collapsed })); }; + setShowHistoryState(showHistory: boolean) { + this.setState({ + ...this.state, + showHistory + }); + } + showHeader() { const { file, classes, t } = this.props; - const collapsed = this.state.collapsed; + const { showHistory, collapsed } = this.state; const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; return ( - + ); @@ -165,40 +144,19 @@ class Content extends React.Component { return null; } - showContent() { - const { file, revision } = this.props; - const { contentType, language } = this.state; - if (contentType.startsWith("image/")) { - return ; - } else if (language) { - return ; - } else if (contentType.startsWith("text/")) { - return ; - } else { - return ( - - - - ); - } - } - render() { - const { file, classes } = this.props; - const { loaded, error } = this.state; - - if (!file || !loaded) { - return ; - } - if (error) { - return ; - } + const { file, revision, repository, path, classes } = this.props; + const {showHistory} = this.state; const header = this.showHeader(); - const content = this.showContent(); + const content = showHistory ? : ( + + ); const moreInformation = this.showMoreInformation(); return ( diff --git a/scm-ui/src/repos/sources/containers/HistoryView.js b/scm-ui/src/repos/sources/containers/HistoryView.js new file mode 100644 index 0000000000..75c5ab3b78 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/HistoryView.js @@ -0,0 +1,75 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; + +import { getContentType } from "./contentType"; +import type { File, Repository } from "@scm-manager/ui-types"; +import { ErrorNotification, Loading } from "@scm-manager/ui-components"; + +type Props = { + repository: Repository, + file: File, + revision: string, + path: string, + classes: any, + t: string => string +}; + +type State = { + loaded: boolean, + error?: Error +}; + +class HistoryView extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + loaded: false + }; + } + + componentDidMount() { + const { file } = this.props; + /* getContentType(file._links.self.href) + .then(result => { + if (result.error) { + this.setState({ + ...this.state, + error: result.error, + loaded: true + }); + } else { + this.setState({ + ...this.state, + contentType: result.type, + language: result.language, + loaded: true + }); + } + }) + .catch(err => {});*/ + } + + showHistory() { + return "Hallo"; + } + + render() { + const { classes, file } = this.props; + const { loaded, error } = this.state; + + if (!file || !loaded) { + return ; + } + if (error) { + return ; + } + + const history = this.showHistory(); + + return <>{history}; + } +} + +export default translate("repos")(HistoryView); diff --git a/scm-ui/src/repos/sources/containers/SourcesView.js b/scm-ui/src/repos/sources/containers/SourcesView.js new file mode 100644 index 0000000000..533d4bd2eb --- /dev/null +++ b/scm-ui/src/repos/sources/containers/SourcesView.js @@ -0,0 +1,100 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; + +import SourcecodeViewer from "../components/content/SourcecodeViewer"; +import ImageViewer from "../components/content/ImageViewer"; +import DownloadViewer from "../components/content/DownloadViewer"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { getContentType } from "./contentType"; +import type { File, Repository } from "@scm-manager/ui-types"; +import { ErrorNotification, Loading } from "@scm-manager/ui-components"; + +type Props = { + repository: Repository, + file: File, + revision: string, + path: string, + classes: any, + t: string => string +}; + +type State = { + contentType: string, + language: string, + loaded: boolean, + error?: Error +}; + +class SourcesView extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + contentType: "", + language: "", + loaded: false + }; + } + + componentDidMount() { + const { file } = this.props; + getContentType(file._links.self.href) + .then(result => { + if (result.error) { + this.setState({ + ...this.state, + error: result.error, + loaded: true + }); + } else { + this.setState({ + ...this.state, + contentType: result.type, + language: result.language, + loaded: true + }); + } + }) + .catch(err => {}); + } + + showSources() { + const { file, revision } = this.props; + const { contentType, language } = this.state; + if (contentType.startsWith("image/")) { + return ; + } else if (language) { + return ; + } else if (contentType.startsWith("text/")) { + return ; + } else { + return ( + + + + ); + } + } + + render() { + const { classes, file } = this.props; + const { loaded, error } = this.state; + + if (!file || !loaded) { + return ; + } + if (error) { + return ; + } + + const sources = this.showSources(); + + return <>{sources}; + } +} + +export default translate("repos")(SourcesView); From 89291cdf46cda16275c04aff5a7355de02cbb4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 26 Nov 2018 15:56:41 +0100 Subject: [PATCH 06/27] show history table of content --- scm-ui/src/containers/Main.js | 1 - .../src/repos/sources/containers/Content.js | 8 ++-- .../repos/sources/containers/FileHistory.js | 14 ------- .../repos/sources/containers/HistoryView.js | 34 ++++++++-------- .../repos/sources/containers/SourcesView.js | 9 ++--- .../src/repos/sources/containers/history.js | 16 ++++++++ .../repos/sources/containers/history.test.js | 39 +++++++++++++++++++ 7 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 scm-ui/src/repos/sources/containers/history.js create mode 100644 scm-ui/src/repos/sources/containers/history.test.js diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 4846c503aa..4bbdf6812a 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -19,7 +19,6 @@ import SingleGroup from "../groups/containers/SingleGroup"; import AddGroup from "../groups/containers/AddGroup"; import Config from "../config/containers/Config"; -import ChangeUserPassword from "./ChangeUserPassword"; import Profile from "./Profile"; type Props = { diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index 7202dbe928..ed62e67609 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -64,7 +64,7 @@ class Content extends React.Component { } showHeader() { - const { file, classes, t } = this.props; + const { file, classes } = this.props; const { showHistory, collapsed } = this.state; const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; @@ -146,10 +146,12 @@ class Content extends React.Component { render() { const { file, revision, repository, path, classes } = this.props; - const {showHistory} = this.state; + const { showHistory } = this.state; const header = this.showHeader(); - const content = showHistory ? : ( + const content = showHistory ? ( + + ) : ( string + repository: Repository }; type State = { loaded: boolean, + changesets: Changeset[], error?: Error }; @@ -25,13 +21,14 @@ class HistoryView extends React.Component { super(props); this.state = { - loaded: false + loaded: false, + changesets: [] }; } componentDidMount() { const { file } = this.props; - /* getContentType(file._links.self.href) + getHistory(file._links.history.href) .then(result => { if (result.error) { this.setState({ @@ -42,21 +39,22 @@ class HistoryView extends React.Component { } else { this.setState({ ...this.state, - contentType: result.type, - language: result.language, - loaded: true + loaded: true, + changesets: result.changesets }); } }) - .catch(err => {});*/ + .catch(err => {}); } showHistory() { - return "Hallo"; + const { repository } = this.props; + const { changesets } = this.state; + return ; } render() { - const { classes, file } = this.props; + const { file } = this.props; const { loaded, error } = this.state; if (!file || !loaded) { @@ -72,4 +70,4 @@ class HistoryView extends React.Component { } } -export default translate("repos")(HistoryView); +export default (HistoryView); diff --git a/scm-ui/src/repos/sources/containers/SourcesView.js b/scm-ui/src/repos/sources/containers/SourcesView.js index 533d4bd2eb..1e76c6d6bf 100644 --- a/scm-ui/src/repos/sources/containers/SourcesView.js +++ b/scm-ui/src/repos/sources/containers/SourcesView.js @@ -1,6 +1,5 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; import SourcecodeViewer from "../components/content/SourcecodeViewer"; import ImageViewer from "../components/content/ImageViewer"; @@ -14,9 +13,7 @@ type Props = { repository: Repository, file: File, revision: string, - path: string, - classes: any, - t: string => string + path: string }; type State = { @@ -81,7 +78,7 @@ class SourcesView extends React.Component { } render() { - const { classes, file } = this.props; + const { file } = this.props; const { loaded, error } = this.state; if (!file || !loaded) { @@ -97,4 +94,4 @@ class SourcesView extends React.Component { } } -export default translate("repos")(SourcesView); +export default SourcesView; diff --git a/scm-ui/src/repos/sources/containers/history.js b/scm-ui/src/repos/sources/containers/history.js new file mode 100644 index 0000000000..7135706f9b --- /dev/null +++ b/scm-ui/src/repos/sources/containers/history.js @@ -0,0 +1,16 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; + +export function getHistory(url: string) { + return apiClient + .get(url) + .then(response => response.json()) + .then(result => { + return { + changesets: result._embedded.changesets + }; + }) + .catch(err => { + return { error: err }; + }); +} diff --git a/scm-ui/src/repos/sources/containers/history.test.js b/scm-ui/src/repos/sources/containers/history.test.js new file mode 100644 index 0000000000..f37567cd79 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/history.test.js @@ -0,0 +1,39 @@ +//@flow +import fetchMock from "fetch-mock"; +import { getHistory } from "./history"; + +describe("get content type", () => { + const FILE_URL = "/repositories/scmadmin/TestRepo/history/file"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should return history", done => { + let changesets: { + changesets: [ + { + id: "1234" + }, + { + id: "2345" + } + ] + }; + let history = { + _embedded: { + changesets + } + }; + + fetchMock.get("/api/v2" + FILE_URL, { + history + }); + + getHistory(FILE_URL).then(content => { + expect(content.changesets).toBe(changesets); + done(); + }); + }); +}); From b38b2d8543c4544b464588e4bdfbec30ab760799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 26 Nov 2018 16:39:26 +0100 Subject: [PATCH 07/27] add dummy paginator --- .../repos/sources/containers/FileHistory.js | 22 ------------ .../repos/sources/containers/HistoryView.js | 30 ++++++++++++---- .../src/repos/sources/containers/history.js | 8 ++++- .../repos/sources/containers/history.test.js | 35 ++++++++++--------- 4 files changed, 50 insertions(+), 45 deletions(-) delete mode 100644 scm-ui/src/repos/sources/containers/FileHistory.js diff --git a/scm-ui/src/repos/sources/containers/FileHistory.js b/scm-ui/src/repos/sources/containers/FileHistory.js deleted file mode 100644 index 0551ff8239..0000000000 --- a/scm-ui/src/repos/sources/containers/FileHistory.js +++ /dev/null @@ -1,22 +0,0 @@ -//@flow - -import React from "react"; -import { translate } from "react-i18next"; -import { connect } from "react-redux"; - -type Props = { - classes: any, - t: string => string -}; - -class FileHistory extends React.Component { - componentDidMount() {} - - render() { - return "History"; - } -} - -const mapStateToProps = (state: any, ownProps: Props) => {}; - -export default connect(mapStateToProps)(translate("repos")(FileHistory)); diff --git a/scm-ui/src/repos/sources/containers/HistoryView.js b/scm-ui/src/repos/sources/containers/HistoryView.js index e68cbe319b..bdfd8596d2 100644 --- a/scm-ui/src/repos/sources/containers/HistoryView.js +++ b/scm-ui/src/repos/sources/containers/HistoryView.js @@ -1,7 +1,16 @@ // @flow import React from "react"; -import type { File, Changeset, Repository } from "@scm-manager/ui-types"; -import { ErrorNotification, Loading } from "@scm-manager/ui-components"; +import type { + File, + Changeset, + Repository, + PagedCollection +} from "@scm-manager/ui-types"; +import { + ErrorNotification, + Loading, + LinkPaginator +} from "@scm-manager/ui-components"; import { getHistory } from "./history"; import ChangesetList from "../../components/changesets/ChangesetList"; @@ -13,6 +22,8 @@ type Props = { type State = { loaded: boolean, changesets: Changeset[], + page: number, + pageCollection?: PagedCollection, error?: Error }; @@ -22,6 +33,7 @@ class HistoryView extends React.Component { this.state = { loaded: false, + page: 0, changesets: [] }; } @@ -40,7 +52,8 @@ class HistoryView extends React.Component { this.setState({ ...this.state, loaded: true, - changesets: result.changesets + changesets: result.changesets, + pageCollection: result.pageCollection }); } }) @@ -49,8 +62,13 @@ class HistoryView extends React.Component { showHistory() { const { repository } = this.props; - const { changesets } = this.state; - return ; + const { changesets, page, pageCollection } = this.state; + return ( + <> + + + + ); } render() { @@ -70,4 +88,4 @@ class HistoryView extends React.Component { } } -export default (HistoryView); +export default HistoryView; diff --git a/scm-ui/src/repos/sources/containers/history.js b/scm-ui/src/repos/sources/containers/history.js index 7135706f9b..1c58523407 100644 --- a/scm-ui/src/repos/sources/containers/history.js +++ b/scm-ui/src/repos/sources/containers/history.js @@ -7,7 +7,13 @@ export function getHistory(url: string) { .then(response => response.json()) .then(result => { return { - changesets: result._embedded.changesets + changesets: result._embedded.changesets, + pageCollection: { + _embedded: result._embedded, + _links: result._links, + page: result.page, + pageTotal: result.pageTotal + } }; }) .catch(err => { diff --git a/scm-ui/src/repos/sources/containers/history.test.js b/scm-ui/src/repos/sources/containers/history.test.js index f37567cd79..960d59bef6 100644 --- a/scm-ui/src/repos/sources/containers/history.test.js +++ b/scm-ui/src/repos/sources/containers/history.test.js @@ -11,28 +11,31 @@ describe("get content type", () => { }); it("should return history", done => { - let changesets: { - changesets: [ + fetchMock.get("/api/v2" + FILE_URL, { + page: 0, + pageTotal: 1, + _embedded: { + changesets: [ + { + id: "1234" + }, + { + id: "2345" + } + ] + } + }); + + getHistory(FILE_URL).then(content => { + expect(content.changesets).toEqual([ { id: "1234" }, { id: "2345" } - ] - }; - let history = { - _embedded: { - changesets - } - }; - - fetchMock.get("/api/v2" + FILE_URL, { - history - }); - - getHistory(FILE_URL).then(content => { - expect(content.changesets).toBe(changesets); + ]); + expect(content.pageCollection.page).toEqual(0); done(); }); }); From 6fbc154fd2883ab0cbc0498e5e4c50ae49464cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 26 Nov 2018 16:48:25 +0100 Subject: [PATCH 08/27] correct page number --- scm-ui/src/repos/containers/RepositoryRoot.js | 10 ---------- scm-ui/src/repos/sources/containers/HistoryView.js | 5 +++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 6831fde68f..81de0296fc 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -29,7 +29,6 @@ import Permissions from "../permissions/containers/Permissions"; import type { History } from "history"; import EditNavLink from "../components/EditNavLink"; -import FileHistory from "../sources/containers/FileHistory"; import BranchRoot from "./ChangesetsRoot"; import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; @@ -153,15 +152,6 @@ class RepositoryRoot extends React.Component { )} /> - ( - - )} - /> ( diff --git a/scm-ui/src/repos/sources/containers/HistoryView.js b/scm-ui/src/repos/sources/containers/HistoryView.js index bdfd8596d2..c3cb423ef6 100644 --- a/scm-ui/src/repos/sources/containers/HistoryView.js +++ b/scm-ui/src/repos/sources/containers/HistoryView.js @@ -33,7 +33,7 @@ class HistoryView extends React.Component { this.state = { loaded: false, - page: 0, + page: 1, changesets: [] }; } @@ -53,7 +53,8 @@ class HistoryView extends React.Component { ...this.state, loaded: true, changesets: result.changesets, - pageCollection: result.pageCollection + pageCollection: result.pageCollection, + page: result.pageCollection.page + 1 }); } }) From 338d0657086d423ba850bf088607aa3fd557d9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Nov 2018 14:20:28 +0100 Subject: [PATCH 09/27] Fix serialization errors --- .../java/sonia/scm/ExceptionWithContext.java | 2 ++ .../java/sonia/scm/NotFoundException.java | 2 ++ .../scm/ScmConstraintViolationException.java | 28 +++++++++++-------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java index dd87b77210..8702567880 100644 --- a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -6,6 +6,8 @@ import static java.util.Collections.unmodifiableList; public abstract class ExceptionWithContext extends RuntimeException { + private static final long serialVersionUID = 4327413456580409224L; + private final List context; public ExceptionWithContext(List context, String message) { diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 69b9617e93..9c478da855 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -7,6 +7,8 @@ import static java.util.stream.Collectors.joining; public class NotFoundException extends ExceptionWithContext { + private static final long serialVersionUID = 1710455380886499111L; + private static final String CODE = "AGR7UzkhA1"; public NotFoundException(Class type, String id) { diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java index 1fb31dc0fc..d50a1c7591 100644 --- a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -1,19 +1,22 @@ package sonia.scm; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import static java.util.Collections.unmodifiableCollection; -public class ScmConstraintViolationException extends RuntimeException { +public class ScmConstraintViolationException extends RuntimeException implements Serializable { + + private static final long serialVersionUID = 6904534307450229887L; private final Collection violations; - private final String furtherInformations; + private final String furtherInformation; - private ScmConstraintViolationException(Collection violations, String furtherInformations) { + private ScmConstraintViolationException(Collection violations, String furtherInformation) { this.violations = violations; - this.furtherInformations = furtherInformations; + this.furtherInformation = furtherInformation; } public Collection getViolations() { @@ -21,12 +24,12 @@ public class ScmConstraintViolationException extends RuntimeException { } public String getUrl() { - return furtherInformations; + return furtherInformation; } public static class Builder { private final Collection violations = new ArrayList<>(); - private String furtherInformations; + private String furtherInformation; public static Builder doThrow() { Builder builder = new Builder(); @@ -35,7 +38,7 @@ public class ScmConstraintViolationException extends RuntimeException { public Builder andThrow() { this.violations.clear(); - this.furtherInformations = null; + this.furtherInformation = null; return this; } @@ -44,19 +47,22 @@ public class ScmConstraintViolationException extends RuntimeException { return this; } - public Builder withFurtherInformations(String furtherInformations) { - this.furtherInformations = furtherInformations; + public Builder withFurtherInformation(String furtherInformation) { + this.furtherInformation = furtherInformation; return this; } public void when(boolean condition) { if (condition && !this.violations.isEmpty()) { - throw new ScmConstraintViolationException(violations, furtherInformations); + throw new ScmConstraintViolationException(violations, furtherInformation); } } } - public static class ScmConstraintViolation { + public static class ScmConstraintViolation implements Serializable { + + private static final long serialVersionUID = -6900317468157084538L; + private final String message; private final String path; From 82c8b86386ebf4a7723b2629768ac3d3c7f9f93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Nov 2018 14:23:26 +0100 Subject: [PATCH 10/27] Fix sonar issue --- .../main/java/sonia/scm/ScmConstraintViolationException.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java index d50a1c7591..01c628e3dd 100644 --- a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -32,8 +32,7 @@ public class ScmConstraintViolationException extends RuntimeException implements private String furtherInformation; public static Builder doThrow() { - Builder builder = new Builder(); - return builder; + return new Builder(); } public Builder andThrow() { From 1401b024a1002b218adf76b12af9e06113c6000d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Wed, 28 Nov 2018 08:53:48 +0100 Subject: [PATCH 11/27] update page in state for using paginator --- .../components/content/StatePaginator.js | 138 ++++++++++++++++++ .../repos/sources/containers/HistoryView.js | 29 +++- 2 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 scm-ui/src/repos/sources/components/content/StatePaginator.js diff --git a/scm-ui/src/repos/sources/components/content/StatePaginator.js b/scm-ui/src/repos/sources/components/content/StatePaginator.js new file mode 100644 index 0000000000..ceedd5edaf --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/StatePaginator.js @@ -0,0 +1,138 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { PagedCollection } from "@scm-manager/ui-types"; +import { Button } from "@scm-manager/ui-components"; + +type Props = { + collection: PagedCollection, + page: number, + updatePage: number => void, + + // context props + t: string => string +}; + +class StatePaginator extends React.Component { + renderFirstButton() { + return ( +
Path{t("sources.content.path")} {file.path}
Branch{t("sources.content.branch")} {revision}
Last modified{t("sources.content.size")}{fileSize}
{t("sources.content.lastModified")} {date}
Description{t("sources.content.description")} {description}