From bb82c18e2ba9ff344497f8279566b700ac2e8bca Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 11 Nov 2020 14:09:15 +0100 Subject: [PATCH] add delete link to branchDto --- .../repos/branches/components/BranchRow.tsx | 28 +++++++-- .../repos/branches/components/BranchTable.tsx | 44 +++++++------- .../branches/containers/BranchesOverview.tsx | 7 ++- .../BranchChangesetCollectionToDtoMapper.java | 2 +- .../BranchCollectionToDtoMapper.java | 6 +- .../api/v2/resources/BranchRootResource.java | 4 +- .../v2/resources/BranchToBranchDtoMapper.java | 15 +++-- .../ChangesetCollectionToDtoMapperBase.java | 2 +- .../resources/DefaultBranchLinkProvider.java | 2 +- .../DefaultChangesetToChangesetDtoMapper.java | 2 +- .../scm/api/v2/resources/ResourceLinks.java | 13 +++-- .../BranchToBranchDtoMapperTest.java | 57 ++++++++++++++++++- .../api/v2/resources/ResourceLinksTest.java | 4 +- 13 files changed, 135 insertions(+), 51 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx index baddb90afd..6237bbe077 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx @@ -22,25 +22,43 @@ * SOFTWARE. */ import React, { FC } from "react"; -import { Link } from "react-router-dom"; -import { Branch } from "@scm-manager/ui-types"; +import { Link as ReactLink } from "react-router-dom"; +import { Branch, Link } from "@scm-manager/ui-types"; import DefaultBranchTag from "./DefaultBranchTag"; +import { Icon } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; type Props = { baseUrl: string; branch: Branch; + onDelete: (url: string) => void; }; -const BranchRow: FC = ({ baseUrl, branch }) => { +const BranchRow: FC = ({ baseUrl, branch, onDelete }) => { const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; + const [t] = useTranslation("repos"); + + let deleteButton; + if ((branch?._links?.delete as Link)?.href) { + const url = (branch._links.delete as Link).href; + deleteButton = ( + onDelete(url)}> + + + + + ); + } + return ( - + {branch.name} - + + {deleteButton} ); }; diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx index db23ca7ad0..05589cffdd 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx @@ -21,41 +21,39 @@ * 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 } from "react"; +import { useTranslation } from "react-i18next"; import BranchRow from "./BranchRow"; import { Branch } from "@scm-manager/ui-types"; -type Props = WithTranslation & { +type Props = { baseUrl: string; branches: Branch[]; + onDelete: (url: string) => void; }; -class BranchTable extends React.Component { - render() { - const { t } = this.props; - return ( - - - - - - - {this.renderRow()} -
{t("branches.table.branches")}
- ); - } +const BranchTable: FC = ({ baseUrl, branches, onDelete }) => { + const [t] = useTranslation("repos"); - renderRow() { - const { baseUrl, branches } = this.props; + const renderRow = () => { let rowContent = null; if (branches) { rowContent = branches.map((branch, index) => { - return ; + return ; }); } return rowContent; - } -} + }; + return ( + + + + + + + {renderRow()} +
{t("branches.table.branches")}
+ ); +}; -export default withTranslation("repos")(BranchTable); +export default BranchTable; diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx index 0e2a01622b..e18593a701 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx @@ -37,6 +37,7 @@ import { } from "../modules/branches"; import { orderBranches } from "../util/orderBranches"; import BranchTable from "../components/BranchTable"; +import { apiClient } from "@scm-manager/ui-components/src"; type Props = WithTranslation & { repository: Repository; @@ -80,11 +81,15 @@ class BranchesOverview extends React.Component { ); } + onDelete(url: string) { + apiClient.delete(url).catch(error => this.setState({ error })); + } + renderBranchesTable() { const { baseUrl, branches, t } = this.props; if (branches && branches.length > 0) { orderBranches(branches); - return ; + return ; } return {t("branches.overview.noBranches")}; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java index 6cb1b1049b..96de0ce2dc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java @@ -45,6 +45,6 @@ public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToD } private String createSelfLink(Repository repository, String branch) { - return resourceLinks.branch().history(repository.getNamespaceAndName(), branch); + return resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java index f640331381..f102d27c4f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java @@ -55,11 +55,11 @@ public class BranchCollectionToDtoMapper { public HalRepresentation map(Repository repository, Collection branches) { return new HalRepresentation( createLinks(repository), - embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches))); + embedDtos(getBranchDtoList(repository, branches))); } - public List getBranchDtoList(String namespace, String name, Collection branches) { - return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); + public List getBranchDtoList(Repository repository, Collection branches) { + return branches.stream().map(branch -> branchToDtoMapper.map(branch, repository)).collect(toList()); } private Links createLinks(Repository repository) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 8d6d23c580..a5ce4d951e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -134,7 +134,7 @@ public class BranchRootResource { .stream() .filter(branch -> branchName.equals(branch.getName())) .findFirst() - .map(branch -> branchToDtoMapper.map(branch, namespaceAndName)) + .map(branch -> branchToDtoMapper.map(branch, repositoryService.getRepository())) .map(Response::ok) .orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName))) .build(); @@ -249,7 +249,7 @@ public class BranchRootResource { branchCommand.from(parentName); } Branch newBranch = branchCommand.branch(branchName); - return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build(); + return Response.created(URI.create(resourceLinks.branch().self(namespace, name, newBranch.getName()))).build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index f8088a79d5..f82c434b3d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -32,6 +32,8 @@ import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; @@ -46,16 +48,21 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper { private ResourceLinks resourceLinks; @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName); + public abstract BranchDto map(Branch branch, @Context Repository repository); @ObjectFactory - BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) { + BranchDto createDto(@Context Repository repository, Branch branch) { + NamespaceAndName namespaceAndName = new NamespaceAndName(repository.getNamespace(), repository.getName()); Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.branch().self(namespaceAndName, branch.getName())) - .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, branch.getName())).build()) + .self(resourceLinks.branch().self(repository.getNamespace(), repository.getName(), branch.getName())) + .single(linkBuilder("history", resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch.getName())).build()) .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()) .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()); + if (!branch.isDefaultBranch() && RepositoryPermissions.modify(repository).isPermitted()) { + linksBuilder.single(linkBuilder("delete", resourceLinks.branch().delete(repository.getNamespace(), repository.getName(), branch.getName())).build()); + } + Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder(); applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java index 0556b7a636..0e582309ad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java @@ -56,7 +56,7 @@ class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper Branch.normalBranch(branchName, source.getId())))); } 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 c826ee8e5e..bbb02a64fc 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 @@ -25,6 +25,7 @@ package sonia.scm.api.v2.resources; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; import sonia.scm.security.gpg.UserPublicKeyResource; import javax.inject.Inject; @@ -485,17 +486,21 @@ class ResourceLinks { branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class); } - String self(NamespaceAndName namespaceAndName, String branch) { - return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("get").parameters(branch).href(); + String self(String namespace, String name, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("get").parameters(branch).href(); } - public String history(NamespaceAndName namespaceAndName, String branch) { - return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href(); + public String history(String namespace, String name, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("history").parameters(branch).href(); } public String create(String namespace, String name) { return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href(); } + + public String delete(String namespace, String name, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("delete").parameters(branch).href(); + } } public IncomingLinks incoming() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java index f1b44bee08..e46dc6eea3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java @@ -24,28 +24,49 @@ package sonia.scm.api.v2.resources; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.Branch; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class BranchToBranchDtoMapperTest { - private final URI baseUri = URI.create("https://hitchhiker.com"); + private final URI baseUri = URI.create("https://hitchhiker.com/api/"); @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @Mock + private Subject subject; + @InjectMocks private BranchToBranchDtoMapperImpl mapper; + @BeforeEach + void setupSubject() { + ThreadContext.bind(subject); + } + + @AfterEach + void tearDown() { + ThreadContext.unbindSubject(); + } + @Test void shouldAppendLinks() { HalEnricherRegistry registry = new HalEnricherRegistry(); @@ -59,7 +80,37 @@ class BranchToBranchDtoMapperTest { Branch branch = Branch.normalBranch("master", "42"); - BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold")); - assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master"); + BranchDto dto = mapper.map(branch, RepositoryTestData.createHeartOfGold()); + assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/HeartOfGold/master"); } + + @Test + void shouldAppendDeleteLink() { + Repository repository = RepositoryTestData.createHeartOfGold(); + when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(true); + Branch branch = Branch.normalBranch("master", "42"); + + BranchDto dto = mapper.map(branch, repository); + assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("https://hitchhiker.com/api/v2/repositories/hitchhiker/HeartOfGold/branches/master"); + } + + @Test + void shouldNotAppendDeleteLinkIfDefaultBranch() { + Repository repository = RepositoryTestData.createHeartOfGold(); + Branch branch = Branch.defaultBranch("master", "42"); + + BranchDto dto = mapper.map(branch, repository); + assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent(); + } + + @Test + void shouldNotAppendDeleteLinkIfNotPermitted() { + Repository repository = RepositoryTestData.createHeartOfGold(); + when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(false); + Branch branch = Branch.normalBranch("master", "42"); + + BranchDto dto = mapper.map(branch, repository); + assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent(); + } + } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index a0b1c9b7cb..9889d4df33 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -140,13 +140,13 @@ public class ResourceLinksTest { @Test public void shouldCreateCorrectBranchUrl() { - String url = resourceLinks.branch().self(new NamespaceAndName("space", "name"), "master"); + String url = resourceLinks.branch().self("space", "name", "master"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master", url); } @Test public void shouldCreateCorrectBranchHiostoryUrl() { - String url = resourceLinks.branch().history(new NamespaceAndName("space", "name"), "master"); + String url = resourceLinks.branch().history("space", "name", "master"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master/changesets/", url); }