mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-06 20:30:52 +01:00
Merge pull request #1376 from scm-manager/feature/fullscreen
Feature/fullscreen
This commit is contained in:
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## Unreleased
|
||||
### Added
|
||||
- Generation of email addresses for users, where none is configured ([#1370](https://github.com/scm-manager/scm-manager/pull/1370))
|
||||
- Source code fullscreen view ([#1376](https://github.com/scm-manager/scm-manager/pull/1376))
|
||||
|
||||
### Fixed
|
||||
- Missing default permission to manage public gpg keys ([#1377](https://github.com/scm-manager/scm-manager/pull/1377))
|
||||
@@ -370,3 +371,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[2.6.1]: https://www.scm-manager.org/download/2.6.1
|
||||
[2.6.2]: https://www.scm-manager.org/download/2.6.2
|
||||
[2.6.3]: https://www.scm-manager.org/download/2.6.3
|
||||
[2.7.0]: https://www.scm-manager.org/download/2.7.0
|
||||
[2.7.1]: https://www.scm-manager.org/download/2.7.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
63
scm-ui/ui-components/src/buttons/OpenInFullscreenButton.tsx
Normal file
63
scm-ui/ui-components/src/buttons/OpenInFullscreenButton.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 * as React from "react";
|
||||
import { FC, ReactNode, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import FullscreenModal from "../modals/FullscreenModal";
|
||||
|
||||
type Props = {
|
||||
modalTitle: string;
|
||||
modalBody: ReactNode;
|
||||
};
|
||||
|
||||
const Button = styled.a`
|
||||
width: 50px;
|
||||
&:hover {
|
||||
color: #33b2e8;
|
||||
}
|
||||
`;
|
||||
|
||||
const OpenInFullscreenButton: FC<Props> = ({ modalTitle, modalBody }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button title={t("diff.fullscreen.open")} className="button" onClick={() => setShowModal(true)}>
|
||||
<i className="fas fa-search-plus" />
|
||||
</Button>
|
||||
{showModal && (
|
||||
<FullscreenModal
|
||||
title={modalTitle}
|
||||
closeFunction={() => setShowModal(false)}
|
||||
body={modalBody}
|
||||
active={showModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenInFullscreenButton;
|
||||
@@ -33,4 +33,5 @@ export { default as SubmitButton } from "./SubmitButton";
|
||||
export { default as DownloadButton } from "./DownloadButton";
|
||||
export { default as ButtonGroup } from "./ButtonGroup";
|
||||
export { default as ButtonAddons } from "./ButtonAddons";
|
||||
export { default as OpenInFullscreenButton } from "./OpenInFullscreenButton";
|
||||
export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton";
|
||||
|
||||
52
scm-ui/ui-components/src/modals/FullscreenModal.tsx
Normal file
52
scm-ui/ui-components/src/modals/FullscreenModal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 * as React from "react";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Modal } from "./Modal";
|
||||
import Button from "../buttons/Button";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
closeFunction: () => void;
|
||||
body: ReactNode;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
const FullSizedModal = styled(Modal)`
|
||||
& .modal-card {
|
||||
width: 98%;
|
||||
max-height: 97vh;
|
||||
}
|
||||
`;
|
||||
|
||||
const FullscreenModal: FC<Props> = ({ title, closeFunction, body, active }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const footer = <Button label={t("diff.fullscreen.close")} action={closeFunction} color="grey" />;
|
||||
|
||||
return <FullSizedModal title={title} closeFunction={closeFunction} body={body} footer={footer} active={active} />;
|
||||
};
|
||||
|
||||
export default FullscreenModal;
|
||||
@@ -22,7 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import {FC} from "react";
|
||||
import { FC } from "react";
|
||||
import classNames from "classnames";
|
||||
import usePortalRootElement from "../usePortalRootElement";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
@@ -26,3 +26,4 @@
|
||||
|
||||
export { default as ConfirmAlert, confirmAlert } from "./ConfirmAlert";
|
||||
export { default as Modal } from "./Modal";
|
||||
export { default as FullscreenModal } from "./FullscreenModal";
|
||||
|
||||
@@ -33,7 +33,7 @@ import Icon from "../Icon";
|
||||
import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes";
|
||||
import TokenizedDiffView from "./TokenizedDiffView";
|
||||
import DiffButton from "./DiffButton";
|
||||
import { MenuContext } from "@scm-manager/ui-components";
|
||||
import { MenuContext, OpenInFullscreenButton } from "@scm-manager/ui-components";
|
||||
import DiffExpander, { ExpandableHunk } from "./DiffExpander";
|
||||
import HunkExpandLink from "./HunkExpandLink";
|
||||
import { Modal } from "../modals";
|
||||
@@ -91,6 +91,10 @@ const ChangeTypeTag = styled(Tag)`
|
||||
margin-left: 0.75rem;
|
||||
`;
|
||||
|
||||
const MarginlessModalContent = styled.div`
|
||||
margin: -1.25rem;
|
||||
`;
|
||||
|
||||
class DiffFile extends React.Component<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
defaultCollapse: false,
|
||||
@@ -426,7 +430,13 @@ class DiffFile extends React.Component<Props, State> {
|
||||
}
|
||||
const collapseIcon = this.hasContent(file) ? <Icon name={icon} color="inherit" /> : null;
|
||||
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
|
||||
const sideBySideToggle = file.hunks && file.hunks.length && (
|
||||
const openInFullscreen = file?.hunks?.length ? (
|
||||
<OpenInFullscreenButton
|
||||
modalTitle={file.type === "delete" ? file.oldPath : file.newPath}
|
||||
modalBody={<MarginlessModalContent>{body}</MarginlessModalContent>}
|
||||
/>
|
||||
) : null;
|
||||
const sideBySideToggle = file?.hunks?.length && (
|
||||
<MenuContext.Consumer>
|
||||
{({ setCollapsed }) => (
|
||||
<DiffButton
|
||||
@@ -447,6 +457,7 @@ class DiffFile extends React.Component<Props, State> {
|
||||
<ButtonWrapper className={classNames("level-right", "is-flex")}>
|
||||
<ButtonGroup>
|
||||
{sideBySideToggle}
|
||||
{openInFullscreen}
|
||||
{fileControls}
|
||||
</ButtonGroup>
|
||||
</ButtonWrapper>
|
||||
@@ -465,7 +476,11 @@ class DiffFile extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<DiffFilePanel className={classNames("panel", "is-size-6")} collapsed={(file && file.isBinary) || collapsed} id={this.getAnchorId(file)}>
|
||||
<DiffFilePanel
|
||||
className={classNames("panel", "is-size-6")}
|
||||
collapsed={(file && file.isBinary) || collapsed}
|
||||
id={this.getAnchorId(file)}
|
||||
>
|
||||
{errorModal}
|
||||
<div className="panel-heading">
|
||||
<FlexWrapLevel className="level">
|
||||
|
||||
@@ -269,7 +269,11 @@
|
||||
"expandLastBottomByLines": "Bis zu {{count}} weitere Zeilen laden",
|
||||
"expandLastBottomComplete": "Alle verbleibenden Zeilen laden",
|
||||
"expanding": "Zeilen werden geladen ...",
|
||||
"expansionFailed": "Fehler beim Laden der zusätzlichen Zeilen"
|
||||
"expansionFailed": "Fehler beim Laden der zusätzlichen Zeilen",
|
||||
"fullscreen": {
|
||||
"open": "In Vollbildansicht öffnen",
|
||||
"close": "Schließen"
|
||||
}
|
||||
},
|
||||
"fileUpload": {
|
||||
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",
|
||||
|
||||
@@ -276,7 +276,11 @@
|
||||
"expandLastBottomByLines": "load up to {{count}} more lines",
|
||||
"expandLastBottomComplete": "load all remaining lines",
|
||||
"expanding": "loading lines ...",
|
||||
"expansionFailed": "Error while loading additional lines"
|
||||
"expansionFailed": "Error while loading additional lines",
|
||||
"fullscreen": {
|
||||
"open": "Open in Fullscreen",
|
||||
"close": "Close"
|
||||
}
|
||||
},
|
||||
"fileUpload": {
|
||||
"clickHere": "Click here to select your file",
|
||||
|
||||
@@ -21,87 +21,47 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { apiClient, ErrorNotification, Loading, SyntaxHighlighter } from "@scm-manager/ui-components";
|
||||
import { File, Link } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
file: File;
|
||||
language: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
content: string;
|
||||
error?: Error;
|
||||
loaded: boolean;
|
||||
currentFileRevision: string;
|
||||
};
|
||||
const SourcecodeViewer: FC<Props> = ({ file, language }) => {
|
||||
const [content, setContent] = useState("");
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentFileRevision, setCurrentFileRevision] = useState("");
|
||||
|
||||
class SourcecodeViewer extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
content: "",
|
||||
loaded: false,
|
||||
currentFileRevision: ""
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchContentIfChanged();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.fetchContentIfChanged();
|
||||
}
|
||||
|
||||
private fetchContentIfChanged() {
|
||||
const { file } = this.props;
|
||||
const { currentFileRevision } = this.state;
|
||||
useEffect(() => {
|
||||
if (file.revision !== currentFileRevision) {
|
||||
this.fetchContent();
|
||||
getContent((file._links.self as Link).href)
|
||||
.then(content => {
|
||||
setContent(content);
|
||||
setCurrentFileRevision(file.revision);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(setError);
|
||||
}
|
||||
}, [currentFileRevision, file]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
fetchContent = () => {
|
||||
const { file } = this.props;
|
||||
getContent((file._links.self as Link).href)
|
||||
.then(content => {
|
||||
this.setState({
|
||||
content,
|
||||
loaded: true,
|
||||
currentFileRevision: file.revision
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
error,
|
||||
loaded: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { content, error, loaded } = this.state;
|
||||
const language = this.props.language;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SyntaxHighlighter language={getLanguage(language)} value={content} />;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SyntaxHighlighter language={getLanguage(language)} value={content} />;
|
||||
};
|
||||
|
||||
export function getLanguage(language: string) {
|
||||
return language.toLowerCase();
|
||||
@@ -111,4 +71,4 @@ export function getContent(url: string) {
|
||||
return apiClient.get(url).then(response => response.text());
|
||||
}
|
||||
|
||||
export default withTranslation("repos")(SourcecodeViewer);
|
||||
export default SourcecodeViewer;
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { File, Repository } from "@scm-manager/ui-types";
|
||||
import { DateFromNow, ErrorNotification, FileSize, Icon } from "@scm-manager/ui-components";
|
||||
import { DateFromNow, ErrorNotification, FileSize, Icon, OpenInFullscreenButton } from "@scm-manager/ui-components";
|
||||
import { getSources } from "../modules/sources";
|
||||
import FileButtonAddons from "../components/content/FileButtonAddons";
|
||||
import SourcesView from "./SourcesView";
|
||||
@@ -82,6 +82,12 @@ const LighterGreyBackgroundTable = styled.table`
|
||||
background-color: #fbfbfb;
|
||||
`;
|
||||
|
||||
const BorderLessDiv = styled.div`
|
||||
margin: -1.25rem;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
export type SourceViewSelection = "source" | "history" | "annotations";
|
||||
|
||||
class Content extends React.Component<Props, State> {
|
||||
@@ -106,7 +112,7 @@ class Content extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
showHeader() {
|
||||
showHeader(content: ReactNode) {
|
||||
const { repository, file, revision } = this.props;
|
||||
const { selected, collapsed } = this.state;
|
||||
const icon = collapsed ? "angle-right" : "angle-down";
|
||||
@@ -129,6 +135,10 @@ class Content extends React.Component<Props, State> {
|
||||
</div>
|
||||
<div className="buttons is-grouped">
|
||||
{selector}
|
||||
<OpenInFullscreenButton
|
||||
modalTitle={file?.name}
|
||||
modalBody={<BorderLessDiv className="panel">{content}</BorderLessDiv>}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="repos.sources.content.actionbar"
|
||||
props={{
|
||||
@@ -211,24 +221,18 @@ class Content extends React.Component<Props, State> {
|
||||
const { file, revision, repository, path, breadcrumb } = this.props;
|
||||
const { selected, errorFromExtension } = this.state;
|
||||
|
||||
const header = this.showHeader();
|
||||
let content;
|
||||
switch (selected) {
|
||||
case "source":
|
||||
content = (
|
||||
<SourcesView revision={revision} file={file} repository={repository} path={path}/>
|
||||
);
|
||||
content = <SourcesView revision={revision} file={file} repository={repository} path={path} />;
|
||||
break;
|
||||
case "history":
|
||||
content = (
|
||||
<HistoryView file={file} repository={repository}/>
|
||||
);
|
||||
content = <HistoryView file={file} repository={repository} />;
|
||||
break;
|
||||
case "annotations":
|
||||
content = (
|
||||
<AnnotateView file={file} repository={repository} />
|
||||
);
|
||||
content = <AnnotateView file={file} repository={repository} />;
|
||||
}
|
||||
const header = this.showHeader(content);
|
||||
const moreInformation = this.showMoreInformation();
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user