Merge branch 'develop' into feature/import_git_from_url

This commit is contained in:
Eduard Heimbuch
2020-12-02 14:39:45 +01:00
71 changed files with 2240 additions and 588 deletions

View File

@@ -36,7 +36,7 @@ class BranchView extends React.Component<Props> {
render() {
const { repository, branch } = this.props;
return (
<div>
<>
<BranchDetail repository={repository} branch={branch} />
<hr />
<div className="content">
@@ -50,7 +50,7 @@ class BranchView extends React.Component<Props> {
/>
</div>
<BranchDangerZone repository={repository} branch={branch} />
</div>
</>
);
}
}

View File

@@ -26,7 +26,7 @@ import { Trans, useTranslation, WithTranslation, withTranslation } from "react-i
import classNames from "classnames";
import styled from "styled-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Changeset, ParentChangeset, Repository } from "@scm-manager/ui-types";
import { Changeset, Link, ParentChangeset, Repository, Tag } from "@scm-manager/ui-types";
import {
AvatarImage,
AvatarWrapper,
@@ -41,7 +41,10 @@ import {
FileControlFactory,
Icon,
Level,
SignatureIcon
SignatureIcon,
Tooltip,
ErrorNotification,
CreateTagModal
} from "@scm-manager/ui-components";
import ContributorTable from "./ContributorTable";
import { Link as ReactLink } from "react-router-dom";
@@ -50,10 +53,7 @@ type Props = WithTranslation & {
changeset: Changeset;
repository: Repository;
fileControlFactory?: FileControlFactory;
};
type State = {
collapsed: boolean;
refetchChangeset?: () => void;
};
const RightMarginP = styled.p`
@@ -82,7 +82,7 @@ const ContributorLine = styled.div`
`;
const ContributorColumn = styled.p`
flex-grow: 1;
flex-grow: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -108,7 +108,6 @@ const ContributorToggleLine = styled.p`
const ChangesetSummary = styled.div`
display: flex;
justify-content: space-between;
`;
const SeparatedParents = styled.div`
@@ -147,7 +146,7 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
</ContributorColumn>
{signatureIcon}
<CountColumn className={"is-hidden-mobile"}>
<CountColumn className={"is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only"}>
(
<span className="has-text-link">
{t("changeset.contributors.count", { count: countContributors(changeset) })}
@@ -159,109 +158,131 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
);
};
class ChangesetDetails extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsed: false
};
}
const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory, t, refetchChangeset }) => {
const [collapsed, setCollapsed] = useState(false);
const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false);
const [error, setError] = useState<Error | undefined>();
render() {
const { changeset, repository, fileControlFactory, t } = this.props;
const { collapsed } = this.state;
const description = changesets.parseDescription(changeset.description);
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
const date = <DateFromNow date={changeset.date} />;
const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => (
<ReactLink title={parent.id} to={parent.id} key={index}>
{parent.id.substring(0, 7)}
</ReactLink>
));
const showCreateButton = "tag" in changeset._links;
const description = changesets.parseDescription(changeset.description);
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
const date = <DateFromNow date={changeset.date} />;
const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => (
<ReactLink title={parent.id} to={parent.id} key={index}>
{parent.id.substring(0, 7)}
</ReactLink>
));
return (
<>
<div className={classNames("content", "is-marginless")}>
<h4>
<ExtensionPoint
name="changeset.description"
props={{
changeset,
value: description.title
}}
renderAll={false}
>
<ChangesetDescription changeset={changeset} value={description.title} />
</ExtensionPoint>
</h4>
<article className="media">
<AvatarWrapper>
<RightMarginP className={classNames("image", "is-64x64")}>
<AvatarImage person={changeset.author} />
</RightMarginP>
</AvatarWrapper>
<div className="media-content">
<Contributors changeset={changeset} />
<ChangesetSummary className="is-ellipsis-overflow">
<p>
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
</p>
{parents?.length > 0 && (
<SeparatedParents>
{t("changeset.parents.label", { count: parents?.length }) + ": "}
{parents}
</SeparatedParents>
)}
</ChangesetSummary>
</div>
<div className="media-right">
<ChangesetTags changeset={changeset} />
</div>
</article>
<p>
{description.message.split("\n").map((item, key) => {
return (
<span key={key}>
<ExtensionPoint
name="changeset.description"
props={{
changeset,
value: item
}}
renderAll={false}
>
<ChangesetDescription changeset={changeset} value={item} />
</ExtensionPoint>
<br />
</span>
);
})}
</p>
</div>
<div>
<BottomMarginLevel
right={
<Button
action={this.collapseDiffs}
color="default"
icon={collapsed ? "eye" : "eye-slash"}
label={t("changesets.collapseDiffs")}
reducedMobile={true}
/>
}
/>
<ChangesetDiff changeset={changeset} fileControlFactory={fileControlFactory} defaultCollapse={collapsed} />
</div>
</>
);
}
collapseDiffs = () => {
this.setState(state => ({
collapsed: !state.collapsed
}));
const collapseDiffs = () => {
setCollapsed(!collapsed);
};
}
if (error) {
return <ErrorNotification error={error} />;
}
return (
<>
<div className={classNames("content", "is-marginless")}>
<h4>
<ExtensionPoint
name="changeset.description"
props={{
changeset,
value: description.title
}}
renderAll={false}
>
<ChangesetDescription changeset={changeset} value={description.title} />
</ExtensionPoint>
</h4>
<article className="media">
<AvatarWrapper>
<RightMarginP className={classNames("image", "is-64x64")}>
<AvatarImage person={changeset.author} />
</RightMarginP>
</AvatarWrapper>
<div className="media-content">
<Contributors changeset={changeset} />
<ChangesetSummary className="is-ellipsis-overflow">
<p>
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
</p>
{parents?.length > 0 && (
<SeparatedParents>
{t("changeset.parents.label", { count: parents?.length }) + ": "}
{parents}
</SeparatedParents>
)}
</ChangesetSummary>
</div>
<div className="media-right">
<ChangesetTags changeset={changeset} />
</div>
{showCreateButton && (
<div className="media-right">
<Tooltip message={t("changeset.tag.create")} location="top">
<Button
color="success"
className="tag"
label={(changeset._embedded["tags"]?.length === 0 && t("changeset.tag.create")) || ""}
icon="plus"
action={() => setTagCreationModalVisible(true)}
/>
</Tooltip>
</div>
)}
{isTagCreationModalVisible && (
<CreateTagModal
revision={changeset.id}
onClose={() => setTagCreationModalVisible(false)}
onCreated={() => {
refetchChangeset?.();
setTagCreationModalVisible(false);
}}
onError={setError}
tagCreationLink={(changeset._links["tag"] as Link).href}
existingTagsLink={(repository._links["tags"] as Link).href}
/>
)}
</article>
<p>
{description.message.split("\n").map((item, key) => {
return (
<span key={key}>
<ExtensionPoint
name="changeset.description"
props={{
changeset,
value: item
}}
renderAll={false}
>
<ChangesetDescription changeset={changeset} value={item} />
</ExtensionPoint>
<br />
</span>
);
})}
</p>
</div>
<div>
<BottomMarginLevel
right={
<Button
action={collapseDiffs}
color="default"
icon={collapsed ? "eye" : "eye-slash"}
label={t("changesets.collapseDiffs")}
reducedMobile={true}
/>
}
/>
<ChangesetDiff changeset={changeset} fileControlFactory={fileControlFactory} defaultCollapse={collapsed} />
</div>
</>
);
};
export default withTranslation("repos")(ChangesetDetails);

View File

@@ -29,6 +29,7 @@ import { WithTranslation, withTranslation } from "react-i18next";
import { Changeset, Repository } from "@scm-manager/ui-types";
import { ErrorPage, Loading } from "@scm-manager/ui-components";
import {
fetchChangeset,
fetchChangesetIfNeeded,
getChangeset,
getFetchChangesetFailure,
@@ -45,6 +46,7 @@ type Props = WithTranslation & {
loading: boolean;
error: Error;
fetchChangesetIfNeeded: (repository: Repository, id: string) => void;
refetchChangeset: (repository: Repository, id: string) => void;
match: any;
};
@@ -62,7 +64,7 @@ class ChangesetView extends React.Component<Props> {
}
render() {
const { changeset, loading, error, t, repository, fileControlFactoryFactory } = this.props;
const { changeset, loading, error, t, repository, fileControlFactoryFactory, refetchChangeset } = this.props;
if (error) {
return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error} />;
@@ -75,6 +77,7 @@ class ChangesetView extends React.Component<Props> {
changeset={changeset}
repository={repository}
fileControlFactory={fileControlFactoryFactory && fileControlFactoryFactory(changeset)}
refetchChangeset={() => refetchChangeset(repository, changeset.id)}
/>
);
}
@@ -98,6 +101,9 @@ const mapDispatchToProps = (dispatch: any) => {
return {
fetchChangesetIfNeeded: (repository: Repository, id: string) => {
dispatch(fetchChangesetIfNeeded(repository, id));
},
refetchChangeset: (repository: Repository, id: string) => {
dispatch(fetchChangeset(repository, id));
}
};
};

View File

@@ -25,7 +25,7 @@
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Repository, Tag } from "@scm-manager/ui-types";
import { DateFromNow } from "@scm-manager/ui-components";
import { DateFromNow, SignatureIcon } from "@scm-manager/ui-components";
import styled from "styled-components";
import TagButtonGroup from "./TagButtonGroup";
@@ -58,9 +58,10 @@ const TagDetail: FC<Props> = ({ tag, repository }) => {
return (
<div className="media">
<FlexRow className="media-content subtitle">
<Label>{t("tag.name") + ": "} </Label> {tag.name}
<Created className="is-ellipsis-overflow">
<FlexRow className="media-content">
<Label className="subtitle has-text-weight-bold has-text-black">{t("tag.name") + ": "} </Label> <span className="subtitle">{tag.name}</span>
<SignatureIcon signatures={tag.signatures} className="ml-2 mb-5" />
<Created className="is-ellipsis-overflow mb-5">
{t("tags.overview.created")} <Date date={tag.date} className="has-text-grey" />
</Created>
</FlexRow>

View File

@@ -24,14 +24,16 @@
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Tag } from "@scm-manager/ui-types";
import { Link as RouterLink } from "react-router-dom";
import { Tag, Link } from "@scm-manager/ui-types";
import styled from "styled-components";
import { DateFromNow } from "@scm-manager/ui-components";
import { DateFromNow, Icon } from "@scm-manager/ui-components";
type Props = {
tag: Tag;
baseUrl: string;
onDelete: (tag: Tag) => void;
// deleting: boolean;
};
const Created = styled.span`
@@ -39,20 +41,32 @@ const Created = styled.span`
font-size: 0.8rem;
`;
const TagRow: FC<Props> = ({ tag, baseUrl }) => {
const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
const [t] = useTranslation("repos");
let deleteButton;
if ((tag?._links?.delete as Link)?.href) {
deleteButton = (
<a className="level-item" onClick={() => onDelete(tag)}>
<span className="icon is-small">
<Icon name="trash" className="fas" title={t("tag.delete.button")} />
</span>
</a>
);
}
const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`;
return (
<tr>
<td>
<Link to={to} title={tag.name}>
<RouterLink to={to} title={tag.name}>
{tag.name}
<Created className="has-text-grey is-ellipsis-overflow">
{t("tags.overview.created")} <DateFromNow date={tag.date} />
</Created>
</Link>
</RouterLink>
</td>
<td className="is-darker">{deleteButton}</td>
</tr>
);
};

View File

@@ -22,38 +22,83 @@
* SOFTWARE.
*/
import React, { FC } from "react";
import { Tag } from "@scm-manager/ui-types";
import React, { FC, useState } from "react";
import { Link, Tag } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
import TagRow from "./TagRow";
import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
type Props = {
baseUrl: string;
tags: Tag[];
fetchTags: () => void;
};
const TagTable: FC<Props> = ({ baseUrl, tags }) => {
const TagTable: FC<Props> = ({ baseUrl, tags, fetchTags }) => {
const [t] = useTranslation("repos");
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [error, setError] = useState<Error | undefined>();
const [tagToBeDeleted, setTagToBeDeleted] = useState<Tag | undefined>();
const onDelete = (tag: Tag) => {
setTagToBeDeleted(tag);
setShowConfirmAlert(true);
};
const abortDelete = () => {
setTagToBeDeleted(undefined);
setShowConfirmAlert(false);
};
const deleteTag = () => {
apiClient
.delete((tagToBeDeleted?._links.delete as Link).href)
.then(() => fetchTags())
.catch(setError);
};
const renderRow = () => {
let rowContent = null;
if (tags) {
rowContent = tags.map((tag, index) => {
return <TagRow key={index} baseUrl={baseUrl} tag={tag} />;
return <TagRow key={index} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />;
});
}
return rowContent;
};
const confirmAlert = (
<ConfirmAlert
title={t("tag.delete.confirmAlert.title")}
message={t("tag.delete.confirmAlert.message", { tag: tagToBeDeleted?.name })}
buttons={[
{
className: "is-outlined",
label: t("tag.delete.confirmAlert.submit"),
onClick: () => deleteTag()
},
{
label: t("tag.delete.confirmAlert.cancel"),
onClick: () => abortDelete()
}
]}
close={() => abortDelete()}
/>
);
return (
<table className="card-table table is-hoverable is-fullwidth is-word-break">
<thead>
<tr>
<th>{t("tags.table.tags")}</th>
</tr>
</thead>
<tbody>{renderRow()}</tbody>
</table>
<>
{showConfirmAlert && confirmAlert}
{error && <ErrorNotification error={error} />}
<table className="card-table table is-hoverable is-fullwidth is-word-break">
<thead>
<tr>
<th>{t("tags.table.tags")}</th>
</tr>
</thead>
<tbody>{renderRow()}</tbody>
</table>
</>
);
};

View File

@@ -26,6 +26,7 @@ import React, { FC } from "react";
import { Repository, Tag } from "@scm-manager/ui-types";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import TagDetail from "./TagDetail";
import TagDangerZone from "../container/TagDangerZone";
type Props = {
repository: Repository;
@@ -47,6 +48,7 @@ const TagView: FC<Props> = ({ repository, tag }) => {
}}
/>
</div>
<TagDangerZone repository={repository} tag={tag} />
</>
);
};

View File

@@ -0,0 +1,93 @@
/*
* 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, useState } from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { apiClient, ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { Link, Repository, Tag } from "@scm-manager/ui-types";
type Props = {
repository: Repository;
tag: Tag;
};
const DeleteTag: FC<Props> = ({ tag, repository }) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [error, setError] = useState<Error | undefined>();
const [t] = useTranslation("repos");
const history = useHistory();
const deleteBranch = () => {
apiClient
.delete((tag._links.delete as Link).href)
.then(() => history.push(`/repo/${repository.namespace}/${repository.name}/tags/`))
.catch(setError);
};
if (!tag._links.delete) {
return null;
}
let confirmAlert = null;
if (showConfirmAlert) {
confirmAlert = (
<ConfirmAlert
title={t("tag.delete.confirmAlert.title")}
message={t("tag.delete.confirmAlert.message", { tag: tag.name })}
buttons={[
{
className: "is-outlined",
label: t("tag.delete.confirmAlert.submit"),
onClick: () => deleteBranch()
},
{
label: t("tag.delete.confirmAlert.cancel"),
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
<>
<ErrorNotification error={error} />
{showConfirmAlert && confirmAlert}
<Level
left={
<p>
<strong>{t("tag.delete.subtitle")}</strong>
<br />
{t("tag.delete.description")}
</p>
}
right={<DeleteButton label={t("tag.delete.button")} action={() => setShowConfirmAlert(true)} />}
/>
</>
);
};
export default DeleteTag;

View File

@@ -0,0 +1,59 @@
/*
* 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 { Repository, Tag } from "@scm-manager/ui-types";
import { Subtitle } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { DangerZoneContainer } from "../../containers/RepositoryDangerZone";
import DeleteTag from "./DeleteTag";
type Props = {
repository: Repository;
tag: Tag;
};
const TagDangerZone: FC<Props> = ({ repository, tag }) => {
const [t] = useTranslation("repos");
const dangerZone = [];
if (tag?._links?.delete) {
dangerZone.push(<DeleteTag repository={repository} tag={tag} key={dangerZone.length} />);
}
if (dangerZone.length === 0) {
return null;
}
return (
<>
<hr />
<Subtitle subtitle={t("tag.dangerZone")} />
<DangerZoneContainer>{dangerZone}</DangerZoneContainer>
</>
);
};
export default TagDangerZone;

View File

@@ -40,7 +40,7 @@ const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
const [error, setError] = useState<Error | undefined>(undefined);
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
const fetchTags = () => {
const link = (repository._links?.tags as Link)?.href;
if (link) {
setLoading(true);
@@ -51,12 +51,16 @@ const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
.then(() => setLoading(false))
.catch(setError);
}
};
useEffect(() => {
fetchTags();
}, [repository]);
const renderTagsTable = () => {
if (!loading && tags?.length > 0) {
orderTags(tags);
return <TagTable baseUrl={baseUrl} tags={tags} />;
return <TagTable baseUrl={baseUrl} tags={tags} fetchTags={fetchTags} />;
}
return <Notification type="info">{t("tags.overview.noTags")}</Notification>;
};