diff --git a/scm-ui/ui-components/src/buttons/Button.tsx b/scm-ui/ui-components/src/buttons/Button.tsx index e8c6494437..fc0bf7ff6b 100644 --- a/scm-ui/ui-components/src/buttons/Button.tsx +++ b/scm-ui/ui-components/src/buttons/Button.tsx @@ -94,9 +94,11 @@ class Button extends React.Component { - - {label} {children} - + {(label || children) && ( + + {label} {children} + + )} ); } diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index a0b6ef015b..b8510069c0 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -90,6 +90,19 @@ }, "table": { "tags": "Tags" + }, + "create": { + "form": { + "field": { + "name": { + "label": "Name" + } + } + }, + "title": "Neuen Tag anlegen", + "hint": "Der Tag wird automatisch mit ihrem Standardschlüssel vom SCM-Manager signiert.", + "confirm": "Tag erstellen", + "cancel": "Abbrechen" } }, "tag": { @@ -156,6 +169,9 @@ "buttons": { "details": "Details", "sources": "Sources" + }, + "tag": { + "create": "Tag erstellen" } }, "commit": { diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index f2f6f31cea..6cf48d8503 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -90,6 +90,19 @@ }, "table": { "tags": "Tags" + }, + "create": { + "form": { + "field": { + "name": { + "label": "Name" + } + } + }, + "title": "Create a new tag", + "hint": "The tag will be automatically signed with your default key by the SCM-Manager.", + "confirm": "Create Tag", + "cancel": "Cancel" } }, "tag": { @@ -156,6 +169,9 @@ "more": "{{count}} more", "count": "{{count}} Contributor", "count_plural": "{{count}} Contributors" + }, + "tag": { + "create": "Create Tag" } }, "commit": { diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx index be9a70dfc8..2b5284235d 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx @@ -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 } from "@scm-manager/ui-types"; import { AvatarImage, AvatarWrapper, @@ -41,7 +41,10 @@ import { FileControlFactory, Icon, Level, - SignatureIcon + SignatureIcon, + Modal, + InputField, + apiClient } from "@scm-manager/ui-components"; import ContributorTable from "./ContributorTable"; import { Link as ReactLink } from "react-router-dom"; @@ -50,6 +53,7 @@ type Props = WithTranslation & { changeset: Changeset; repository: Repository; fileControlFactory?: FileControlFactory; + refetchChangeset?: (re) => void; }; type State = { @@ -82,11 +86,12 @@ const ContributorLine = styled.div` `; const ContributorColumn = styled.p` - flex-grow: 1; + flex-grow: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; + margin-right: 16px; `; const CountColumn = styled.p` @@ -159,109 +164,154 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => { ); }; -class ChangesetDetails extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - collapsed: false - }; - } +const ChangesetDetails: FC = ({ changeset, repository, fileControlFactory, t , refetchChangeset}) => { + const [collapsed, setCollapsed] = useState(false); + const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false); + const [newTagName, setNewTagName] = useState(""); - render() { - const { changeset, repository, fileControlFactory, t } = this.props; - const { collapsed } = this.state; + const description = changesets.parseDescription(changeset.description); + const id = ; + const date = ; + const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => ( + + {parent.id.substring(0, 7)} + + )); + const showCreateButton = "tag" in changeset._links; - const description = changesets.parseDescription(changeset.description); - const id = ; - const date = ; - const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => ( - - {parent.id.substring(0, 7)} - - )); - - return ( - <> -
-

- - - -

-
- - - - - -
- - -

- -

- {parents?.length > 0 && ( - - {t("changeset.parents.label", { count: parents?.length }) + ": "} - {parents} - - )} -
-
-
- -
-
-

- {description.message.split("\n").map((item, key) => { - return ( - - - - -
-
- ); - })} -

-
-
- - } - /> - -
- - ); - } - - collapseDiffs = () => { - this.setState(state => ({ - collapsed: !state.collapsed - })); + const collapseDiffs = () => { + setCollapsed(!collapsed); }; -} + + const createTag = () => { + apiClient + .post((changeset._links["tag"] as Link).href, { + revision: changeset.id, + name: newTagName + }) + .then(() => { + refetchChangeset?.(); + closeTagCreationModal(); + }); + }; + + const closeTagCreationModal = () => { + setNewTagName(""); + setTagCreationModalVisible(false); + }; + + return ( + <> +
+

+ + + +

+
+ + + + + +
+ + +

+ +

+ {parents?.length > 0 && ( + + {t("changeset.parents.label", { count: parents?.length }) + ": "} + {parents} + + )} +
+
+
+ +
+
+ {showCreateButton && ( + + + + } + closeFunction={() => closeTagCreationModal()} + /> + )} +
+
+

+ {description.message.split("\n").map((item, key) => { + return ( + + + + +
+
+ ); + })} +

+
+
+ + } + /> + +
+ + ); +}; export default withTranslation("repos")(ChangesetDetails); diff --git a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx index fd9c55d32b..20791822aa 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx @@ -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 { } render() { - const { changeset, loading, error, t, repository, fileControlFactoryFactory } = this.props; + const { changeset, loading, error, t, repository, fileControlFactoryFactory, refetchChangeset } = this.props; if (error) { return ; @@ -75,6 +77,7 @@ class ChangesetView extends React.Component { 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)); } }; }; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index 53e6d52c0a..69977065e7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -130,6 +130,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, getListOfObjects(source.getTags(), tags::getTagByName))); } + linksBuilder.single(link("tag", resourceLinks.tag().create(namespace, name))); } if (repositoryService.isSupported(Command.BRANCHES)) { embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index f24490eef1..79cfc3ace0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -449,6 +449,10 @@ class ResourceLinks { return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("delete").parameters(tagName).href(); } + String create(String namespace, String name) { + return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("create").parameters().href(); + } + String all(String namespace, String name) { return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href(); }