diff --git a/lerna.json b/lerna.json index ffa2cdc5ad..e17c5bcb57 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "2.1.1" + "version": "2.2.0-SNAPSHOT" } diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java index 4cb7cc32d9..48b3cd09b6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import sonia.scm.plugin.ExtensionPoint; @@ -36,8 +36,14 @@ public interface NamespaceStrategy { * Create new namespace for the given repository. * * @param repository repository - * * @return namespace */ String createNamespace(Repository repository); + + /** + * Checks if the namespace can be changed when using this namespace strategy + * + * @return namespace can be changed + */ + boolean canBeChanged(); } diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index 07e0b276b7..e21ea133bc 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -26,8 +26,9 @@ import styled from "styled-components"; import { WithTranslation, withTranslation } from "react-i18next"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Repository, RepositoryType } from "@scm-manager/ui-types"; -import { Checkbox, Level, InputField, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components"; +import { Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components"; import * as validator from "./repositoryValidation"; +import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos"; const CheckboxWrapper = styled.div` margin-top: 2em; @@ -59,8 +60,6 @@ type State = { contactValidationError: boolean; }; -const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy"; - class RepositoryForm extends React.Component { constructor(props: Props) { super(props); @@ -108,7 +107,7 @@ class RepositoryForm extends React.Component { ); }; - submit = (event: Event) => { + submit = (event: React.FormEvent) => { event.preventDefault(); if (this.isValid()) { this.props.submitForm(this.state.repository, this.state.initRepository); diff --git a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx b/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx index 97cd60b68d..5fb14e4557 100644 --- a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx @@ -23,7 +23,7 @@ */ import React, { FC } from "react"; -import { Repository } from "@scm-manager/ui-types"; +import { Repository, Links } from "@scm-manager/ui-types"; import RenameRepository from "./RenameRepository"; import DeleteRepo from "./DeleteRepo"; import styled from "styled-components"; @@ -32,6 +32,7 @@ import { useTranslation } from "react-i18next"; type Props = { repository: Repository; + indexLinks: Links; }; const DangerZoneContainer = styled.div` @@ -44,17 +45,15 @@ const DangerZoneContainer = styled.div` } `; -const DangerZone: FC = ({ repository }) => { +const DangerZone: FC = ({ repository, indexLinks }) => { const [t] = useTranslation("repos"); const dangerZone = []; - if (repository?._links?.rename) { - dangerZone.push(); - } - if (repository?._links?.renameWithNamespace) { - dangerZone.push(); + if (repository?._links?.rename || repository?._links?.renameWithNamespace) { + dangerZone.push(); } if (repository?._links?.delete) { + // @ts-ignore dangerZone.push(); } diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx index 7b6a71b51c..e4d16dcfa8 100644 --- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx @@ -25,17 +25,19 @@ import React from "react"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; import RepositoryForm from "../components/form"; -import { Repository } from "@scm-manager/ui-types"; +import { Repository, Links } from "@scm-manager/ui-types"; import { getModifyRepoFailure, isModifyRepoPending, modifyRepo, modifyRepoReset } from "../modules/repos"; import { History } from "history"; import { ErrorNotification } from "@scm-manager/ui-components"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { compose } from "redux"; import DangerZone from "./DangerZone"; +import { getLinks } from "../../modules/indexResource"; type Props = { loading: boolean; error: Error; + indexLinks: Links; modifyRepo: (p1: Repository, p2: () => void) => void; modifyRepoReset: (p: Repository) => void; @@ -69,7 +71,7 @@ class EditRepo extends React.Component { }; render() { - const { loading, error, repository } = this.props; + const { loading, error, repository, indexLinks } = this.props; const url = this.matchedUrl(); @@ -89,7 +91,7 @@ class EditRepo extends React.Component { }} /> - + ); } @@ -99,9 +101,12 @@ const mapStateToProps = (state: any, ownProps: Props) => { const { namespace, name } = ownProps.repository; const loading = isModifyRepoPending(state, namespace, name); const error = getModifyRepoFailure(state, namespace, name); + const indexLinks = getLinks(state); + return { loading, - error + error, + indexLinks }; }; diff --git a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx index 5209d68f61..e9bce9767e 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx @@ -22,29 +22,22 @@ * SOFTWARE. */ -import React, { FC, useState } from "react"; -import { Repository, Link } from "@scm-manager/ui-types"; -import { CONTENT_TYPE } from "../modules/repos"; -import { - ErrorNotification, - Level, - Button, - Loading, - Modal, - InputField, - validation, - ButtonGroup -} from "@scm-manager/ui-components"; +import React, { FC, useEffect, useState } from "react"; +import { Link, Links, Repository } from "@scm-manager/ui-types"; +import { CONTENT_TYPE, CUSTOM_NAMESPACE_STRATEGY } from "../modules/repos"; +import { Button, ButtonGroup, ErrorNotification, InputField, Level, Loading, Modal } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { apiClient } from "@scm-manager/ui-components/src"; import { useHistory } from "react-router-dom"; +import { ExtensionPoint } from "@scm-manager/ui-extensions/src"; +import * as validator from "../components/form/repositoryValidation"; type Props = { repository: Repository; - renameNamespace: boolean; + indexLinks: Links; }; -const RenameRepository: FC = ({ repository, renameNamespace }) => { +const RenameRepository: FC = ({ repository, indexLinks }) => { let history = useHistory(); const [t] = useTranslation("repos"); const [error, setError] = useState(undefined); @@ -52,6 +45,17 @@ const RenameRepository: FC = ({ repository, renameNamespace }) => { const [showModal, setShowModal] = useState(false); const [name, setName] = useState(repository.name); const [namespace, setNamespace] = useState(repository.namespace); + const [nameValidationError, setNameValidationError] = useState(false); + const [namespaceValidationError, setNamespaceValidationError] = useState(false); + const [currentNamespaceStrategie, setCurrentNamespaceStrategy] = useState(""); + + useEffect(() => { + apiClient + .get((indexLinks?.namespaceStrategies as Link).href) + .then(result => result.json()) + .then(result => setCurrentNamespaceStrategy(result.current)) + .catch(setError); + }, [repository]); if (error) { return ; @@ -62,13 +66,40 @@ const RenameRepository: FC = ({ repository, renameNamespace }) => { } const isValid = - validation.isNameValid(name) && - validation.isNameValid(namespace) && + !nameValidationError && + !namespaceValidationError && (repository.name !== name || repository.namespace !== namespace); + const handleNamespaceChange = (namespace: string) => { + setNamespaceValidationError(!validator.isNameValid(namespace)); + setNamespace(namespace); + }; + + const handleNameChange = (name: string) => { + setNameValidationError(!validator.isNameValid(name)); + setName(name); + }; + + const renderNamespaceField = () => { + const props = { + label: t("repository.namespace"), + helpText: t("help.namespaceHelpText"), + value: namespace, + onChange: handleNamespaceChange, + errorMessage: t("validation.namespace-invalid"), + validationError: namespaceValidationError + }; + + if (currentNamespaceStrategie === CUSTOM_NAMESPACE_STRATEGY) { + return ; + } + + return ; + }; + const rename = () => { setLoading(true); - const url = renameNamespace + const url = repository?._links?.renameWithNamespace ? (repository?._links?.renameWithNamespace as Link).href : (repository?._links?.rename as Link).href; @@ -84,17 +115,13 @@ const RenameRepository: FC = ({ repository, renameNamespace }) => { - {renameNamespace && ( - - )} + {renderNamespaceField()} ); diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index b9ea59b069..2cc46f033d 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -27,7 +27,6 @@ import * as types from "../../modules/types"; import { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; -import React from "react"; export const FETCH_REPOS = "scm/repos/FETCH_REPOS"; export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`; @@ -58,6 +57,8 @@ export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`; export const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2"; +export const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy"; + // fetch repos const SORT_BY = "sortBy=namespaceAndName"; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 3de24d64a7..31901e4259 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -32,6 +32,7 @@ import org.mapstruct.ObjectFactory; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.Feature; import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.Command; @@ -43,6 +44,7 @@ import sonia.scm.web.api.RepositoryToHalMapper; import javax.inject.Inject; import java.util.List; +import java.util.Set; import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; @@ -60,6 +62,8 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper strategies; abstract HealthCheckFailureDto toDto(HealthCheckFailure failure); @@ -76,7 +80,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper allStrategies() { - return strategies(new AwesomeNamespaceStrategy(), new SuperNamespaceStrategy(), new MegaNamespaceStrategy()); + return strategies(new AwesomeNamespaceStrategy(), new SuperNamespaceStrategy(), new MegaNamespaceStrategy()); } private Set strategies(NamespaceStrategy... strategies) { @@ -80,6 +80,11 @@ class NamespaceStrategyResourceTest { public String createNamespace(Repository repository) { return "awesome"; } + + @Override + public boolean canBeChanged() { + return false; + } } private static class SuperNamespaceStrategy implements NamespaceStrategy { @@ -87,6 +92,11 @@ class NamespaceStrategyResourceTest { public String createNamespace(Repository repository) { return "super"; } + + @Override + public boolean canBeChanged() { + return false; + } } private static class MegaNamespaceStrategy implements NamespaceStrategy { @@ -94,5 +104,10 @@ class NamespaceStrategyResourceTest { public String createNamespace(Repository repository) { return "mega"; } + + @Override + public boolean canBeChanged() { + return false; + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index a6b34b3fc4..3c958cae74 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -26,6 +26,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; @@ -40,7 +41,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.CustomNamespaceStrategy; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryInitializer; import sonia.scm.repository.RepositoryManager; @@ -56,6 +59,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Set; import java.util.function.Predicate; import static java.util.Collections.singletonList; @@ -72,6 +76,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -105,6 +110,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { private RepositoryInitializer repositoryInitializer; @Mock private ScmConfiguration configuration; + @Mock + private Set strategies; @Captor private ArgumentCaptor> filterCaptor; @@ -129,6 +136,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(scmPathInfoStore.get()).thenReturn(uriInfo); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); + doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator(); SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM); trillian.add(new User("trillian"), REALM); shiro.setSubject( diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index f1ffb2eda0..34a49ccbda 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -21,11 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.util.ThreadContext; import org.junit.After; import org.junit.Before; @@ -34,7 +35,9 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.CustomNamespaceStrategy; import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; @@ -42,13 +45,16 @@ import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.ScmProtocol; import java.net.URI; +import java.util.Set; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Stream.of; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -75,6 +81,8 @@ public class RepositoryToRepositoryDtoMapperTest { private ScmPathInfo uriInfo; @Mock private ScmConfiguration configuration; + @Mock + private Set strategies; @InjectMocks private RepositoryToRepositoryDtoMapperImpl mapper; @@ -88,6 +96,7 @@ public class RepositoryToRepositoryDtoMapperTest { when(scmPathInfoStore.get()).thenReturn(uriInfo); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); + doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator(); } @After diff --git a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java index 7f518254c4..da6bc1c908 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import org.junit.jupiter.api.Test; @@ -66,12 +66,17 @@ class NamespaceStrategyProviderTest { return new LinkedHashSet<>(Arrays.asList(new Trillian(), new Zaphod(), new Arthur())); } - private static class Trillian implements NamespaceStrategy{ + private static class Trillian implements NamespaceStrategy { @Override public String createNamespace(Repository repository) { return "trillian"; } + + @Override + public boolean canBeChanged() { + return false; + } } private static class Zaphod implements NamespaceStrategy { @@ -80,6 +85,11 @@ class NamespaceStrategyProviderTest { public String createNamespace(Repository repository) { return "zaphod"; } + + @Override + public boolean canBeChanged() { + return false; + } } private static class Arthur implements NamespaceStrategy { @@ -88,6 +98,11 @@ class NamespaceStrategyProviderTest { public String createNamespace(Repository repository) { return "arthur"; } + + @Override + public boolean canBeChanged() { + return false; + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java index 9f2f5b7709..7de644468e 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import com.google.common.collect.Sets; @@ -30,7 +30,7 @@ import sonia.scm.ScmConstraintViolationException; import java.util.Collections; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; class NamespaceStrategyValidatorTest { @@ -52,6 +52,11 @@ class NamespaceStrategyValidatorTest { public String createNamespace(Repository repository) { return null; } + + @Override + public boolean canBeChanged() { + return false; + } } }