mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
Merge branch 'develop' into feature/import_git_from_url
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
93
scm-ui/ui-webapp/src/repos/tags/container/DeleteTag.tsx
Normal file
93
scm-ui/ui-webapp/src/repos/tags/container/DeleteTag.tsx
Normal 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;
|
||||
59
scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx
Normal file
59
scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx
Normal 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;
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user