diff --git a/Jenkinsfile b/Jenkinsfile index 8bb52d6030..e817ac2ba1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -50,6 +50,11 @@ node('docker') { def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}" if (isMainBranch()) { + + stage('Lifecycle') { + nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' + } + stage('Archive') { archiveArtifacts 'scm-webapp/target/scm-webapp.war' archiveArtifacts 'scm-server/target/scm-server-app.*' diff --git a/pom.xml b/pom.xml index 2d8bc13f28..a7108df873 100644 --- a/pom.xml +++ b/pom.xml @@ -351,21 +351,6 @@ test - - - - - commons-beanutils - commons-beanutils - 1.9.3 - - - - commons-collections - commons-collections - 3.2.2 - - @@ -825,11 +810,11 @@ 1.2.3 3.0.1 - 2.0.1 - 3.1.3.Final + 2.1.1 + 3.6.2.Final 1.19.4 2.11.1 - 2.8.6 + 2.9.8 4.0 2.3.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index c4bf3a2e6f..f19d50064d 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -93,6 +93,7 @@ javax.ws.rs javax.ws.rs-api + provided @@ -235,7 +236,6 @@ http://download.oracle.com/javase/6/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ - http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ https://google.github.io/guice/api-docs/${guice.version}/javadoc http://www.slf4j.org/api/ http://shiro.apache.org/static/${shiro.version}/apidocs/ diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index a621f4f697..d960cc6107 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -36,7 +36,6 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.Closeable; -import java.io.IOException; /** * The base class of all handlers. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java new file mode 100644 index 0000000000..cd7d7ecebe --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Context; +import org.mapstruct.Mapping; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +public interface ChangesetToChangesetDtoMapper { + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + ChangesetDto map(Changeset changeset, @Context Repository repository); + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/PersonDto.java diff --git a/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java index 1fa525d4fd..ef0ec8a5ef 100644 --- a/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java +++ b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java @@ -1,24 +1,59 @@ package sonia.scm.filter; -import lombok.extern.slf4j.Slf4j; -import sonia.scm.util.WebUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.ext.Provider; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; import java.util.zip.GZIPOutputStream; -@Provider -@Slf4j -public class GZipResponseFilter implements ContainerResponseFilter { - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { - if (WebUtil.isGzipSupported(requestContext::getHeaderString)) { - log.trace("compress output with gzip"); - GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream()); - responseContext.getHeaders().add("Content-Encoding", "gzip"); - responseContext.setEntityStream(wrappedResponse); +@javax.ws.rs.ext.Provider +public class GZipResponseFilter implements WriterInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(GZipResponseFilter.class); + + private final Provider requestProvider; + + @Inject + public GZipResponseFilter(Provider requestProvider) { + this.requestProvider = requestProvider; + } + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + if (isGZipSupported()) { + LOG.trace("compress output with gzip"); + encodeWithGZip(context); + } else { + context.proceed(); } } + + private void encodeWithGZip(WriterInterceptorContext context) throws IOException { + context.getHeaders().remove(HttpHeaders.CONTENT_LENGTH); + context.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip"); + + OutputStream outputStream = context.getOutputStream(); + GZIPOutputStream compressedOutputStream = new GZIPOutputStream(outputStream); + context.setOutputStream(compressedOutputStream); + try { + context.proceed(); + } finally { + compressedOutputStream.finish(); + context.setOutputStream(outputStream); + } + } + + private boolean isGZipSupported() { + Object encoding = requestProvider.get().getHeader(HttpHeaders.ACCEPT_ENCODING); + return encoding != null && encoding.toString().toLowerCase(Locale.ENGLISH).contains("gzip"); + } } diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java index c80ae9b1c4..0723f44b6c 100644 --- a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java +++ b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java @@ -35,7 +35,6 @@ package sonia.scm.net.ahc; import com.google.common.base.Charsets; import com.google.common.base.Strings; -import com.google.common.collect.HashMultimap; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java index 0bf37054a8..2d65d1cc98 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginLoader.java @@ -35,8 +35,6 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Module; - //~--- JDK imports ------------------------------------------------------------ import java.util.Collection; diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 9941a4253b..663d053ca5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.ConfigurationException; import sonia.scm.io.CommandResult; import sonia.scm.io.ExtendedCommand; +import sonia.scm.plugin.PluginLoader; import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; @@ -67,11 +68,14 @@ public abstract class AbstractSimpleRepositoryHandler verbs; + private Set verbs; /** * Constructs a new {@link RepositoryPermission}. @@ -79,7 +79,7 @@ public class RepositoryPermission implements PermissionObject, Serializable public RepositoryPermission(String name, Collection verbs, boolean groupPermission) { this.name = name; - this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs)); + this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); this.groupPermission = groupPermission; } @@ -109,7 +109,8 @@ public class RepositoryPermission implements PermissionObject, Serializable final RepositoryPermission other = (RepositoryPermission) obj; return Objects.equal(name, other.name) - && CollectionUtils.isEqualCollection(verbs, other.verbs) + && verbs.containsAll(other.verbs) + && verbs.size() == other.verbs.size() && Objects.equal(groupPermission, other.groupPermission); } @@ -209,6 +210,6 @@ public class RepositoryPermission implements PermissionObject, Serializable */ public void setVerbs(Collection verbs) { - this.verbs = verbs; + this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java index baf03a0aef..45dfc0b2b7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -5,7 +5,6 @@ import com.google.common.base.Objects; import com.google.common.base.Strings; import sonia.scm.Validateable; import sonia.scm.repository.Person; -import sonia.scm.util.Util; import java.io.Serializable; diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index a005583256..8d95131ee6 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -39,7 +39,6 @@ import com.google.common.base.Objects; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java index 4b2e46b665..903f86df90 100644 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java @@ -34,8 +34,6 @@ package sonia.scm.security; //~--- JDK imports ------------------------------------------------------------ -import com.google.common.base.Objects; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; diff --git a/scm-core/src/main/java/sonia/scm/util/Comparables.java b/scm-core/src/main/java/sonia/scm/util/Comparables.java new file mode 100644 index 0000000000..1fb0c5e358 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/util/Comparables.java @@ -0,0 +1,88 @@ +package sonia.scm.util; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class Comparables { + + private static final CacheLoader beanInfoCacheLoader = new CacheLoader() { + @Override + public BeanInfo load(Class type) throws IntrospectionException { + return Introspector.getBeanInfo(type); + } + }; + + private static final LoadingCache beanInfoCache = CacheBuilder.newBuilder() + .maximumSize(50) // limit the cache to avoid consuming to much memory on miss usage + .build(beanInfoCacheLoader); + + private Comparables() { + } + + public static Comparator comparator(Class type, String sortBy) { + BeanInfo info = createBeanInfo(type); + PropertyDescriptor propertyDescriptor = findPropertyDescriptor(sortBy, info); + + Method readMethod = propertyDescriptor.getReadMethod(); + checkIfPropertyIsComparable(readMethod, sortBy); + + return new MethodComparator<>(readMethod); + } + + private static void checkIfPropertyIsComparable(Method readMethod, String sortBy) { + checkArgument(isReturnTypeComparable(readMethod), "property %s is not comparable", sortBy); + } + + private static boolean isReturnTypeComparable(Method readMethod) { + return Comparable.class.isAssignableFrom(readMethod.getReturnType()); + } + + private static PropertyDescriptor findPropertyDescriptor(String sortBy, BeanInfo info) { + PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors(); + + Optional sortByPropertyDescriptor = Arrays.stream(propertyDescriptors) + .filter(p -> p.getName().equals(sortBy)) + .findFirst(); + + return sortByPropertyDescriptor.orElseThrow(() -> new IllegalArgumentException("could not find property " + sortBy)); + } + + private static BeanInfo createBeanInfo(Class type) { + return beanInfoCache.getUnchecked(type); + } + + private static class MethodComparator implements Comparator { + + private final Method readMethod; + + private MethodComparator(Method readMethod) { + this.readMethod = readMethod; + } + + @Override + @SuppressWarnings("unchecked") + public int compare(T left, T right) { + try { + Comparable leftResult = (Comparable) readMethod.invoke(left); + Comparable rightResult = (Comparable) readMethod.invoke(right); + return leftResult.compareTo(rightResult); + } catch (IllegalAccessException | InvocationTargetException ex) { + throw new IllegalArgumentException("failed to invoke read method", ex); + } + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java new file mode 100644 index 0000000000..c3648fd4b8 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java @@ -0,0 +1,87 @@ +package sonia.scm.filter; + +import com.google.inject.util.Providers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GZipResponseFilterTest { + + @Mock + private HttpServletRequest request; + + @Mock + private WriterInterceptorContext context; + + @Mock + private MultivaluedMap headers; + + private GZipResponseFilter filter; + + @BeforeEach + void setupObjectUnderTest() { + filter = new GZipResponseFilter(Providers.of(request)); + } + + @Test + void shouldSkipGZipCompression() throws IOException { + when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("deflate, br"); + + filter.aroundWriteTo(context); + + verifySkipped(); + } + + @Test + void shouldSkipGZipCompressionWithoutAcceptEncodingHeader() throws IOException { + filter.aroundWriteTo(context); + + verifySkipped(); + } + + private void verifySkipped() throws IOException { + verify(context, never()).getOutputStream(); + verify(context).proceed(); + } + + + @Nested + class AcceptGZipEncoding { + + @BeforeEach + void setUpContext() { + when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("gzip, deflate, br"); + when(context.getHeaders()).thenReturn(headers); + when(context.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + } + + @Test + void shouldEncode() throws IOException { + filter.aroundWriteTo(context); + + verify(headers).remove(HttpHeaders.CONTENT_LENGTH); + verify(headers).add(HttpHeaders.CONTENT_ENCODING, "gzip"); + + verify(context).setOutputStream(any(GZIPOutputStream.class)); + verify(context, times(2)).setOutputStream(any(OutputStream.class)); + } + + } + + +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java index b7fa9ae84a..92ca488ddf 100644 --- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java @@ -36,7 +36,7 @@ import com.google.common.io.ByteSource; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.UnsupportedEncodingException; + import org.junit.Test; import static org.junit.Assert.*; import static org.hamcrest.Matchers.*; diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java index 2e9383b2e2..d65358a66e 100644 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java @@ -46,4 +46,13 @@ class RepositoryPermissionTest { assertThat(permission1).isNotEqualTo(permission2); } + + @Test + void shouldBeEqualWithRedundantVerbs() { + RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one", "two"), false); + RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two"), false); + permission2.setVerbs(asList("one", "two", "two")); + + assertThat(permission1).isEqualTo(permission2); + } } diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java index af4bf37915..7ddeabd8ac 100644 --- a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -1,7 +1,6 @@ package sonia.scm.security; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UnknownAccountException; diff --git a/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java new file mode 100644 index 0000000000..50ae2254e6 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java @@ -0,0 +1,57 @@ +package sonia.scm.util; + +import org.junit.jupiter.api.Test; + +import java.util.Comparator; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ComparablesTest { + + @Test + void shouldCompare() { + One a = new One("a"); + One b = new One("b"); + + Comparator comparable = Comparables.comparator(One.class, "value"); + assertThat(comparable.compare(a, b)).isEqualTo(-1); + } + + @Test + void shouldThrowAnExceptionForNonExistingField() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "awesome")); + } + + @Test + void shouldThrowAnExceptionForNonComparableField() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "nonComparable")); + } + + @Test + void shouldThrowAnExceptionIfTheFieldHasNoGetter() { + assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "incredible")); + } + + private static class One { + + private String value; + private String incredible; + private NonComparable nonComparable; + + One(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public NonComparable getNonComparable() { + return nonComparable; + } + } + + private static class NonComparable {} + +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java index 5b5c00a298..579ef75b71 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java @@ -37,9 +37,6 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import sonia.scm.SCMContextProvider; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.security.KeyGenerator; diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 1ce0508616..5b24096eb5 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -38,7 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.GenericDAO; import sonia.scm.ModelObject; -import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.store.ConfigurationStore; import java.util.Collection; diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java index d812eedc35..4b3d9b0f28 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java @@ -1,6 +1,5 @@ package sonia.scm.xml; -import com.google.common.base.Charsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index 3c67ca3dc3..c49a65bea2 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -36,7 +36,6 @@ package sonia.scm.it; import org.apache.http.HttpStatus; import org.assertj.core.api.Assertions; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java index 9386f1d9c5..0a5693ad2e 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -5,10 +5,8 @@ import io.restassured.response.Response; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.user.User; import sonia.scm.web.VndMediaType; -import java.net.ConnectException; import java.util.List; import java.util.Map; import java.util.function.Consumer; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index e078b04b08..7cda4bc9d3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -18,8 +18,6 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; -import static sonia.scm.ContextEntry.ContextBuilder.entity; - /** * RESTful Web Service Resource to manage the configuration of the git plugin. */ diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 95225c9e30..63800e8a02 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -44,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.GitRepositoryServiceProvider; import sonia.scm.schedule.Scheduler; import sonia.scm.schedule.Task; @@ -103,9 +104,10 @@ public class GitRepositoryHandler public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver, - GitWorkdirFactory workdirFactory) + GitWorkdirFactory workdirFactory, + PluginLoader pluginLoader) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, pluginLoader); this.scheduler = scheduler; this.workdirFactory = workdirFactory; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java index a2114a1b6a..6db7d694d5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java @@ -35,7 +35,6 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.inject.Inject; import org.eclipse.jgit.errors.RepositoryNotFoundException; diff --git a/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js b/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js index e34a0ef96f..aadb58eed6 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js +++ b/scm-plugins/scm-git-plugin/src/main/js/RepositoryConfig.js @@ -2,7 +2,7 @@ import React from "react"; -import {apiClient, BranchSelector, ErrorPage, Loading, SubmitButton} from "@scm-manager/ui-components"; +import {apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton} from "@scm-manager/ui-components"; import type {Branch, Repository} from "@scm-manager/ui-types"; import {translate} from "react-i18next"; @@ -113,6 +113,7 @@ class RepositoryConfig extends React.Component { if (!(loadingBranches || loadingDefaultBranch)) { return ( <> + {this.renderBranchChangedNotification()}
{ disabled={!this.state.selectedBranchName} /> +
); } else { diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.js b/scm-plugins/scm-git-plugin/src/main/js/index.js index a066247dde..43e3950beb 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.js +++ b/scm-plugins/scm-git-plugin/src/main/js/index.js @@ -27,14 +27,9 @@ binder.bind( ); binder.bind("repos.repository-avatar", GitAvatar, gitPredicate); -cfgBinder.bindRepository( - "/configuration", - "scm-git-plugin.repo-config.link", - "configuration", - RepositoryConfig -); -// global config +binder.bind("repo-config.route", RepositoryConfig, gitPredicate); +// global config cfgBinder.bindGlobal( "/git", "scm-git-plugin.config.link", diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json index 8902a57f32..cd88897e74 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json @@ -27,6 +27,7 @@ }, "repo-config": { "link": "Konfiguration", + "title": "Git Einstellungen", "default-branch": "Standard Branch", "submit": "Speichern", "error": { diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index 2b579801dd..551573fb72 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -27,6 +27,7 @@ }, "repo-config": { "link": "Configuration", + "title": "Git Settings", "default-branch": "Default branch", "submit": "Submit", "error": { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java index ed34db3008..6a05875aa9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import static org.junit.Assert.*; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java index 0c28a28e59..8e657b1050 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -17,7 +17,7 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitRepositoryHandler; @@ -29,6 +29,7 @@ import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.web.GitVndMediaType; import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -100,7 +101,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readWrite") - public void shouldGetGitConfig() throws URISyntaxException { + public void shouldGetGitConfig() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -115,7 +116,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readWrite") - public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException { + public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException { when(repositoryHandler.getConfig()).thenReturn(null); MockHttpResponse response = get(); @@ -126,7 +127,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException { + public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -159,7 +160,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "writeOnly") - public void shouldReadDefaultRepositoryConfig() throws URISyntaxException { + public void shouldReadDefaultRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); @@ -176,7 +177,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException { + public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException, UnsupportedEncodingException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X"); @@ -193,7 +194,7 @@ public class GitConfigResourceTest { @Test @SubjectAware(username = "writeOnly") - public void shouldReadStoredRepositoryConfig() throws URISyntaxException { + public void shouldReadStoredRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig(); gitRepositoryConfig.setDefaultBranch("test"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java index 40cf36e8dd..62fa8d33b4 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java @@ -11,10 +11,9 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; -import java.io.File; import java.net.URI; import static org.junit.Assert.assertEquals; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java index d2942d08a3..a1e349dd57 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitRepositoryConfigEnricherTest.java @@ -14,9 +14,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.api.Command; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.JsonEnricherContext; import sonia.scm.web.VndMediaType; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java index 23b3110567..3362c8a22b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHeadModifierTest.java @@ -40,7 +40,7 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.io.File; import java.io.IOException; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index cb10e15271..66ec320067 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -94,7 +94,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { RepositoryLocationResolver locationResolver, File directory) { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - scheduler, locationResolver, gitWorkdirFactory); + scheduler, locationResolver, gitWorkdirFactory, null); repositoryHandler.init(contextProvider); GitConfig config = new GitConfig(); @@ -108,7 +108,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - scheduler, locationResolver, gitWorkdirFactory); + scheduler, locationResolver, gitWorkdirFactory, null); GitConfig config = new GitConfig(); config.setDisabled(false); config.setGcExpression("gc exp"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index 630236b20b..f2a4ed4954 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -35,10 +35,8 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.After; -import org.junit.Before; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.GitRepositoryConfig; -import sonia.scm.store.InMemoryConfigurationStore; import sonia.scm.store.InMemoryConfigurationStoreFactory; /** diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java index 817e4641dd..c8d260d503 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java @@ -35,11 +35,9 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.BlameLine; import sonia.scm.repository.BlameResult; import sonia.scm.repository.GitRepositoryConfig; -import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 2ff3c73420..1feceba652 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -32,11 +32,9 @@ package sonia.scm.repository.spi; import org.junit.Test; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.GitRepositoryConfig; -import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; import java.util.Collection; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index e2ab85d9a7..06e9b17fe7 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -36,10 +36,8 @@ package sonia.scm.repository.spi; import com.google.common.io.Files; import org.junit.Test; -import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.ClearRepositoryCacheEvent; import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.Modifications; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java index dc0822deba..4ed9d5a46a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java @@ -42,7 +42,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java index 785aa399b1..27fdc7a296 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java @@ -35,14 +35,12 @@ package sonia.scm.installer; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; -import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ import java.io.File; -import java.io.IOException; + import sonia.scm.net.ahc.AdvancedHttpClient; /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java index 4b6998f09a..b1d431c742 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java @@ -39,7 +39,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.io.INIConfiguration; import sonia.scm.io.INIConfigurationReader; -import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.util.ValidationUtil; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index c2c0439fc1..7db9a8becb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -51,6 +51,7 @@ import sonia.scm.io.INIConfigurationReader; import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.HgRepositoryServiceProvider; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; @@ -111,9 +112,10 @@ public class HgRepositoryHandler @Inject public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, Provider hgContextProvider, - RepositoryLocationResolver repositoryLocationResolver) + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, pluginLoader); this.hgContextProvider = hgContextProvider; try diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index f351ffa572..0897a191a1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -41,7 +41,6 @@ import com.aragost.javahg.internals.AbstractCommand; import com.aragost.javahg.internals.HgInputStream; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import sonia.scm.repository.FileObject; import sonia.scm.repository.SubRepository; @@ -52,7 +51,6 @@ import java.io.IOException; import java.util.Deque; import java.util.LinkedList; -import java.util.List; /** * Mercurial command to list files of a repository. diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index 93b9699fc9..18b716b665 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -44,7 +44,6 @@ import sonia.scm.web.filter.PermissionFilter; import sonia.scm.repository.HgRepositoryHandler; -import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.Set; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java index 4b66444bbe..1f88bfe665 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java @@ -14,7 +14,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java index 524e33e265..6e181f4886 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java @@ -3,11 +3,9 @@ package sonia.scm.api.v2.resources; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; -import java.io.File; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java index 65b9c262cb..bcd9543d28 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java @@ -14,7 +14,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.inject.Provider; import javax.servlet.http.HttpServletResponse; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java index 7cae1d9f7e..80f8ec32b1 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java @@ -6,7 +6,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.net.URI; import java.util.Arrays; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java index f1558b6efb..473ddfe4b4 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java @@ -17,7 +17,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.installer.HgPackage; import sonia.scm.installer.HgPackageReader; import sonia.scm.net.ahc.AdvancedHttpClient; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java index c4431da6d5..0b5d7b14d0 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java @@ -6,7 +6,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.installer.HgPackage; import sonia.scm.installer.HgPackages; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java index df59954971..e0253ad86a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java @@ -16,15 +16,15 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; import javax.inject.Provider; import javax.servlet.http.HttpServletResponse; -import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -99,7 +99,7 @@ public class HgConfigResourceTest { @Test @SubjectAware(username = "readWrite") - public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException { + public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException, UnsupportedEncodingException { when(repositoryHandler.getConfig()).thenReturn(null); MockHttpResponse response = get(); @@ -110,7 +110,7 @@ public class HgConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException { + public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java index 84343cdf72..a3430aac43 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java @@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources; import sonia.scm.installer.HgPackage; import sonia.scm.repository.HgConfig; -import java.io.File; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java index 81c50f3d58..d4bc8be549 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java @@ -11,7 +11,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.HgConfig; import java.net.URI; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java index c45d9ab358..ed222f5119 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java @@ -77,7 +77,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { - HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver); + HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null); handler.init(contextProvider); HgTestUtil.checkForSkip(handler); @@ -87,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { - HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver); + HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null); HgConfig hgConfig = new HgConfig(); hgConfig.setHgBinary("hg"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index 68f7e18a76..131dad0837 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -105,7 +105,7 @@ public final class HgTestUtil RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver()); HgRepositoryHandler handler = - new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver); + new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null); Path repoDir = directory.toPath(); when(repoDao.getPath(any())).thenReturn(repoDir); handler.init(context); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java index 7d74024630..efe9983951 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java @@ -8,7 +8,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import static org.mockito.Matchers.anyInt; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index b3a4a0c2a4..f9bc77bbda 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -48,8 +48,6 @@ import javax.servlet.http.HttpServletRequest; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES; import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.BOOKMARKS; import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java index 519dadfd6c..9237127c88 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java @@ -37,7 +37,7 @@ import com.google.common.collect.Lists; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index 86f99cd517..639a16968c 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -53,9 +53,9 @@ import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.SvnRepositoryServiceProvider; -import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.Util; @@ -97,9 +97,10 @@ public class SvnRepositoryHandler @Inject public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory, HookEventFacade eventFacade, - RepositoryLocationResolver repositoryLocationResolver) + RepositoryLocationResolver repositoryLocationResolver, + PluginLoader pluginLoader) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, pluginLoader); // register logger SVNDebugLog.setDefaultLog(new SVNKitLogger()); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index e2f58b593b..df266a11af 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -36,7 +36,6 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; -import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tmatesoft.svn.core.SVNDirEntry; @@ -53,7 +52,6 @@ import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; import java.util.Collection; -import java.util.List; //~--- JDK imports ------------------------------------------------------------ diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java index 8ab947fbaf..27ca6d5635 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Compatibility; import sonia.scm.repository.SvnConfig; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java index 3077bb34f3..f7ccf039b2 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java @@ -16,14 +16,14 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.SvnConfig; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.web.SvnVndMediaType; import javax.servlet.http.HttpServletResponse; -import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -98,7 +98,7 @@ public class SvnConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException { + public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpResponse response = get(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java index 6bbff499e1..07ead15322 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java @@ -11,11 +11,10 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Compatibility; import sonia.scm.repository.SvnConfig; -import java.io.File; import java.net.URI; import static org.junit.Assert.assertEquals; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java index 7b22e15c94..c81c6311e1 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java @@ -32,14 +32,10 @@ package sonia.scm.repository; -import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.spi.HookEventFacade; -import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; @@ -47,7 +43,7 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -93,7 +89,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { - SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver); + SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, locationResolver, null); handler.init(contextProvider); @@ -109,7 +105,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { public void getDirectory() { when(factory.withType(any())).thenCallRealMethod(); SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory, - facade, locationResolver); + facade, locationResolver, null); SvnConfig svnConfig = new SvnConfig(); repositoryHandler.setConfig(svnConfig); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java index f2511a9ad9..0cfeaa3a1c 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java @@ -39,8 +39,6 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; -import java.io.IOException; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; diff --git a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java index 3efe78c820..ccaeee8631 100644 --- a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java +++ b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java @@ -59,7 +59,7 @@ public class DummyRepositoryHandler private final Set existingRepoNames = new HashSet<>(); public DummyRepositoryHandler(ConfigurationStoreFactory storeFactory, RepositoryLocationResolver repositoryLocationResolver) { - super(storeFactory, repositoryLocationResolver); + super(storeFactory, repositoryLocationResolver, null); } @Override diff --git a/scm-ui-components/packages/ui-components/src/BranchSelector.js b/scm-ui-components/packages/ui-components/src/BranchSelector.js index 99c93fd677..c473527472 100644 --- a/scm-ui-components/packages/ui-components/src/BranchSelector.js +++ b/scm-ui-components/packages/ui-components/src/BranchSelector.js @@ -13,10 +13,11 @@ const styles = { minWidthOfLabel: { minWidth: "4.5rem" }, - wrapper: { - padding: "1rem 1.5rem 0.25rem 1.5rem", - border: "1px solid #eee", - borderRadius: "5px 5px 0 0" + labelSizing: { + fontSize: "1rem !important" + }, + noBottomMargin: { + marginBottom: "0 !important" } }; @@ -52,9 +53,9 @@ class BranchSelector extends React.Component { return (
{ classes.minWidthOfLabel )} > - +
-
+
void }; class DownloadButton extends React.Component { render() { - const { displayName, url } = this.props; + const { displayName, url, disabled, onClick } = this.props; + const onClickOrDefault = !!onClick ? onClick : () => {}; return ( - + diff --git a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js index 96e8c630b8..56964b016b 100644 --- a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js +++ b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js @@ -72,6 +72,33 @@ class ConfigurationBinder { binder.bind("repository.route", RepoRoute, repoPredicate); } + bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) { + + // create predicate based on the link name of the current repository route + // if the linkname is not available, the navigation link and the route are not bound to the extension points + const repoPredicate = (props: Object) => { + return props.repository && props.repository._links && props.repository._links[linkName]; + }; + + // create NavigationLink with translated label + const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => { + return this.navLink(url + "/settings" + to, labelI18nKey, t); + }); + + // bind navigation link to extension point + binder.bind("repository.subnavigation", RepoNavLink, repoPredicate); + + + // route for global configuration, passes the current repository to component + const RepoRoute = ({url, repository, ...additionalProps}) => { + const link = repository._links[linkName].href; + return this.route(url + "/settings" + to, ); + }; + + // bind config route to extension point + binder.bind("repository.route", RepoRoute, repoPredicate); + } + } export default new ConfigurationBinder(); diff --git a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js index 013014cd98..24b1ced28a 100644 --- a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js +++ b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js @@ -10,7 +10,8 @@ type Props = { buttonLabel: string, fieldLabel: string, errorMessage: string, - helpText?: string + helpText?: string, + validateEntry?: string => boolean }; type State = { @@ -25,6 +26,15 @@ class AddEntryToTableField extends React.Component { }; } + isValid = () => { + const {validateEntry} = this.props; + if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) { + return true; + } else { + return validateEntry(this.state.entryToAdd); + } + }; + render() { const { disabled, @@ -39,7 +49,7 @@ class AddEntryToTableField extends React.Component { label={fieldLabel} errorMessage={errorMessage} onChange={this.handleAddEntryChange} - validationError={false} + validationError={!this.isValid()} value={this.state.entryToAdd} onReturnPressed={this.appendEntry} disabled={disabled} @@ -48,7 +58,7 @@ class AddEntryToTableField extends React.Component {
); diff --git a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js index 53b124ef31..98c3138a8f 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js +++ b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js @@ -28,7 +28,7 @@ class NavLink extends React.Component { let showIcon = null; if (icon) { - showIcon = (<>{" "}); + showIcon = (<>{" "}); } return ( diff --git a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js index f2401729d6..897c63138e 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js +++ b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js @@ -50,8 +50,19 @@ class PrimaryNavigation extends React.Component { createNavigationItems = () => { const navigationItems = []; + const { t, links } = this.props; + + const props = { + links, + label: t("primary-navigation.first-menu") + }; const append = this.createNavigationAppender(navigationItems); + if (binder.hasExtension("primary-navigation.first-menu", props)) { + navigationItems.push( + + ); + } append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories"); append("/users", "/(user|users)", "primary-navigation.users", "users"); append("/groups", "/(group|groups)", "primary-navigation.groups", "groups"); diff --git a/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js b/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js new file mode 100644 index 0000000000..0a6612a173 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js @@ -0,0 +1,65 @@ +//@flow +import * as React from "react"; +import { Link, Route } from "react-router-dom"; + +type Props = { + to: string, + icon?: string, + label: string, + activeOnlyWhenExact?: boolean, + activeWhenMatch?: (route: any) => boolean, + children?: React.Node +}; + +class SubNavigation extends React.Component { + static defaultProps = { + activeOnlyWhenExact: false + }; + + isActive(route: any) { + const { activeWhenMatch } = this.props; + return route.match || (activeWhenMatch && activeWhenMatch(route)); + } + + renderLink = (route: any) => { + const { to, icon, label } = this.props; + + let defaultIcon = "fas fa-cog"; + if (icon) { + defaultIcon = icon; + } + + let children = null; + if (this.isActive(route)) { + children =
    {this.props.children}
; + } + + return ( +
  • + + {label} + + {children} +
  • + ); + }; + + render() { + const { to, activeOnlyWhenExact } = this.props; + + // removes last part of url + let parents = to.split("/"); + parents.splice(-1, 1); + let parent = parents.join("/"); + + return ( + + ); + } +} + +export default SubNavigation; diff --git a/scm-ui-components/packages/ui-components/src/navigation/index.js b/scm-ui-components/packages/ui-components/src/navigation/index.js index ca82073b56..b696f98328 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/index.js +++ b/scm-ui-components/packages/ui-components/src/navigation/index.js @@ -3,6 +3,7 @@ export { default as NavAction } from "./NavAction.js"; export { default as NavLink } from "./NavLink.js"; export { default as Navigation } from "./Navigation.js"; +export { default as SubNavigation } from "./SubNavigation.js"; export { default as PrimaryNavigation } from "./PrimaryNavigation.js"; export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js"; export { default as Section } from "./Section.js"; diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetAuthor.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetAuthor.js index 6ee76e3927..4035cbd899 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetAuthor.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetAuthor.js @@ -28,7 +28,7 @@ class ChangesetAuthor extends React.Component { renderWithMail(name: string, mail: string) { const { t } = this.props; return ( -
    + {name} ); @@ -38,7 +38,7 @@ class ChangesetAuthor extends React.Component { const { t } = this.props; return ( <> - {t("changesets.author.prefix")} {child} + {t("changeset.author.prefix")} {child} { - {t("changesets.buttons.details")} + {t("changeset.buttons.details")} diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js index 857ff8c827..d5d3c4e665 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js @@ -25,7 +25,7 @@ class ChangesetDiff extends React.Component { render() { const { changeset, t } = this.props; if (!this.isDiffSupported(changeset)) { - return {t("changesets.diff.not-supported")}; + return {t("changeset.diffNotSupported")}; } else { const url = this.createUrl(changeset); return ; diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js index 74ec816369..4cd8ec319c 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetList.js @@ -21,7 +21,7 @@ class ChangesetList extends React.Component { /> ); }); - return
    {content}
    ; + return <>{content}; } } diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetRow.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetRow.js index 10287ec71f..90a568f615 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetRow.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetRow.js @@ -70,7 +70,7 @@ class ChangesetRow extends React.Component {

    @@ -89,14 +89,14 @@ class ChangesetRow extends React.Component {

    diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetTagsCollapsed.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetTagsCollapsed.js index 50fb36644d..50b09d4d65 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetTagsCollapsed.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetTagsCollapsed.js @@ -18,7 +18,7 @@ class ChangesetTagsCollapsed extends React.Component { const message = tags.map((tag) => tag.name).join(", "); return ( - + ); } diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index c38dff2468..f32d764ce8 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -43,8 +43,10 @@ "previous": "Zurück" }, "profile": { - "navigation-label": "Navigation", - "actions-label": "Aktionen", + "navigationLabel": "Profil Navigation", + "informationNavLink": "Information", + "changePasswordNavLink": "Passwort ändern", + "settingsNavLink": "Einstellungen", "username": "Benutzername", "displayName": "Anzeigename", "mail": "E-Mail", diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index e26c2cddac..5767a2b376 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -1,12 +1,10 @@ { "config": { - "navigation-title": "Navigation" - }, - "global-config": { + "navigationLabel": "Einstellungs Navigation", + "globalConfigurationNavLink": "Globale Einstellungen", "title": "Einstellungen", - "navigation-label": "Globale Einstellungen", - "error-title": "Fehler", - "error-subtitle": "Unbekannter Einstellungen Fehler" + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Einstellungen Fehler" }, "config-form": { "submit": "Speichern", diff --git a/scm-ui/public/locales/de/groups.json b/scm-ui/public/locales/de/groups.json index 768704bf38..1b2504a644 100644 --- a/scm-ui/public/locales/de/groups.json +++ b/scm-ui/public/locales/de/groups.json @@ -11,13 +11,16 @@ "title": "Gruppen", "subtitle": "Verwaltung der Gruppen" }, - "single-group": { - "error-title": "Fehler", - "error-subtitle": "Unbekannter Gruppen Fehler", - "navigation-label": "Navigation", - "actions-label": "Aktionen", - "information-label": "Informationen", - "back-label": "Zurück" + "singleGroup": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Gruppen Fehler", + "menu": { + "navigationLabel": "Gruppen Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "setPermissionsNavLink": "Berechtigungen" + } }, "add-group": { "title": "Gruppe erstellen", @@ -43,28 +46,26 @@ "placeholder": "Benutzername eingeben", "loading": "Suche...", "no-options": "Kein Vorschlag für Benutzername verfügbar" - }, - -"group-form": { + }, + "groupForm": { + "subtitle": "Gruppe bearbeiten", "submit": "Speichern", - "name-error": "Name ist ungültig", - "description-error": "Beschreibung ist ungültig", + "nameError": "Name ist ungültig", + "descriptionError": "Beschreibung ist ungültig", "help": { "nameHelpText": "Eindeutiger Name der Gruppe", "descriptionHelpText": "Eine kurze Beschreibung der Gruppe", "memberHelpText": "Benutzername des Mitglieds der Gruppe" } }, - "delete-group-button": { - "label": "Löschen", - "confirm-alert": { + "deleteGroup": { + "subtitle": "Gruppe löschen", + "button": "Löschen", + "confirmAlert": { "title": "Gruppe löschen", "message": "Soll die Gruppe wirklich gelöscht werden?", "submit": "Ja", "cancel": "Nein" } - }, - "set-permissions-button": { - "label": "Berechtigungen ändern" } } diff --git a/scm-ui/public/locales/de/permissions.json b/scm-ui/public/locales/de/permissions.json index d1280808e5..57c061743a 100644 --- a/scm-ui/public/locales/de/permissions.json +++ b/scm-ui/public/locales/de/permissions.json @@ -1,8 +1,6 @@ { - "form": { - "submit-button": { - "label": "Berechtigungen speichern" - }, - "set-permissions-successful": "Berechtigungen erfolgreich gespeichert" + "setPermissions": { + "button": "Berechtigungen speichern", + "setPermissionsSuccessful": "Berechtigungen erfolgreich gespeichert" } } diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index 13c302aa67..922a2b270f 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -11,41 +11,58 @@ "name-invalid": "Der Name des Repository ist ungültig", "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" }, + "help": { + "nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.", + "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", + "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", + "descriptionHelpText": "Eine kurze Beschreibung des Repository." + }, + "repositoryRoot": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Repository Fehler", + "menu": { + "navigationLabel": "Repository Navigation", + "informationNavLink": "Informationen", + "historyNavLink": "Commits", + "sourcesNavLink": "Sources", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "permissionsNavLink": "Berechtigungen" + } + }, "overview": { "title": "Repositories", "subtitle": "Übersicht aller verfügbaren Repositories", - "create-button": "Repository erstellen" - }, - "repository-root": { - "error-title": "Fehler", - "error-subtitle": "Unbekannter Repository Fehler", - "actions-label": "Aktionen", - "back-label": "Zurück", - "navigation-label": "Navigation", - "history": "Commits", - "information": "Informationen", - "permissions": "Berechtigungen", - "sources": "Sources" + "createButton": "Repository erstellen" }, "create": { "title": "Repository erstellen", "subtitle": "Erstellen eines neuen Repository" }, - "repository-form": { - "submit": "Speichern" + "changesets": { + "errorTitle": "Fehler", + "errorSubtitle": "Changesets konnten nicht abgerufen werden", + "branchSelectorLabel": "Branches" }, - "edit-nav-link": { - "label": "Bearbeiten" - }, - "delete-nav-action": { - "label": "Löschen", - "confirm-alert": { - "title": "Repository löschen", - "message": "Soll das Repository wirklich gelöscht werden?", - "submit": "Ja", - "cancel": "Nein" + "changeset": { + "description": "Beschreibung", + "summary": "Changeset {{id}} wurde committet {{time}}", + "shortSummary": "Committet {{id}} {{time}}", + "tags": "Tags", + "diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt", + "author": { + "prefix": "Verfasst von", + "mailto": "Mail senden an" + }, + "buttons": { + "details": "Details", + "sources": "Sources" } }, + "repositoryForm": { + "subtitle": "Repository bearbeiten", + "submit": "Speichern" + }, "sources": { "file-tree": { "name": "Name", @@ -65,36 +82,8 @@ "size": "Größe" } }, - "changesets": { - "diff": { - "not-supported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt" - }, - "error-title": "Fehler", - "error-subtitle": "Changesets konnten nicht abgerufen werden", - "changeset": { - "id": "ID", - "description": "Beschreibung", - "contact": "Kontakt", - "date": "Datum", - "summary": "Changeset {{id}} wurde committet {{time}}", - "short-summary": "Committet {{id}} {{time}}" - }, - "tags": "Tags", - "author": { - "name": "Autor", - "mail": "Mail", - "prefix": "Verfasst von", - "mailto": "Mail senden an" - }, - "buttons": { - "details": "Details", - "sources": "Sources" - } - }, - "branch-selector": { - "label": "Branches" - }, "permission": { + "title": "Berechtigungen bearbeiten", "user": "Benutzer", "group": "Gruppe", "error-title": "Fehler", @@ -106,7 +95,7 @@ "user-permission": "Benutzerberechtigung", "edit-permission": { "delete-button": "Löschen", - "save-button": "Änderungen Speichern" + "save-button": "Änderungen speichern" }, "advanced-button": { "label": "Erweitert" @@ -146,10 +135,14 @@ } } }, - "help": { - "nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.", - "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", - "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", - "descriptionHelpText": "Eine kurze Beschreibung des Repository." + "deleteRepo": { + "subtitle": "Repository löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Repository löschen", + "message": "Soll das Repository wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } } } diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json index 31b954d996..b38dd9c2fb 100644 --- a/scm-ui/public/locales/de/users.json +++ b/scm-ui/public/locales/de/users.json @@ -10,59 +10,55 @@ "creationDate": "Erstellt", "lastModified": "Zuletzt bearbeitet" }, - "users": { - "title": "Benutzer", - "subtitle": "Verwaltung der Benutzer" - }, - "create-user-button": { - "label": "Benutzer erstellen" - }, - "delete-user-button": { - "label": "Löschen", - "confirm-alert": { - "title": "Benutzer löschen", - "message": "Soll der Benutzer wirklich gelöscht werden?", - "submit": "Ja", - "cancel": "Nein" - } - }, - "edit-user-button": { - "label": "Bearbeiten" - }, - "set-password-button": { - "label": "Passwort ändern" - }, - "set-permissions-button": { - "label": "Berechtigungen ändern" - }, - "user-form": { - "submit": "Speichern" - }, - "add-user": { - "title": "Benutzer erstellen", - "subtitle": "Erstellen eines neuen Benutzers" - }, - "single-user": { - "error-title": "Fehler", - "error-subtitle": "Unbekannter Benutzer Fehler", - "navigation-label": "Navigation", - "actions-label": "Aktionen", - "information-label": "Informationen", - "back-label": "Zurück" - }, "validation": { "mail-invalid": "Diese E-Mail ist ungültig", "name-invalid": "Dieser Name ist ungültig", "displayname-invalid": "Dieser Anzeigename ist ungültig" }, - "password": { - "set-password-successful": "Das Passwort wurde erfolgreich gespeichert." - }, "help": { "usernameHelpText": "Einzigartiger Name des Benutzers", "displayNameHelpText": "Anzeigename des Benutzers", "mailHelpText": "E-Mail Adresse des Benutzers", "adminHelpText": "Ein Administrator kann Repositories, Gruppen und Benutzer erstellen, bearbeiten und löschen.", "activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers" + }, + "users": { + "title": "Benutzer", + "subtitle": "Verwaltung der Benutzer", + "createButton": "Benutzer erstellen" + }, + "singleUser": { + "errorTitle": "Fehler", + "errorSubtitle": "Unbekannter Benutzer Fehler", + "menu": { + "navigationLabel": "Benutzer Navigation", + "informationNavLink": "Informationen", + "settingsNavLink": "Einstellungen", + "generalNavLink": "Generell", + "setPasswordNavLink": "Passwort", + "setPermissionsNavLink": "Berechtigungen" + } + }, + "addUser": { + "title": "Benutzer erstellen", + "subtitle": "Erstellen eines neuen Benutzers" + }, + "deleteUser": { + "subtitle": "Benutzer löschen", + "button": "Löschen", + "confirmAlert": { + "title": "Benutzer löschen", + "message": "Soll der Benutzer wirklich gelöscht werden?", + "submit": "Ja", + "cancel": "Nein" + } + }, + "singleUserPassword": { + "button": "Passwort setzen", + "setPasswordSuccessful": "Das Passwort wurde erfolgreich gespeichert." + }, + "userForm": { + "subtitle": "Benutzer bearbeiten", + "button": "Speichern" } } diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index 4788f7b39f..fed749a200 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -43,8 +43,10 @@ "previous": "Previous" }, "profile": { - "navigation-label": "Navigation", - "actions-label": "Actions", + "navigationLabel": "Profile Navigation", + "informationNavLink": "Information", + "changePasswordNavLink": "Change password", + "settingsNavLink": "Settings", "username": "Username", "displayName": "Display Name", "mail": "E-Mail", @@ -67,6 +69,6 @@ "passwordInvalid": "Password has to be between 6 and 32 characters", "passwordConfirmFailed": "Passwords have to be identical", "submit": "Submit", - "changedSuccessfully": "Password successfully changed" + "changedSuccessfully": "Password changed successfully" } } diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index bdc7ec21f6..b08c5c2d1b 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -1,12 +1,10 @@ { "config": { - "navigation-title": "Navigation" - }, - "global-config": { + "navigationLabel": "Configuration Navigation", + "globalConfigurationNavLink": "Global Configuration", "title": "Configuration", - "navigation-label": "Global Configuration", - "error-title": "Error", - "error-subtitle": "Unknown Config Error" + "errorTitle": "Error", + "errorSubtitle": "Unknown Config Error" }, "config-form": { "submit": "Submit", diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index 60a10e4302..3b34e5722b 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -11,13 +11,16 @@ "title": "Groups", "subtitle": "Create, read, update and delete groups" }, - "single-group": { - "error-title": "Error", - "error-subtitle": "Unknown group error", - "navigation-label": "Navigation", - "actions-label": "Actions", - "information-label": "Information", - "back-label": "Back" + "singleGroup": { + "errorTitle": "Error", + "errorSubtitle": "Unknown group error", + "menu": { + "navigationLabel": "Group Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "setPermissionsNavLink": "Permissions" + } }, "add-group": { "title": "Create Group", @@ -43,28 +46,26 @@ "placeholder": "Enter Member", "loading": "Loading...", "no-options": "No suggestion available" - }, - -"group-form": { + }, + "groupForm": { + "subtitle": "Edit Group", "submit": "Submit", - "name-error": "Group name is invalid", - "description-error": "Description is invalid", + "nameError": "Group name is invalid", + "descriptionError": "Description is invalid", "help": { "nameHelpText": "Unique name of the group", "descriptionHelpText": "A short description of the group", "memberHelpText": "Usernames of the group members" } }, - "delete-group-button": { - "label": "Delete", - "confirm-alert": { + "deleteGroup": { + "subtitle": "Delete Group", + "button": "Delete", + "confirmAlert": { "title": "Delete Group", "message": "Do you really want to delete the group?", "submit": "Yes", "cancel": "No" } - }, - "set-permissions-button": { - "label": "Set Permissions" } } diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json index f5ba065ced..9c3663e77b 100644 --- a/scm-ui/public/locales/en/permissions.json +++ b/scm-ui/public/locales/en/permissions.json @@ -1,8 +1,6 @@ { - "form": { - "submit-button": { - "label": "Set Permissions" - }, - "set-permissions-successful": "Permissions set successfully" + "setPermissions": { + "button": "Set permissions", + "setPermissionsSuccessful": "Permissions set successfully" } } diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 94e6dd7fdb..bdb0491489 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -11,41 +11,58 @@ "name-invalid": "The repository name is invalid", "contact-invalid": "Contact must be a valid mail address" }, + "help": { + "nameHelpText": "The name of the repository. This name will be part of the repository url.", + "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", + "contactHelpText": "Email address of the person who is responsible for this repository.", + "descriptionHelpText": "A short description of the repository." + }, + "repositoryRoot": { + "errorTitle": "Error", + "errorSubtitle": "Unknown repository error", + "menu": { + "navigationLabel": "Repository Navigation", + "informationNavLink": "Information", + "historyNavLink": "Commits", + "sourcesNavLink": "Sources", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "permissionsNavLink": "Permissions" + } + }, "overview": { "title": "Repositories", "subtitle": "Overview of available repositories", - "create-button": "Create Repository" - }, - "repository-root": { - "error-title": "Error", - "error-subtitle": "Unknown repository error", - "actions-label": "Actions", - "back-label": "Back", - "navigation-label": "Navigation", - "history": "Commits", - "information": "Information", - "permissions": "Permissions", - "sources": "Sources" + "createButton": "Create Repository" }, "create": { "title": "Create Repository", "subtitle": "Create a new repository" }, - "repository-form": { - "submit": "Save" + "changesets": { + "errorTitle": "Error", + "errorSubtitle": "Could not fetch changesets", + "branchSelectorLabel": "Branches" }, - "edit-nav-link": { - "label": "Edit" - }, - "delete-nav-action": { - "label": "Delete", - "confirm-alert": { - "title": "Delete Repository", - "message": "Do you really want to delete the repository?", - "submit": "Yes", - "cancel": "No" + "changeset": { + "description": "Description", + "summary": "Changeset {{id}} was committed {{time}}", + "shortSummary": "Committed {{id}} {{time}}", + "tags": "Tags", + "diffNotSupported": "Diff of changesets is not supported by the type of repository", + "author": { + "prefix": "Authored by", + "mailto": "Send mail to" + }, + "buttons": { + "details": "Details", + "sources": "Sources" } }, + "repositoryForm": { + "subtitle": "Edit Repository", + "submit": "Save" + }, "sources": { "file-tree": { "name": "Name", @@ -65,36 +82,8 @@ "size": "Size" } }, - "changesets": { - "diff": { - "not-supported": "Diff of changesets is not supported by the type of repository" - }, - "error-title": "Error", - "error-subtitle": "Could not fetch changesets", - "changeset": { - "id": "ID", - "description": "Description", - "contact": "Contact", - "date": "Date", - "summary": "Changeset {{id}} was committed {{time}}", - "short-summary": "Committed {{id}} {{time}}" - }, - "tags": "Tags", - "author": { - "name": "Author", - "mail": "Mail", - "prefix": "Authored by", - "mailto": "Send mail to" - }, - "buttons": { - "details": "Details", - "sources": "Sources" - } - }, - "branch-selector": { - "label": "Branches" - }, "permission": { + "title": "Edit Permissions", "user": "User", "group": "Group", "error-title": "Error", @@ -114,7 +103,7 @@ "delete-permission-button": { "label": "Delete", "confirm-alert": { - "title": "Delete Permission", + "title": "Delete permission", "message": "Do you really want to delete the permission?", "submit": "Yes", "cancel": "No" @@ -146,10 +135,14 @@ } } }, - "help": { - "nameHelpText": "The name of the repository. This name will be part of the repository url.", - "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", - "contactHelpText": "Email address of the person who is responsible for this repository.", - "descriptionHelpText": "A short description of the repository." + "deleteRepo": { + "subtitle": "Delete Repository", + "button": "Delete", + "confirmAlert": { + "title": "Delete repository", + "message": "Do you really want to delete the repository?", + "submit": "Yes", + "cancel": "No" + } } } diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index 4c671617ec..2b72d85cbc 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -10,59 +10,55 @@ "creationDate": "Creation Date", "lastModified": "Last Modified" }, - "users": { - "title": "Users", - "subtitle": "Create, read, update and delete users" - }, - "create-user-button": { - "label": "Create User" - }, - "delete-user-button": { - "label": "Delete", - "confirm-alert": { - "title": "Delete User", - "message": "Do you really want to delete the user?", - "submit": "Yes", - "cancel": "No" - } - }, - "edit-user-button": { - "label": "Edit" - }, - "set-password-button": { - "label": "Set Password" - }, - "set-permissions-button": { - "label": "Set Permissions" - }, - "user-form": { - "submit": "Submit" - }, - "add-user": { - "title": "Create User", - "subtitle": "Create a new user" - }, - "single-user": { - "error-title": "Error", - "error-subtitle": "Unknown user error", - "navigation-label": "Navigation", - "actions-label": "Actions", - "information-label": "Information", - "back-label": "Back" - }, "validation": { "mail-invalid": "This email is invalid", "name-invalid": "This name is invalid", "displayname-invalid": "This displayname is invalid" }, - "password": { - "set-password-successful": "Password successfully set" - }, "help": { "usernameHelpText": "Unique name of the user.", "displayNameHelpText": "Display name of the user.", "mailHelpText": "Email address of the user.", "adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.", "activeHelpText": "Activate or deactivate the user." + }, + "users": { + "title": "Users", + "subtitle": "Create, read, update and delete users", + "createButton": "Create User" + }, + "singleUser": { + "errorTitle": "Error", + "errorSubtitle": "Unknown user error", + "menu": { + "navigationLabel": "User Navigation", + "informationNavLink": "Information", + "settingsNavLink": "Settings", + "generalNavLink": "General", + "setPasswordNavLink": "Password", + "setPermissionsNavLink": "Permissions" + } + }, + "addUser": { + "title": "Create User", + "subtitle": "Create a new user" + }, + "deleteUser": { + "subtitle": "Delete User", + "button": "Delete", + "confirmAlert": { + "title": "Delete user", + "message": "Do you really want to delete the user?", + "submit": "Yes", + "cancel": "No" + } + }, + "singleUserPassword": { + "button": "Set password", + "setPasswordSuccessful": "Password successfully set" + }, + "userForm": { + "subtitle": "Edit User", + "button": "Submit" } } diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 60dc9bb75f..04de525c95 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -1,86 +1,87 @@ -// @flow -import React from "react"; -import { translate } from "react-i18next"; -import { Route } from "react-router"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; - -import type { Links } from "@scm-manager/ui-types"; -import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; -import GlobalConfig from "./GlobalConfig"; -import type { History } from "history"; -import {connect} from "react-redux"; -import {compose} from "redux"; -import { getLinks } from "../../modules/indexResource"; - -type Props = { - links: Links, - - // context objects - t: string => string, - match: any, - history: History -}; - -class Config extends React.Component { - stripEndingSlash = (url: string) => { - if (url.endsWith("/")) { - return url.substring(0, url.length - 2); - } - return url; - }; - - matchedUrl = () => { - return this.stripEndingSlash(this.props.match.url); - }; - - render() { - const { links, t } = this.props; - - const url = this.matchedUrl(); - const extensionProps = { - links, - url - }; - - return ( - -

    -
    - - -
    -
    - -
    - - -
    -
    -
    -
    - - ); - } -} - -const mapStateToProps = (state: any) => { - const links = getLinks(state); - return { - links - }; -}; - -export default compose( - connect(mapStateToProps), - translate("config") -)(Config); - +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Route } from "react-router"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; + +import type { Links } from "@scm-manager/ui-types"; +import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; +import GlobalConfig from "./GlobalConfig"; +import type { History } from "history"; +import { connect } from "react-redux"; +import { compose } from "redux"; +import { getLinks } from "../../modules/indexResource"; + +type Props = { + links: Links, + + // context objects + t: string => string, + match: any, + history: History +}; + +class Config extends React.Component { + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + + render() { + const { links, t } = this.props; + + const url = this.matchedUrl(); + const extensionProps = { + links, + url + }; + + return ( + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    +
    +
    + ); + } +} + +const mapStateToProps = (state: any) => { + const links = getLinks(state); + return { + links + }; +}; + +export default compose( + connect(mapStateToProps), + translate("config") +)(Config); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 71be3fdd7f..eac8e27bee 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -78,8 +78,8 @@ class GlobalConfig extends React.Component { if (error) { return ( @@ -91,7 +91,7 @@ class GlobalConfig extends React.Component { return (
    - + <Title title={t("config.title")} /> {this.renderConfigChangedNotification()} <ConfigForm submitForm={config => this.modifyConfig(config)} diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index e8c5352d58..3ee9b141ef 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -143,7 +143,6 @@ class Login extends React.Component<Props, State> { /> <SubmitButton label={t("login.submit")} - disabled={this.isInValid()} fullWidth={true} loading={loading} /> diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index d2bc50faf2..e963fb00f9 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -10,7 +10,7 @@ import Login from "../containers/Login"; import Logout from "../containers/Logout"; import { ProtectedRoute } from "@scm-manager/ui-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import {binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import AddUser from "../users/containers/AddUser"; import SingleUser from "../users/containers/SingleUser"; @@ -32,10 +32,15 @@ type Props = { class Main extends React.Component<Props> { render() { const { authenticated, links } = this.props; + const redirectUrlFactory = binder.getExtension("main.redirect", this.props); + let url ="/repos"; + if (redirectUrlFactory){ + url = redirectUrlFactory(this.props); + } return ( <div className="main"> <Switch> - <Redirect exact path="/" to="/repos" /> + <Redirect exact path="/" to={url}/> <Route exact path="/login" component={Login} /> <Route path="/logout" component={Logout} /> <ProtectedRoute diff --git a/scm-ui/src/containers/Profile.js b/scm-ui/src/containers/Profile.js index 3464e125dd..e9bdbaa4b9 100644 --- a/scm-ui/src/containers/Profile.js +++ b/scm-ui/src/containers/Profile.js @@ -12,11 +12,13 @@ import { ErrorPage, Page, Navigation, + SubNavigation, Section, NavLink } from "@scm-manager/ui-components"; import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { me: Me, @@ -57,26 +59,43 @@ class Profile extends React.Component<Props, State> { ); } + const extensionProps = { + me, + url + }; + return ( <Page title={me.displayName}> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact render={() => <ProfileInfo me={me} />} /> <Route - path={`${url}/password`} + path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} /> </div> <div className="column"> <Navigation> - <Section label={t("profile.navigation-label")}> - <NavLink to={`${url}`} icon="fas fa-info-circle" label={t("profile.information")} /> - </Section> - <Section label={t("profile.actions-label")}> + <Section label={t("profile.navigationLabel")}> <NavLink - to={`${url}/password`} - label={t("profile.change-password")} + to={`${url}`} + icon="fas fa-info-circle" + label={t("profile.informationNavLink")} /> + <SubNavigation + to={`${url}/settings/password`} + label={t("profile.settingsNavLink")} + > + <NavLink + to={`${url}/settings/password`} + label={t("profile.changePasswordNavLink")} + /> + <ExtensionPoint + name="profile.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> diff --git a/scm-ui/src/containers/ProfileInfo.js b/scm-ui/src/containers/ProfileInfo.js index 4c333174d1..49c79f1fe8 100644 --- a/scm-ui/src/containers/ProfileInfo.js +++ b/scm-ui/src/containers/ProfileInfo.js @@ -28,30 +28,20 @@ class ProfileInfo extends React.Component<Props, State> { <div className="media-content"> <table className="table"> <tbody> - <tr> - <td className="has-text-weight-semibold">{t("profile.username")}</td> - <td>{me.name}</td> - </tr> - <tr> - <td className="has-text-weight-semibold">{t("profile.displayName")}</td> - <td>{me.displayName}</td> - </tr> - <tr> - <td className="has-text-weight-semibold">{t("profile.mail")}</td> - <td> - <MailLink address={me.mail} /> - </td> - </tr> - <tr> - <td className="has-text-weight-semibold">{t("profile.groups")}</td> - <td className="content"> - <ul> - {me.groups.map((group) => { - return <li>{group}</li>; - })} - </ul> - </td> - </tr> + <tr> + <td className="has-text-weight-semibold">{t("profile.username")}</td> + <td>{me.name}</td> + </tr> + <tr> + <td className="has-text-weight-semibold">{t("profile.displayName")}</td> + <td>{me.displayName}</td> + </tr> + <tr> + <td className="has-text-weight-semibold">{t("profile.mail")}</td> + <td> + <MailLink address={me.mail} /> + </td> + </tr> </tbody> </table> </div> diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 7cc2ee5d24..8693cb9b47 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { + Subtitle, AutocompleteAddEntryToTableField, LabelWithHelpIcon, MemberNameTable, @@ -71,59 +72,67 @@ class GroupForm extends React.Component<Props, State> { }; render() { - const { t, loading } = this.props; + const { loading, t } = this.props; const { group } = this.state; let nameField = null; + let subtitle = null; if (!this.props.group) { + // create new group nameField = ( <InputField label={t("group.name")} - errorMessage={t("group-form.name-error")} + errorMessage={t("groupForm.nameError")} onChange={this.handleGroupNameChange} value={group.name} validationError={this.state.nameValidationError} - helpText={t("group-form.help.nameHelpText")} + helpText={t("groupForm.help.nameHelpText")} /> ); + } else { + // edit existing group + subtitle = <Subtitle subtitle={t("groupForm.subtitle")} />; } return ( - <form onSubmit={this.submit}> - {nameField} - <Textarea - label={t("group.description")} - errorMessage={t("group-form.description-error")} - onChange={this.handleDescriptionChange} - value={group.description} - validationError={false} - helpText={t("group-form.help.descriptionHelpText")} - /> - <LabelWithHelpIcon - label={t("group.members")} - helpText={t("group-form.help.memberHelpText")} - /> - <MemberNameTable - members={group.members} - memberListChanged={this.memberListChanged} - /> + <> + {subtitle} + <form onSubmit={this.submit}> + {nameField} + <Textarea + label={t("group.description")} + errorMessage={t("groupForm.descriptionError")} + onChange={this.handleDescriptionChange} + value={group.description} + validationError={false} + helpText={t("groupForm.help.descriptionHelpText")} + /> + <LabelWithHelpIcon + label={t("group.members")} + helpText={t("groupForm.help.memberHelpText")} + /> + <MemberNameTable + members={group.members} + memberListChanged={this.memberListChanged} + /> - <AutocompleteAddEntryToTableField - addEntry={this.addMember} - disabled={false} - buttonLabel={t("add-member-button.label")} - fieldLabel={t("add-member-textfield.label")} - errorMessage={t("add-member-textfield.error")} - loadSuggestions={this.props.loadUserSuggestions} - placeholder={t("add-member-autocomplete.placeholder")} - loadingMessage={t("add-member-autocomplete.loading")} - noOptionsMessage={t("add-member-autocomplete.no-options")} - /> - <SubmitButton - disabled={!this.isValid()} - label={t("group-form.submit")} - loading={loading} - /> - </form> + <AutocompleteAddEntryToTableField + addEntry={this.addMember} + disabled={false} + buttonLabel={t("add-member-button.label")} + fieldLabel={t("add-member-textfield.label")} + errorMessage={t("add-member-textfield.error")} + loadSuggestions={this.props.loadUserSuggestions} + placeholder={t("add-member-autocomplete.placeholder")} + loadingMessage={t("add-member-autocomplete.loading")} + noOptionsMessage={t("add-member-autocomplete.no-options")} + /> + <SubmitButton + disabled={!this.isValid()} + label={t("groupForm.submit")} + loading={loading} + /> + </form> + </> ); } diff --git a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.js b/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.js deleted file mode 100644 index 71119b6e9f..0000000000 --- a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.js +++ /dev/null @@ -1,56 +0,0 @@ -// @flow -import React from "react"; -import { translate } from "react-i18next"; -import type { Group } from "@scm-manager/ui-types"; -import { NavAction, confirmAlert } from "@scm-manager/ui-components"; - -type Props = { - group: Group, - confirmDialog?: boolean, - t: string => string, - deleteGroup: (group: Group) => void -}; - -export class DeleteGroupNavLink extends React.Component<Props> { - static defaultProps = { - confirmDialog: true - }; - - deleteGroup = () => { - this.props.deleteGroup(this.props.group); - }; - - confirmDelete = () => { - const { t } = this.props; - confirmAlert({ - title: t("delete-group-button.confirm-alert.title"), - message: t("delete-group-button.confirm-alert.message"), - buttons: [ - { - label: t("delete-group-button.confirm-alert.submit"), - onClick: () => this.deleteGroup(), - }, - { - label: t("delete-group-button.confirm-alert.cancel"), - onClick: () => null - } - ] - }); - }; - - isDeletable = () => { - return this.props.group._links.delete; - }; - - render() { - const { confirmDialog, t } = this.props; - const action = confirmDialog ? this.confirmDelete : this.deleteGroup; - - if (!this.isDeletable()) { - return null; - } - return <NavAction icon="fas fa-times" label={t("delete-group-button.label")} action={action} />; - } -} - -export default translate("groups")(DeleteGroupNavLink); diff --git a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.test.js b/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.test.js deleted file mode 100644 index 49f8d95c63..0000000000 --- a/scm-ui/src/groups/components/navLinks/DeleteGroupNavLink.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; -import "../../../tests/enzyme"; -import "../../../tests/i18n"; -import DeleteGroupNavLink from "./DeleteGroupNavLink"; - -import { confirmAlert } from "@scm-manager/ui-components"; -jest.mock("@scm-manager/ui-components", () => ({ - confirmAlert: jest.fn(), - NavAction: require.requireActual("@scm-manager/ui-components").NavAction -})); - -describe("DeleteGroupNavLink", () => { - it("should render nothing, if the delete link is missing", () => { - const group = { - _links: {} - }; - - const navLink = shallow( - <DeleteGroupNavLink group={group} deleteGroup={() => {}} /> - ); - expect(navLink.text()).toBe(""); - }); - - it("should render the navLink", () => { - const group = { - _links: { - delete: { - href: "/groups" - } - } - }; - - const navLink = mount( - <DeleteGroupNavLink group={group} deleteGroup={() => {}} /> - ); - expect(navLink.text()).not.toBe(""); - }); - - it("should open the confirm dialog on navLink click", () => { - const group = { - _links: { - delete: { - href: "/groups" - } - } - }; - - const navLink = mount( - <DeleteGroupNavLink group={group} deleteGroup={() => {}} /> - ); - navLink.find("a").simulate("click"); - - expect(confirmAlert.mock.calls.length).toBe(1); - }); - - it("should call the delete group function with delete url", () => { - const group = { - _links: { - delete: { - href: "/groups" - } - } - }; - - let calledUrl = null; - function capture(group) { - calledUrl = group._links.delete.href; - } - - const navLink = mount( - <DeleteGroupNavLink - group={group} - confirmDialog={false} - deleteGroup={capture} - /> - ); - navLink.find("a").simulate("click"); - - expect(calledUrl).toBe("/groups"); - }); -}); diff --git a/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js b/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js index e8bcd26385..9713c5c5c9 100644 --- a/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js +++ b/scm-ui/src/groups/components/navLinks/EditGroupNavLink.js @@ -1,29 +1,28 @@ //@flow import React from "react"; +import type { Group } from "@scm-manager/ui-types"; import { NavLink } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; -import type { Group } from "@scm-manager/ui-types"; type Props = { - t: string => string, + group: Group, editUrl: string, - group: Group + t: string => string }; -type State = {}; - -class EditGroupNavLink extends React.Component<Props, State> { - render() { - const { t, editUrl } = this.props; - if (!this.isEditable()) { - return null; - } - return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-group-button.label")} />; - } - +class EditGroupNavLink extends React.Component<Props> { isEditable = () => { return this.props.group._links.update; }; + + render() { + const { t, editUrl } = this.props; + + if (!this.isEditable()) { + return null; + } + return <NavLink to={editUrl} label={t("singleGroup.menu.generalNavLink")} />; + } } export default translate("groups")(EditGroupNavLink); diff --git a/scm-ui/src/groups/components/navLinks/editGroupNavLink.test.js b/scm-ui/src/groups/components/navLinks/EditGroupNavLink.test.js similarity index 100% rename from scm-ui/src/groups/components/navLinks/editGroupNavLink.test.js rename to scm-ui/src/groups/components/navLinks/EditGroupNavLink.test.js diff --git a/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js index a1314f9987..b2d7062f5e 100644 --- a/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js +++ b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js @@ -17,7 +17,7 @@ class ChangePermissionNavLink extends React.Component<Props> { if (!this.hasPermissionToSetPermission()) { return null; } - return <NavLink to={permissionsUrl} label={t("set-permissions-button.label")} />; + return <NavLink to={permissionsUrl} label={t("singleGroup.menu.setPermissionsNavLink")} />; } hasPermissionToSetPermission = () => { diff --git a/scm-ui/src/groups/components/navLinks/index.js b/scm-ui/src/groups/components/navLinks/index.js index e589e5b6c9..992e9ab9a3 100644 --- a/scm-ui/src/groups/components/navLinks/index.js +++ b/scm-ui/src/groups/components/navLinks/index.js @@ -1,3 +1,2 @@ -export { default as DeleteGroupNavLink } from "./DeleteGroupNavLink"; export { default as EditGroupNavLink } from "./EditGroupNavLink"; export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/groups/containers/DeleteGroup.js b/scm-ui/src/groups/containers/DeleteGroup.js new file mode 100644 index 0000000000..d497e4434d --- /dev/null +++ b/scm-ui/src/groups/containers/DeleteGroup.js @@ -0,0 +1,113 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Group } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { + deleteGroup, + getDeleteGroupFailure, + isDeleteGroupPending +} from "../modules/groups"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + loading: boolean, + error: Error, + group: Group, + confirmDialog?: boolean, + deleteGroup: (group: Group, callback?: () => void) => void, + + // context props + history: History, + t: string => string +}; + +export class DeleteGroup extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + deleteGroup = () => { + this.props.deleteGroup(this.props.group, this.groupDeleted); + }; + + groupDeleted = () => { + this.props.history.push("/groups"); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteGroup.confirmAlert.title"), + message: t("deleteGroup.confirmAlert.message"), + buttons: [ + { + label: t("deleteGroup.confirmAlert.submit"), + onClick: () => this.deleteGroup() + }, + { + label: t("deleteGroup.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.group._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteGroup; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteGroup.subtitle")} /> + <ErrorNotification error={error} /> + <div className="columns"> + <div className="column"> + <DeleteButton + label={t("deleteGroup.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isDeleteGroupPending(state, ownProps.group.name); + const error = getDeleteGroupFailure(state, ownProps.group.name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteGroup: (group: Group, callback?: () => void) => { + dispatch(deleteGroup(group, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("groups")(DeleteGroup))); diff --git a/scm-ui/src/groups/containers/EditGroup.js b/scm-ui/src/groups/containers/EditGroup.js index 223ea1eef6..a1f9f31e46 100644 --- a/scm-ui/src/groups/containers/EditGroup.js +++ b/scm-ui/src/groups/containers/EditGroup.js @@ -3,9 +3,9 @@ import React from "react"; import { connect } from "react-redux"; import GroupForm from "../components/GroupForm"; import { + modifyGroup, getModifyGroupFailure, isModifyGroupPending, - modifyGroup, modifyGroupReset } from "../modules/groups"; import type { History } from "history"; @@ -13,12 +13,13 @@ import { withRouter } from "react-router-dom"; import type { Group } from "@scm-manager/ui-types"; import { ErrorNotification } from "@scm-manager/ui-components"; import { getUserAutoCompleteLink } from "../../modules/indexResource"; +import DeleteGroup from "./DeleteGroup"; type Props = { group: Group, + fetchGroup: (name: string) => void, modifyGroup: (group: Group, callback?: () => void) => void, modifyGroupReset: Group => void, - fetchGroup: (name: string) => void, autocompleteLink: string, history: History, loading?: boolean, @@ -54,7 +55,7 @@ class EditGroup extends React.Component<Props> { }; render() { - const { group, loading, error } = this.props; + const { loading, error, group } = this.props; return ( <div> <ErrorNotification error={error} /> @@ -66,6 +67,8 @@ class EditGroup extends React.Component<Props> { loading={loading} loadUserSuggestions={this.loadUserAutocompletion} /> + <hr /> + <DeleteGroup group={group} /> </div> ); } diff --git a/scm-ui/src/groups/containers/SingleGroup.js b/scm-ui/src/groups/containers/SingleGroup.js index 19c5d53c01..f8d88d7a7d 100644 --- a/scm-ui/src/groups/containers/SingleGroup.js +++ b/scm-ui/src/groups/containers/SingleGroup.js @@ -6,33 +6,30 @@ import { ErrorPage, Loading, Navigation, + SubNavigation, Section, NavLink } from "@scm-manager/ui-components"; import { Route } from "react-router"; import { Details } from "./../components/table"; import { - DeleteGroupNavLink, EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; import { - deleteGroup, fetchGroupByName, getGroupByName, isFetchGroupPending, - getFetchGroupFailure, - getDeleteGroupFailure, - isDeleteGroupPending + getFetchGroupFailure } from "../modules/groups"; import { translate } from "react-i18next"; import EditGroup from "./EditGroup"; import { getGroupsLink } from "../../modules/indexResource"; import SetPermissions from "../../permissions/components/SetPermissions"; -import {ExtensionPoint} from "@scm-manager/ui-extensions"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { name: string, @@ -42,7 +39,6 @@ type Props = { groupLink: string, // dispatcher functions - deleteGroup: (group: Group, callback?: () => void) => void, fetchGroupByName: (string, string) => void, // context objects @@ -63,14 +59,6 @@ class SingleGroup extends React.Component<Props> { return url; }; - deleteGroup = (group: Group) => { - this.props.deleteGroup(group, this.groupDeleted); - }; - - groupDeleted = () => { - this.props.history.push("/groups"); - }; - matchedUrl = () => { return this.stripEndingSlash(this.props.match.url); }; @@ -81,8 +69,8 @@ class SingleGroup extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("single-group.error-title")} - subtitle={t("single-group.error-subtitle")} + title={t("singleGroup.errorTitle")} + subtitle={t("singleGroup.errorSubtitle")} error={error} /> ); @@ -109,15 +97,17 @@ class SingleGroup extends React.Component<Props> { component={() => <Details group={group} />} /> <Route - path={`${url}/edit`} + path={`${url}/settings/general`} exact component={() => <EditGroup group={group} />} /> <Route - path={`${url}/permissions`} + path={`${url}/settings/permissions`} exact component={() => ( - <SetPermissions selectedPermissionsLink={group._links.permissions} /> + <SetPermissions + selectedPermissionsLink={group._links.permissions} + /> )} /> <ExtensionPoint @@ -128,33 +118,35 @@ class SingleGroup extends React.Component<Props> { </div> <div className="column"> <Navigation> - <Section label={t("single-group.navigation-label")}> + <Section label={t("singleGroup.menu.navigationLabel")}> <NavLink to={`${url}`} icon="fas fa-info-circle" - label={t("single-group.information-label")} - /> - <SetPermissionsNavLink - group={group} - permissionsUrl={`${url}/permissions`} + label={t("singleGroup.menu.informationNavLink")} /> <ExtensionPoint name="group.navigation" props={extensionProps} renderAll={true} /> - </Section> - <Section label={t("single-group.actions-label")}> - <DeleteGroupNavLink - group={group} - deleteGroup={this.deleteGroup} - /> - <EditGroupNavLink group={group} editUrl={`${url}/edit`} /> - <NavLink - to="/groups" - icon="fas fa-undo-alt" - label={t("single-group.back-label")} - /> + <SubNavigation + to={`${url}/settings/general`} + label={t("singleGroup.menu.settingsNavLink")} + > + <EditGroupNavLink + group={group} + editUrl={`${url}/settings/general`} + /> + <SetPermissionsNavLink + group={group} + permissionsUrl={`${url}/settings/permissions`} + /> + <ExtensionPoint + name="group.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> @@ -167,10 +159,8 @@ class SingleGroup extends React.Component<Props> { const mapStateToProps = (state, ownProps) => { const name = ownProps.match.params.name; const group = getGroupByName(state, name); - const loading = - isFetchGroupPending(state, name) || isDeleteGroupPending(state, name); - const error = - getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name); + const loading = isFetchGroupPending(state, name); + const error = getFetchGroupFailure(state, name); const groupLink = getGroupsLink(state); return { @@ -186,9 +176,6 @@ const mapDispatchToProps = dispatch => { return { fetchGroupByName: (link: string, name: string) => { dispatch(fetchGroupByName(link, name)); - }, - deleteGroup: (group: Group, callback?: () => void) => { - dispatch(deleteGroup(group, callback)); } }; }; diff --git a/scm-ui/src/permissions/components/SetPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js index e7e561d739..d78177a745 100644 --- a/scm-ui/src/permissions/components/SetPermissions.js +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -113,7 +113,7 @@ class SetPermissions extends React.Component<Props, State> { message = ( <Notification type={"success"} - children={t("form.set-permissions-successful")} + children={t("setPermissions.setPermissionsSuccessful")} onClose={() => this.onClose()} /> ); @@ -128,7 +128,7 @@ class SetPermissions extends React.Component<Props, State> { <SubmitButton disabled={!this.state.permissionsChanged} loading={loading} - label={t("form.submit-button.label")} + label={t("setPermissions.button")} /> </form> ); diff --git a/scm-ui/src/repos/components/DeleteNavAction.js b/scm-ui/src/repos/components/DeleteNavAction.js deleted file mode 100644 index fe35a9caf1..0000000000 --- a/scm-ui/src/repos/components/DeleteNavAction.js +++ /dev/null @@ -1,58 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { NavAction, confirmAlert } from "@scm-manager/ui-components"; -import type { Repository } from "@scm-manager/ui-types"; - -type Props = { - repository: Repository, - confirmDialog?: boolean, - delete: Repository => void, - - // context props - t: string => string -}; - -class DeleteNavAction extends React.Component<Props> { - static defaultProps = { - confirmDialog: true - }; - - delete = () => { - this.props.delete(this.props.repository); - }; - - confirmDelete = () => { - const { t } = this.props; - confirmAlert({ - title: t("delete-nav-action.confirm-alert.title"), - message: t("delete-nav-action.confirm-alert.message"), - buttons: [ - { - label: t("delete-nav-action.confirm-alert.submit"), - onClick: () => this.delete() - }, - { - label: t("delete-nav-action.confirm-alert.cancel"), - onClick: () => null - } - ] - }); - }; - - isDeletable = () => { - return this.props.repository._links.delete; - }; - - render() { - const { confirmDialog, t } = this.props; - const action = confirmDialog ? this.confirmDelete : this.delete(); - - if (!this.isDeletable()) { - return null; - } - return <NavAction action={action} icon="fas fa-times" label={t("delete-nav-action.label")} />; - } -} - -export default translate("repos")(DeleteNavAction); diff --git a/scm-ui/src/repos/components/DeleteNavAction.test.js b/scm-ui/src/repos/components/DeleteNavAction.test.js deleted file mode 100644 index 7c2191864a..0000000000 --- a/scm-ui/src/repos/components/DeleteNavAction.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; -import "../../tests/enzyme"; -import "../../tests/i18n"; -import DeleteNavAction from "./DeleteNavAction"; - -import { confirmAlert } from "@scm-manager/ui-components"; -jest.mock("@scm-manager/ui-components", () => ({ - confirmAlert: jest.fn(), - NavAction: require.requireActual("@scm-manager/ui-components").NavAction -})); - -describe("DeleteNavAction", () => { - it("should render nothing, if the delete link is missing", () => { - const repository = { - _links: {} - }; - - const navLink = shallow( - <DeleteNavAction repository={repository} delete={() => {}} /> - ); - expect(navLink.text()).toBe(""); - }); - - it("should render the navLink", () => { - const repository = { - _links: { - delete: { - href: "/repositories" - } - } - }; - - const navLink = mount( - <DeleteNavAction repository={repository} delete={() => {}} /> - ); - expect(navLink.text()).not.toBe(""); - }); - - it("should open the confirm dialog on navLink click", () => { - const repository = { - _links: { - delete: { - href: "/repositorys" - } - } - }; - - const navLink = mount( - <DeleteNavAction repository={repository} delete={() => {}} /> - ); - navLink.find("a").simulate("click"); - - expect(confirmAlert.mock.calls.length).toBe(1); - }); - - it("should call the delete repository function with delete url", () => { - const repository = { - _links: { - delete: { - href: "/repos" - } - } - }; - - let calledUrl = null; - function capture(repository) { - calledUrl = repository._links.delete.href; - } - - const navLink = mount( - <DeleteNavAction - repository={repository} - confirmDialog={false} - delete={capture} - /> - ); - navLink.find("a").simulate("click"); - - expect(calledUrl).toBe("/repos"); - }); -}); diff --git a/scm-ui/src/repos/components/EditNavLink.js b/scm-ui/src/repos/components/EditRepoNavLink.js similarity index 57% rename from scm-ui/src/repos/components/EditNavLink.js rename to scm-ui/src/repos/components/EditRepoNavLink.js index a42625a154..9a502cdafb 100644 --- a/scm-ui/src/repos/components/EditNavLink.js +++ b/scm-ui/src/repos/components/EditRepoNavLink.js @@ -1,22 +1,28 @@ //@flow import React from "react"; +import type { Repository } from "@scm-manager/ui-types"; import { NavLink } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; -import type { Repository } from "@scm-manager/ui-types"; -type Props = { editUrl: string, t: string => string, repository: Repository }; +type Props = { + repository: Repository, + editUrl: string, + t: string => string +}; -class EditNavLink extends React.Component<Props> { +class EditRepoNavLink extends React.Component<Props> { isEditable = () => { return this.props.repository._links.update; }; + render() { + const { editUrl, t } = this.props; + if (!this.isEditable()) { return null; } - const { editUrl, t } = this.props; - return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-nav-link.label")} />; + return <NavLink to={editUrl} label={t("repositoryRoot.menu.generalNavLink")} />; } } -export default translate("repos")(EditNavLink); +export default translate("repos")(EditRepoNavLink); diff --git a/scm-ui/src/repos/components/EditNavLink.test.js b/scm-ui/src/repos/components/EditRepoNavLink.test.js similarity index 71% rename from scm-ui/src/repos/components/EditNavLink.test.js rename to scm-ui/src/repos/components/EditRepoNavLink.test.js index fdb13ade8d..22bb06fae0 100644 --- a/scm-ui/src/repos/components/EditNavLink.test.js +++ b/scm-ui/src/repos/components/EditRepoNavLink.test.js @@ -3,9 +3,9 @@ import { shallow, mount } from "enzyme"; import "../../tests/enzyme"; import "../../tests/i18n"; import ReactRouterEnzymeContext from "react-router-enzyme-context"; -import EditNavLink from "./EditNavLink"; +import EditRepoNavLink from "./EditRepoNavLink"; -describe("EditNavLink", () => { +describe("GeneralNavLink", () => { const options = new ReactRouterEnzymeContext(); it("should render nothing, if the modify link is missing", () => { @@ -14,7 +14,7 @@ describe("EditNavLink", () => { }; const navLink = shallow( - <EditNavLink repository={repository} editUrl="" />, + <EditRepoNavLink repository={repository} editUrl="" />, options.get() ); expect(navLink.text()).toBe(""); @@ -30,9 +30,9 @@ describe("EditNavLink", () => { }; const navLink = mount( - <EditNavLink repository={repository} editUrl="" />, + <EditRepoNavLink repository={repository} editUrl="" />, options.get() ); - expect(navLink.text()).toBe(" edit-nav-link.label"); + expect(navLink.text()).toBe("repositoryRoot.menu.generalNavLink"); }); }); diff --git a/scm-ui/src/repos/components/PermissionsNavLink.js b/scm-ui/src/repos/components/PermissionsNavLink.js index 3a6f97588b..773ad94246 100644 --- a/scm-ui/src/repos/components/PermissionsNavLink.js +++ b/scm-ui/src/repos/components/PermissionsNavLink.js @@ -20,7 +20,7 @@ class PermissionsNavLink extends React.Component<Props> { } const { permissionUrl, t } = this.props; return ( - <NavLink to={permissionUrl} icon="fas fa-lock" label={t("repository-root.permissions")} /> + <NavLink to={permissionUrl} label={t("repositoryRoot.menu.permissionsNavLink")} /> ); } } diff --git a/scm-ui/src/repos/components/PermissionsNavLink.test.js b/scm-ui/src/repos/components/PermissionsNavLink.test.js index 901175caa0..3f6a95fe7d 100644 --- a/scm-ui/src/repos/components/PermissionsNavLink.test.js +++ b/scm-ui/src/repos/components/PermissionsNavLink.test.js @@ -33,6 +33,6 @@ describe("PermissionsNavLink", () => { <PermissionsNavLink repository={repository} permissionUrl="" />, options.get() ); - expect(navLink.text()).toBe(" repository-root.permissions"); + expect(navLink.text()).toBe("repositoryRoot.menu.permissionsNavLink"); }); }); diff --git a/scm-ui/src/repos/components/changesets/ChangesetDetails.js b/scm-ui/src/repos/components/changesets/ChangesetDetails.js index fab976b797..33d63a30d4 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetDetails.js +++ b/scm-ui/src/repos/components/changesets/ChangesetDetails.js @@ -53,7 +53,7 @@ class ChangesetDetails extends React.Component<Props> { <div className="content"> <h4> <ExtensionPoint - name="changesets.changeset.description" + name="changeset.description" props={{ changeset, value: description.title }} renderAll={false} > @@ -72,7 +72,7 @@ class ChangesetDetails extends React.Component<Props> { </p> <p> <Interpolate - i18nKey="changesets.changeset.summary" + i18nKey="changeset.summary" id={id} time={date} /> @@ -86,7 +86,7 @@ class ChangesetDetails extends React.Component<Props> { return ( <span key={key}> <ExtensionPoint - name="changesets.changeset.description" + name="changeset.description" props={{ changeset, value: item }} renderAll={false} > diff --git a/scm-ui/src/repos/components/form/RepositoryForm.js b/scm-ui/src/repos/components/form/RepositoryForm.js index 8f5d932778..fcd88f0417 100644 --- a/scm-ui/src/repos/components/form/RepositoryForm.js +++ b/scm-ui/src/repos/components/form/RepositoryForm.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { + Subtitle, InputField, Select, SubmitButton, @@ -81,30 +82,39 @@ class RepositoryForm extends React.Component<Props, State> { const { loading, t } = this.props; const repository = this.state.repository; - return ( - <form onSubmit={this.submit}> - {this.renderCreateOnlyFields()} - <InputField - label={t("repository.contact")} - onChange={this.handleContactChange} - value={repository ? repository.contact : ""} - validationError={this.state.contactValidationError} - errorMessage={t("validation.contact-invalid")} - helpText={t("help.contactHelpText")} - /> + let subtitle = null; + if (this.props.repository) { + // edit existing repo + subtitle = <Subtitle subtitle={t("repositoryForm.subtitle")} />; + } - <Textarea - label={t("repository.description")} - onChange={this.handleDescriptionChange} - value={repository ? repository.description : ""} - helpText={t("help.descriptionHelpText")} - /> - <SubmitButton - disabled={!this.isValid()} - loading={loading} - label={t("repository-form.submit")} - /> - </form> + return ( + <> + {subtitle} + <form onSubmit={this.submit}> + {this.renderCreateOnlyFields()} + <InputField + label={t("repository.contact")} + onChange={this.handleContactChange} + value={repository ? repository.contact : ""} + validationError={this.state.contactValidationError} + errorMessage={t("validation.contact-invalid")} + helpText={t("help.contactHelpText")} + /> + + <Textarea + label={t("repository.description")} + onChange={this.handleDescriptionChange} + value={repository ? repository.description : ""} + helpText={t("help.descriptionHelpText")} + /> + <SubmitButton + disabled={!this.isValid()} + loading={loading} + label={t("repositoryForm.submit")} + /> + </form> + </> ); } diff --git a/scm-ui/src/repos/components/list/RepositoryEntry.js b/scm-ui/src/repos/components/list/RepositoryEntry.js index 28a6fe1ad3..b8b03a3523 100644 --- a/scm-ui/src/repos/components/list/RepositoryEntry.js +++ b/scm-ui/src/repos/components/list/RepositoryEntry.js @@ -64,7 +64,7 @@ class RepositoryEntry extends React.Component<Props> { return ( <RepositoryEntryLink iconClass="fa-cog fa-lg" - to={repositoryLink + "/edit"} + to={repositoryLink + "/settings/general"} /> ); } diff --git a/scm-ui/src/repos/containers/ChangesetView.js b/scm-ui/src/repos/containers/ChangesetView.js index 80ab0b71d6..dc53b5d798 100644 --- a/scm-ui/src/repos/containers/ChangesetView.js +++ b/scm-ui/src/repos/containers/ChangesetView.js @@ -37,8 +37,8 @@ class ChangesetView extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("changeset-error.title")} - subtitle={t("changeset-error.subtitle")} + title={t("changesets.errorTitle")} + subtitle={t("changesets.errorSubtitle")} error={error} /> ); diff --git a/scm-ui/src/repos/containers/Changesets.js b/scm-ui/src/repos/containers/Changesets.js index 95bf0459a6..5edc677e60 100644 --- a/scm-ui/src/repos/containers/Changesets.js +++ b/scm-ui/src/repos/containers/Changesets.js @@ -1,8 +1,13 @@ // @flow import React from "react"; -import {withRouter} from "react-router-dom"; -import type {Branch, Changeset, PagedCollection, Repository} from "@scm-manager/ui-types"; +import { withRouter } from "react-router-dom"; +import type { + Branch, + Changeset, + PagedCollection, + Repository +} from "@scm-manager/ui-types"; import { fetchChangesets, getChangesets, @@ -11,9 +16,15 @@ import { selectListAsCollection } from "../modules/changesets"; -import {connect} from "react-redux"; -import {ErrorNotification, getPageFromMatch, LinkPaginator, ChangesetList, Loading} from "@scm-manager/ui-components"; -import {compose} from "redux"; +import { connect } from "react-redux"; +import { + ErrorNotification, + getPageFromMatch, + LinkPaginator, + ChangesetList, + Loading +} from "@scm-manager/ui-components"; +import { compose } from "redux"; type Props = { repository: Repository, @@ -64,13 +75,21 @@ class Changesets extends React.Component<Props> { renderList = () => { const { repository, changesets } = this.props; - return <ChangesetList repository={repository} changesets={changesets} />; + return ( + <div className="panel-block"> + <ChangesetList repository={repository} changesets={changesets} /> + </div> + ); }; renderPaginator = () => { const { page, list } = this.props; if (list) { - return <LinkPaginator page={page} collection={list} />; + return ( + <div className="panel-footer"> + <LinkPaginator page={page} collection={list} /> + </div> + ); } return null; }; diff --git a/scm-ui/src/repos/containers/ChangesetsRoot.js b/scm-ui/src/repos/containers/ChangesetsRoot.js index 2eea64b8e1..f6e0c3d359 100644 --- a/scm-ui/src/repos/containers/ChangesetsRoot.js +++ b/scm-ui/src/repos/containers/ChangesetsRoot.js @@ -89,10 +89,12 @@ class BranchRoot extends React.Component<Props> { const changesets = <Changesets repository={repository} branch={branch} />; return ( - <> + <div className="panel"> + <div className="panel-heading"> {this.renderBranchSelector()} + </div> <Route path={`${url}/:page?`} component={() => changesets} /> - </> + </div> ); } @@ -101,7 +103,7 @@ class BranchRoot extends React.Component<Props> { if (repository._links.branches) { return ( <BranchSelector - label={t("branch-selector.label")} + label={t("changesets.branchSelectorLabel")} branches={branches} selectedBranch={selected} selected={(b: Branch) => { diff --git a/scm-ui/src/repos/containers/DeleteRepo.js b/scm-ui/src/repos/containers/DeleteRepo.js new file mode 100644 index 0000000000..b621a1998b --- /dev/null +++ b/scm-ui/src/repos/containers/DeleteRepo.js @@ -0,0 +1,114 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Repository } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { + deleteRepo, + getDeleteRepoFailure, + isDeleteRepoPending +} from "../modules/repos"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + loading: boolean, + error: Error, + repository: Repository, + confirmDialog?: boolean, + deleteRepo: (Repository, () => void) => void, + + // context props + history: History, + t: string => string +}; + +class DeleteRepo extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + deleted = () => { + this.props.history.push("/repos"); + }; + + deleteRepo = () => { + this.props.deleteRepo(this.props.repository, this.deleted); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteRepo.confirmAlert.title"), + message: t("deleteRepo.confirmAlert.message"), + buttons: [ + { + label: t("deleteRepo.confirmAlert.submit"), + onClick: () => this.deleteRepo() + }, + { + label: t("deleteRepo.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.repository._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteRepo; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteRepo.subtitle")} /> + <ErrorNotification error={error} /> + <div className="columns"> + <div className="column"> + <DeleteButton + label={t("deleteRepo.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const { namespace, name } = ownProps.repository; + const loading = isDeleteRepoPending(state, namespace, name); + const error = getDeleteRepoFailure(state, namespace, name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteRepo: (repo: Repository, callback: () => void) => { + dispatch(deleteRepo(repo, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("repos")(DeleteRepo))); diff --git a/scm-ui/src/repos/containers/Edit.js b/scm-ui/src/repos/containers/EditRepo.js similarity index 69% rename from scm-ui/src/repos/containers/Edit.js rename to scm-ui/src/repos/containers/EditRepo.js index 816dae8de9..2e5bb69172 100644 --- a/scm-ui/src/repos/containers/Edit.js +++ b/scm-ui/src/repos/containers/EditRepo.js @@ -1,8 +1,9 @@ // @flow import React from "react"; import { connect } from "react-redux"; -import { translate } from "react-i18next"; +import { withRouter } from "react-router-dom"; import RepositoryForm from "../components/form"; +import DeleteRepo from "./DeleteRepo"; import type { Repository } from "@scm-manager/ui-types"; import { modifyRepo, @@ -10,34 +11,55 @@ import { getModifyRepoFailure, modifyRepoReset } from "../modules/repos"; -import { withRouter } from "react-router-dom"; import type { History } from "history"; import { ErrorNotification } from "@scm-manager/ui-components"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { - repository: Repository, - modifyRepo: (Repository, () => void) => void, - modifyRepoReset: Repository => void, loading: boolean, error: Error, + modifyRepo: (Repository, () => void) => void, + modifyRepoReset: Repository => void, + // context props - t: string => string, - history: History + repository: Repository, + history: History, + match: any }; -class Edit extends React.Component<Props> { +class EditRepo extends React.Component<Props> { componentDidMount() { const { modifyRepoReset, repository } = this.props; modifyRepoReset(repository); } + repoModified = () => { const { history, repository } = this.props; history.push(`/repo/${repository.namespace}/${repository.name}`); }; + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + render() { - const { loading, error } = this.props; + const { loading, error, repository } = this.props; + + const url = this.matchedUrl(); + + const extensionProps = { + repository, + url + }; + return ( <div> <ErrorNotification error={error} /> @@ -48,6 +70,13 @@ class Edit extends React.Component<Props> { this.props.modifyRepo(repo, this.repoModified); }} /> + <hr /> + <ExtensionPoint + name="repo-config.route" + props={extensionProps} + renderAll={true} + /> + <DeleteRepo repository={repository} /> </div> ); } @@ -77,4 +106,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("repos")(withRouter(Edit))); +)(withRouter(EditRepo)); diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index bbafe14539..598b6c94f2 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -90,7 +90,7 @@ class Overview extends React.Component<Props> { if (showCreateButton) { return ( <CreateButton - label={t("overview.create-button")} + label={t("overview.createButton")} link="/repos/create" /> ); diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 4ab9382cc1..d0d722e1fd 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -1,20 +1,32 @@ //@flow import React from "react"; -import {deleteRepo, fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos"; +import { + fetchRepoByName, + getFetchRepoFailure, + getRepository, + isFetchRepoPending +} from "../modules/repos"; -import {connect} from "react-redux"; -import {Route, Switch} from "react-router-dom"; -import type {Repository} from "@scm-manager/ui-types"; +import { connect } from "react-redux"; +import { Route, Switch } from "react-router-dom"; +import type { Repository } from "@scm-manager/ui-types"; -import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components"; -import {translate} from "react-i18next"; +import { + ErrorPage, + Loading, + Navigation, + SubNavigation, + NavLink, + Page, + Section +} from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; import RepositoryDetails from "../components/RepositoryDetails"; -import DeleteNavAction from "../components/DeleteNavAction"; -import Edit from "../containers/Edit"; +import EditRepo from "./EditRepo"; import Permissions from "../permissions/containers/Permissions"; -import type {History} from "history"; -import EditNavLink from "../components/EditNavLink"; +import type { History } from "history"; +import EditRepoNavLink from "../components/EditRepoNavLink"; import BranchRoot from "./ChangesetsRoot"; import ChangesetView from "./ChangesetView"; @@ -35,7 +47,6 @@ type Props = { // dispatch functions fetchRepoByName: (link: string, namespace: string, name: string) => void, - deleteRepo: (repository: Repository, () => void) => void, // context props t: string => string, @@ -61,14 +72,6 @@ class RepositoryRoot extends React.Component<Props> { return this.stripEndingSlash(this.props.match.url); }; - deleted = () => { - this.props.history.push("/repos"); - }; - - delete = (repository: Repository) => { - this.props.deleteRepo(repository, this.deleted); - }; - matches = (route: any) => { const url = this.matchedUrl(); const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`); @@ -81,8 +84,8 @@ class RepositoryRoot extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("repository-root.error-title")} - subtitle={t("repository-root.error-subtitle")} + title={t("repositoryRoot.errorTitle")} + subtitle={t("repositoryRoot.errorSubtitle")} error={error} /> ); @@ -111,11 +114,11 @@ class RepositoryRoot extends React.Component<Props> { component={() => <RepositoryDetails repository={repository} />} /> <Route - path={`${url}/edit`} - component={() => <Edit repository={repository} />} + path={`${url}/settings/general`} + component={() => <EditRepo repository={repository} />} /> <Route - path={`${url}/permissions`} + path={`${url}/settings/permissions`} render={() => ( <Permissions namespace={this.props.repository.namespace} @@ -170,14 +173,18 @@ class RepositoryRoot extends React.Component<Props> { </div> <div className="column"> <Navigation> - <Section label={t("repository-root.navigation-label")}> - <NavLink to={url} icon="fas fa-info-circle" label={t("repository-root.information")} /> + <Section label={t("repositoryRoot.menu.navigationLabel")}> + <NavLink + to={url} + icon="fas fa-info-circle" + label={t("repositoryRoot.menu.informationNavLink")} + /> <RepositoryNavLink repository={repository} linkName="changesets" to={`${url}/changesets/`} icon="fas fa-code-branch" - label={t("repository-root.history")} + label={t("repositoryRoot.menu.historyNavLink")} activeWhenMatch={this.matches} activeOnlyWhenExact={false} /> @@ -186,23 +193,32 @@ class RepositoryRoot extends React.Component<Props> { linkName="sources" to={`${url}/sources`} icon="fas fa-code" - label={t("repository-root.sources")} + label={t("repositoryRoot.menu.sourcesNavLink")} activeOnlyWhenExact={false} /> - <PermissionsNavLink - permissionUrl={`${url}/permissions`} - repository={repository} - /> <ExtensionPoint name="repository.navigation" props={extensionProps} renderAll={true} /> - </Section> - <Section label={t("repository-root.actions-label")}> - <DeleteNavAction repository={repository} delete={this.delete} /> - <EditNavLink repository={repository} editUrl={`${url}/edit`} /> - <NavLink to="/repos" icon="fas fa-undo" label={t("repository-root.back-label")} /> + <SubNavigation + to={`${url}/settings/general`} + label={t("repositoryRoot.menu.settingsNavLink")} + > + <EditRepoNavLink + repository={repository} + editUrl={`${url}/settings/general`} + /> + <PermissionsNavLink + permissionUrl={`${url}/settings/permissions`} + repository={repository} + /> + <ExtensionPoint + name="repository.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> @@ -234,9 +250,6 @@ const mapDispatchToProps = dispatch => { return { fetchRepoByName: (link: string, namespace: string, name: string) => { dispatch(fetchRepoByName(link, namespace, name)); - }, - deleteRepo: (repository: Repository, callback: () => void) => { - dispatch(deleteRepo(repository, callback)); } }; }; diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 7c20f503c5..38afea441b 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -24,6 +24,7 @@ import { import { Loading, ErrorPage, + Subtitle, LabelWithHelpIcon } from "@scm-manager/ui-components"; import type { @@ -143,6 +144,7 @@ class Permissions extends React.Component<Props> { return ( <div> + <Subtitle subtitle={t("permission.title")} /> <table className="has-background-light table is-hoverable is-fullwidth"> <thead> <tr> diff --git a/scm-ui/src/repos/sources/components/FileTree.js b/scm-ui/src/repos/sources/components/FileTree.js index cbe03df62f..18ef1b01c5 100644 --- a/scm-ui/src/repos/sources/components/FileTree.js +++ b/scm-ui/src/repos/sources/components/FileTree.js @@ -108,32 +108,34 @@ class FileTree extends React.Component<Props> { } return ( - <table className="table table-hover table-sm is-fullwidth"> - <thead> - <tr> - <th className={classes.iconColumn} /> - <th>{t("sources.file-tree.name")}</th> - <th className="is-hidden-mobile"> - {t("sources.file-tree.length")} - </th> - <th className="is-hidden-mobile"> - {t("sources.file-tree.lastModified")} - </th> - <th className="is-hidden-mobile"> - {t("sources.file-tree.description")} - </th> - </tr> - </thead> - <tbody> - {files.map(file => ( - <FileTreeLeaf - key={file.name} - file={file} - baseUrl={baseUrlWithRevision} - /> - ))} - </tbody> - </table> + <div className="panel-block"> + <table className="table table-hover table-sm is-fullwidth"> + <thead> + <tr> + <th className={classes.iconColumn} /> + <th>{t("sources.file-tree.name")}</th> + <th className="is-hidden-mobile"> + {t("sources.file-tree.length")} + </th> + <th className="is-hidden-mobile"> + {t("sources.file-tree.lastModified")} + </th> + <th className="is-hidden-mobile"> + {t("sources.file-tree.description")} + </th> + </tr> + </thead> + <tbody> + {files.map(file => ( + <FileTreeLeaf + key={file.name} + file={file} + baseUrl={baseUrlWithRevision} + /> + ))} + </tbody> + </table> + </div> ); } } diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index a7e9874058..2f3f5ba853 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -29,9 +29,6 @@ type State = { }; const styles = { - toCenterContent: { - display: "block" - }, pointer: { cursor: "pointer" }, @@ -126,7 +123,6 @@ class Content extends React.Component<Props, State> { <div className={classNames( "panel-block", - classes.toCenterContent, classes.hasBackground )} > @@ -161,7 +157,7 @@ class Content extends React.Component<Props, State> { } render() { - const { file, revision, repository, path, classes } = this.props; + const { file, revision, repository, path } = this.props; const { showHistory } = this.state; const header = this.showHeader(); @@ -180,13 +176,11 @@ class Content extends React.Component<Props, State> { return ( <div> - <nav className="panel"> - <article className="panel-heading">{header}</article> + <div className="panel"> + <div className="panel-heading">{header}</div> {moreInformation} - <div className={classNames("panel-block", classes.toCenterContent)}> - {content} - </div> - </nav> + {content} + </div> </div> ); } diff --git a/scm-ui/src/repos/sources/containers/HistoryView.js b/scm-ui/src/repos/sources/containers/HistoryView.js index 98400248d9..d13b5904d2 100644 --- a/scm-ui/src/repos/sources/containers/HistoryView.js +++ b/scm-ui/src/repos/sources/containers/HistoryView.js @@ -79,12 +79,16 @@ class HistoryView extends React.Component<Props, State> { const currentPage = page + 1; return ( <> - <ChangesetList repository={repository} changesets={changesets} /> - <StatePaginator - page={currentPage} - collection={pageCollection} - updatePage={(newPage: number) => this.updatePage(newPage)} - /> + <div className="panel-block"> + <ChangesetList repository={repository} changesets={changesets} /> + </div> + <div className="panel-footer"> + <StatePaginator + page={currentPage} + collection={pageCollection} + updatePage={(newPage: number) => this.updatePage(newPage)} + /> + </div> </> ); } diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index 810e2309ee..05705a61d1 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -93,8 +93,10 @@ class Sources extends React.Component<Props> { if (currentFileIsDirectory) { return ( - <div className={"has-border-around"}> - {this.renderBranchSelector()} + <div className="panel"> + <div className="panel-heading"> + {this.renderBranchSelector()} + </div> <FileTree repository={repository} revision={revision} @@ -118,7 +120,7 @@ class Sources extends React.Component<Props> { <BranchSelector branches={branches} selectedBranch={revision} - label={t("branch-selector.label")} + label={t("changesets.branchSelectorLabel")} selected={(b: Branch) => { this.branchSelected(b); }} diff --git a/scm-ui/src/repos/sources/containers/SourcesView.js b/scm-ui/src/repos/sources/containers/SourcesView.js index 1e76c6d6bf..0f729beb2e 100644 --- a/scm-ui/src/repos/sources/containers/SourcesView.js +++ b/scm-ui/src/repos/sources/containers/SourcesView.js @@ -90,7 +90,7 @@ class SourcesView extends React.Component<Props, State> { const sources = this.showSources(); - return <>{sources}</>; + return <div className="panel-block">{sources}</div>; } } diff --git a/scm-ui/src/users/components/SetUserPassword.js b/scm-ui/src/users/components/SetUserPassword.js index d318025f21..a0fe844b0c 100644 --- a/scm-ui/src/users/components/SetUserPassword.js +++ b/scm-ui/src/users/components/SetUserPassword.js @@ -90,7 +90,7 @@ class SetUserPassword extends React.Component<Props, State> { message = ( <Notification type={"success"} - children={t("password.set-password-successful")} + children={t("singleUserPassword.setPasswordSuccessful")} onClose={() => this.onClose()} /> ); @@ -108,7 +108,7 @@ class SetUserPassword extends React.Component<Props, State> { <SubmitButton disabled={!this.state.passwordValid} loading={loading} - label={t("user-form.submit")} + label={t("singleUserPassword.button")} /> </form> ); diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 9bf36d19d0..c8fae1ff0a 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -3,6 +3,7 @@ import React from "react"; import { translate } from "react-i18next"; import type { User } from "@scm-manager/ui-types"; import { + Subtitle, Checkbox, InputField, PasswordConfirmation, @@ -113,7 +114,9 @@ class UserForm extends React.Component<Props, State> { let nameField = null; let passwordChangeField = null; + let subtitle = null; if (!this.props.user) { + // create new user nameField = ( <div className="column is-half"> <InputField @@ -130,59 +133,65 @@ class UserForm extends React.Component<Props, State> { passwordChangeField = ( <PasswordConfirmation passwordChanged={this.handlePasswordChange} /> ); + } else { + // edit existing user + subtitle = <Subtitle subtitle={t("userForm.subtitle")} />; } return ( - <form onSubmit={this.submit}> - <div className="columns is-multiline"> - {nameField} - <div className="column is-half"> - <InputField - label={t("user.displayName")} - onChange={this.handleDisplayNameChange} - value={user ? user.displayName : ""} - validationError={this.state.displayNameValidationError} - errorMessage={t("validation.displayname-invalid")} - helpText={t("help.displayNameHelpText")} - /> + <> + {subtitle} + <form onSubmit={this.submit}> + <div className="columns is-multiline"> + {nameField} + <div className="column is-half"> + <InputField + label={t("user.displayName")} + onChange={this.handleDisplayNameChange} + value={user ? user.displayName : ""} + validationError={this.state.displayNameValidationError} + errorMessage={t("validation.displayname-invalid")} + helpText={t("help.displayNameHelpText")} + /> + </div> + <div className="column is-half"> + <InputField + label={t("user.mail")} + onChange={this.handleEmailChange} + value={user ? user.mail : ""} + validationError={this.state.mailValidationError} + errorMessage={t("validation.mail-invalid")} + helpText={t("help.mailHelpText")} + /> + </div> </div> - <div className="column is-half"> - <InputField - label={t("user.mail")} - onChange={this.handleEmailChange} - value={user ? user.mail : ""} - validationError={this.state.mailValidationError} - errorMessage={t("validation.mail-invalid")} - helpText={t("help.mailHelpText")} - /> + <div className="columns"> + <div className="column"> + {passwordChangeField} + <Checkbox + label={t("user.admin")} + onChange={this.handleAdminChange} + checked={user ? user.admin : false} + helpText={t("help.adminHelpText")} + /> + <Checkbox + label={t("user.active")} + onChange={this.handleActiveChange} + checked={user ? user.active : false} + helpText={t("help.activeHelpText")} + /> + </div> </div> - </div> - <div className="columns"> - <div className="column"> - {passwordChangeField} - <Checkbox - label={t("user.admin")} - onChange={this.handleAdminChange} - checked={user ? user.admin : false} - helpText={t("help.adminHelpText")} - /> - <Checkbox - label={t("user.active")} - onChange={this.handleActiveChange} - checked={user ? user.active : false} - helpText={t("help.activeHelpText")} - /> + <div className="columns"> + <div className="column"> + <SubmitButton + disabled={!this.isValid()} + loading={loading} + label={t("userForm.button")} + /> + </div> </div> - </div> - <div className="columns"> - <div className="column"> - <SubmitButton - disabled={!this.isValid()} - loading={loading} - label={t("user-form.submit")} - /> - </div> - </div> - </form> + </form> + </> ); } diff --git a/scm-ui/src/users/components/buttons/CreateUserButton.js b/scm-ui/src/users/components/buttons/CreateUserButton.js deleted file mode 100644 index f34820cd0d..0000000000 --- a/scm-ui/src/users/components/buttons/CreateUserButton.js +++ /dev/null @@ -1,20 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { CreateButton } from "@scm-manager/ui-components"; - -// TODO remove -type Props = { - t: string => string -}; - -class CreateUserButton extends React.Component<Props> { - render() { - const { t } = this.props; - return ( - <CreateButton label={t("create-user-button.label")} link="/users/add" /> - ); - } -} - -export default translate("users")(CreateUserButton); diff --git a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.js b/scm-ui/src/users/components/navLinks/DeleteUserNavLink.js deleted file mode 100644 index 80c355e999..0000000000 --- a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.js +++ /dev/null @@ -1,56 +0,0 @@ -// @flow -import React from "react"; -import { translate } from "react-i18next"; -import type { User } from "@scm-manager/ui-types"; -import { NavAction, confirmAlert } from "@scm-manager/ui-components"; - -type Props = { - user: User, - confirmDialog?: boolean, - t: string => string, - deleteUser: (user: User) => void -}; - -class DeleteUserNavLink extends React.Component<Props> { - static defaultProps = { - confirmDialog: true - }; - - deleteUser = () => { - this.props.deleteUser(this.props.user); - }; - - confirmDelete = () => { - const { t } = this.props; - confirmAlert({ - title: t("delete-user-button.confirm-alert.title"), - message: t("delete-user-button.confirm-alert.message"), - buttons: [ - { - label: t("delete-user-button.confirm-alert.submit"), - onClick: () => this.deleteUser() - }, - { - label: t("delete-user-button.confirm-alert.cancel"), - onClick: () => null - } - ] - }); - }; - - isDeletable = () => { - return this.props.user._links.delete; - }; - - render() { - const { confirmDialog, t } = this.props; - const action = confirmDialog ? this.confirmDelete : this.deleteUser; - - if (!this.isDeletable()) { - return null; - } - return <NavAction icon="fas fa-times" label={t("delete-user-button.label")} action={action} />; - } -} - -export default translate("users")(DeleteUserNavLink); diff --git a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.test.js b/scm-ui/src/users/components/navLinks/DeleteUserNavLink.test.js deleted file mode 100644 index 500235ab94..0000000000 --- a/scm-ui/src/users/components/navLinks/DeleteUserNavLink.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; -import "../../../tests/enzyme"; -import "../../../tests/i18n"; -import DeleteUserNavLink from "./DeleteUserNavLink"; - -import { confirmAlert } from "@scm-manager/ui-components"; -jest.mock("@scm-manager/ui-components", () => ({ - confirmAlert: jest.fn(), - NavAction: require.requireActual("@scm-manager/ui-components").NavAction -})); - -describe("DeleteUserNavLink", () => { - it("should render nothing, if the delete link is missing", () => { - const user = { - _links: {} - }; - - const navLink = shallow( - <DeleteUserNavLink user={user} deleteUser={() => {}} /> - ); - expect(navLink.text()).toBe(""); - }); - - it("should render the navLink", () => { - const user = { - _links: { - delete: { - href: "/users" - } - } - }; - - const navLink = mount( - <DeleteUserNavLink user={user} deleteUser={() => {}} /> - ); - expect(navLink.text()).not.toBe(""); - }); - - it("should open the confirm dialog on navLink click", () => { - const user = { - _links: { - delete: { - href: "/users" - } - } - }; - - const navLink = mount( - <DeleteUserNavLink user={user} deleteUser={() => {}} /> - ); - navLink.find("a").simulate("click"); - - expect(confirmAlert.mock.calls.length).toBe(1); - }); - - it("should call the delete user function with delete url", () => { - const user = { - _links: { - delete: { - href: "/users" - } - } - }; - - let calledUrl = null; - function capture(user) { - calledUrl = user._links.delete.href; - } - - const navLink = mount( - <DeleteUserNavLink - user={user} - confirmDialog={false} - deleteUser={capture} - /> - ); - navLink.find("a").simulate("click"); - - expect(calledUrl).toBe("/users"); - }); -}); diff --git a/scm-ui/src/users/components/navLinks/EditUserNavLink.js b/scm-ui/src/users/components/navLinks/EditUserNavLink.js index 8be8dbc621..051bf9a4bd 100644 --- a/scm-ui/src/users/components/navLinks/EditUserNavLink.js +++ b/scm-ui/src/users/components/navLinks/EditUserNavLink.js @@ -1,28 +1,28 @@ //@flow import React from "react"; -import { translate } from "react-i18next"; import type { User } from "@scm-manager/ui-types"; import { NavLink } from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; type Props = { - t: string => string, user: User, - editUrl: String + editUrl: String, + t: string => string }; class EditUserNavLink extends React.Component<Props> { + isEditable = () => { + return this.props.user._links.update; + }; + render() { const { t, editUrl } = this.props; if (!this.isEditable()) { return null; } - return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-user-button.label")} />; + return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />; } - - isEditable = () => { - return this.props.user._links.update; - }; } export default translate("users")(EditUserNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js index 46e931e788..79234308aa 100644 --- a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js +++ b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js @@ -17,7 +17,7 @@ class ChangePasswordNavLink extends React.Component<Props> { if (!this.hasPermissionToSetPassword()) { return null; } - return <NavLink to={passwordUrl} label={t("set-password-button.label")} />; + return <NavLink to={passwordUrl} label={t("singleUser.menu.setPasswordNavLink")} />; } hasPermissionToSetPassword = () => { diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js index 3c593d9427..84b0f9da76 100644 --- a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js @@ -17,7 +17,7 @@ class ChangePermissionNavLink extends React.Component<Props> { if (!this.hasPermissionToSetPermission()) { return null; } - return <NavLink to={permissionsUrl} label={t("set-permissions-button.label")} />; + return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />; } hasPermissionToSetPermission = () => { diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index eb39bb6726..cb97c57e3f 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,4 +1,3 @@ -export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/users/containers/AddUser.js b/scm-ui/src/users/containers/AddUser.js index f19f974265..069df04187 100644 --- a/scm-ui/src/users/containers/AddUser.js +++ b/scm-ui/src/users/containers/AddUser.js @@ -49,8 +49,8 @@ class AddUser extends React.Component<Props> { return ( <Page - title={t("add-user.title")} - subtitle={t("add-user.subtitle")} + title={t("addUser.title")} + subtitle={t("addUser.subtitle")} error={error} showContentOnError={true} > diff --git a/scm-ui/src/users/containers/DeleteUser.js b/scm-ui/src/users/containers/DeleteUser.js new file mode 100644 index 0000000000..b8b42fd9e8 --- /dev/null +++ b/scm-ui/src/users/containers/DeleteUser.js @@ -0,0 +1,113 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { + Subtitle, + DeleteButton, + confirmAlert, + ErrorNotification +} from "@scm-manager/ui-components"; +import { + deleteUser, + getDeleteUserFailure, + isDeleteUserPending +} from "../modules/users"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import type { History } from "history"; + +type Props = { + loading: boolean, + error: Error, + user: User, + confirmDialog?: boolean, + deleteUser: (user: User, callback?: () => void) => void, + + // context props + history: History, + t: string => string +}; + +class DeleteUser extends React.Component<Props> { + static defaultProps = { + confirmDialog: true + }; + + userDeleted = () => { + this.props.history.push("/users"); + }; + + deleteUser = () => { + this.props.deleteUser(this.props.user, this.userDeleted); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("deleteUser.confirmAlert.title"), + message: t("deleteUser.confirmAlert.message"), + buttons: [ + { + label: t("deleteUser.confirmAlert.submit"), + onClick: () => this.deleteUser() + }, + { + label: t("deleteUser.confirmAlert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.user._links.delete; + }; + + render() { + const { loading, error, confirmDialog, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deleteUser; + + if (!this.isDeletable()) { + return null; + } + + return ( + <> + <Subtitle subtitle={t("deleteUser.subtitle")} /> + <ErrorNotification error={error} /> + <div className="columns"> + <div className="column"> + <DeleteButton + label={t("deleteUser.button")} + action={action} + loading={loading} + /> + </div> + </div> + </> + ); + } +} + +const mapStateToProps = (state, ownProps) => { + const loading = isDeleteUserPending(state, ownProps.user.name); + const error = getDeleteUserFailure(state, ownProps.user.name); + return { + loading, + error + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteUser: (user: User, callback?: () => void) => { + dispatch(deleteUser(user, callback)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(translate("users")(DeleteUser))); diff --git a/scm-ui/src/users/containers/EditUser.js b/scm-ui/src/users/containers/EditUser.js index 55062ecb5b..942d5182e7 100644 --- a/scm-ui/src/users/containers/EditUser.js +++ b/scm-ui/src/users/containers/EditUser.js @@ -2,7 +2,8 @@ import React from "react"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; -import UserForm from "./../components/UserForm"; +import UserForm from "../components/UserForm"; +import DeleteUser from "./DeleteUser"; import type { User } from "@scm-manager/ui-types"; import { modifyUser, @@ -31,6 +32,7 @@ class EditUser extends React.Component<Props> { const { modifyUserReset, user } = this.props; modifyUserReset(user); } + userModified = (user: User) => () => { this.props.history.push(`/user/${user.name}`); }; @@ -49,11 +51,22 @@ class EditUser extends React.Component<Props> { user={user} loading={loading} /> + <hr /> + <DeleteUser user={user} /> </div> ); } } +const mapStateToProps = (state, ownProps) => { + const loading = isModifyUserPending(state, ownProps.user.name); + const error = getModifyUserFailure(state, ownProps.user.name); + return { + loading, + error + }; +}; + const mapDispatchToProps = dispatch => { return { modifyUser: (user: User, callback?: () => void) => { @@ -65,15 +78,6 @@ const mapDispatchToProps = dispatch => { }; }; -const mapStateToProps = (state, ownProps) => { - const loading = isModifyUserPending(state, ownProps.user.name); - const error = getModifyUserFailure(state, ownProps.user.name); - return { - loading, - error - }; -}; - export default connect( mapStateToProps, mapDispatchToProps diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index 4a827ee75a..9a1633a162 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -5,6 +5,7 @@ import { Page, Loading, Navigation, + SubNavigation, Section, NavLink, ErrorPage @@ -16,24 +17,16 @@ import type { User } from "@scm-manager/ui-types"; import type { History } from "history"; import { fetchUserByName, - deleteUser, getUserByName, isFetchUserPending, - getFetchUserFailure, - isDeleteUserPending, - getDeleteUserFailure + getFetchUserFailure } from "../modules/users"; - -import { - DeleteUserNavLink, - EditUserNavLink, - SetPasswordNavLink, - SetPermissionsNavLink -} from "./../components/navLinks"; +import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; import SetPermissions from "../../permissions/components/SetPermissions"; +import {ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { name: string, @@ -42,8 +35,7 @@ type Props = { error: Error, usersLink: string, - // dispatcher functions - deleteUser: (user: User, callback?: () => void) => void, + // dispatcher function fetchUserByName: (string, string) => void, // context objects @@ -57,14 +49,6 @@ class SingleUser extends React.Component<Props> { this.props.fetchUserByName(this.props.usersLink, this.props.name); } - userDeleted = () => { - this.props.history.push("/users"); - }; - - deleteUser = (user: User) => { - this.props.deleteUser(user, this.userDeleted); - }; - stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -82,8 +66,8 @@ class SingleUser extends React.Component<Props> { if (error) { return ( <ErrorPage - title={t("single-user.error-title")} - subtitle={t("single-user.error-subtitle")} + title={t("singleUser.errorTitle")} + subtitle={t("singleUser.errorSubtitle")} error={error} /> ); @@ -95,21 +79,26 @@ class SingleUser extends React.Component<Props> { const url = this.matchedUrl(); + const extensionProps = { + user, + url + }; + return ( <Page title={user.displayName}> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact component={() => <Details user={user} />} /> <Route - path={`${url}/edit`} + path={`${url}/settings/general`} component={() => <EditUser user={user} />} /> <Route - path={`${url}/password`} + path={`${url}/settings/password`} component={() => <SetUserPassword user={user} />} /> <Route - path={`${url}/permissions`} + path={`${url}/settings/permissions`} component={() => ( <SetPermissions selectedPermissionsLink={user._links.permissions} @@ -119,25 +108,34 @@ class SingleUser extends React.Component<Props> { </div> <div className="column"> <Navigation> - <Section label={t("single-user.navigation-label")}> + <Section label={t("singleUser.menu.navigationLabel")}> <NavLink to={`${url}`} icon="fas fa-info-circle" - label={t("single-user.information-label")} + label={t("singleUser.menu.informationNavLink")} /> - <EditUserNavLink user={user} editUrl={`${url}/edit`} /> - <SetPasswordNavLink - user={user} - passwordUrl={`${url}/password`} - /> - <SetPermissionsNavLink - user={user} - permissionsUrl={`${url}/permissions`} - /> - </Section> - <Section label={t("single-user.actions-label")}> - <DeleteUserNavLink user={user} deleteUser={this.deleteUser} /> - <NavLink to="/users" icon="fas fa-undo" label={t("single-user.back-label")} /> + <SubNavigation + to={`${url}/settings/general`} + label={t("singleUser.menu.settingsNavLink")} + > + <EditUserNavLink + user={user} + editUrl={`${url}/settings/general`} + /> + <SetPasswordNavLink + user={user} + passwordUrl={`${url}/settings/password`} + /> + <SetPermissionsNavLink + user={user} + permissionsUrl={`${url}/settings/permissions`} + /> + <ExtensionPoint + name="user.subnavigation" + props={extensionProps} + renderAll={true} + /> + </SubNavigation> </Section> </Navigation> </div> @@ -150,10 +148,8 @@ class SingleUser extends React.Component<Props> { const mapStateToProps = (state, ownProps) => { const name = ownProps.match.params.name; const user = getUserByName(state, name); - const loading = - isFetchUserPending(state, name) || isDeleteUserPending(state, name); - const error = - getFetchUserFailure(state, name) || getDeleteUserFailure(state, name); + const loading = isFetchUserPending(state, name); + const error = getFetchUserFailure(state, name); const usersLink = getUsersLink(state); return { usersLink, @@ -168,9 +164,6 @@ const mapDispatchToProps = dispatch => { return { fetchUserByName: (link: string, name: string) => { dispatch(fetchUserByName(link, name)); - }, - deleteUser: (user: User, callback?: () => void) => { - dispatch(deleteUser(user, callback)); } }; }; diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index cbb33dc68b..041ac226b4 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,143 +1,143 @@ -// @flow -import React from "react"; -import type { History } from "history"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; - -import { - fetchUsersByPage, - fetchUsersByLink, - getUsersFromState, - selectListAsCollection, - isPermittedToCreateUsers, - isFetchUsersPending, - getFetchUsersFailure -} from "../modules/users"; - -import { Page, Paginator } from "@scm-manager/ui-components"; -import { UserTable } from "./../components/table"; -import type { User, PagedCollection } from "@scm-manager/ui-types"; -import CreateUserButton from "../components/buttons/CreateUserButton"; -import { getUsersLink } from "../../modules/indexResource"; - -type Props = { - users: User[], - loading: boolean, - error: Error, - canAddUsers: boolean, - list: PagedCollection, - page: number, - usersLink: string, - - // context objects - t: string => string, - history: History, - - // dispatch functions - fetchUsersByPage: (link: string, page: number) => void, - fetchUsersByLink: (link: string) => void -}; - -class Users extends React.Component<Props> { - componentDidMount() { - this.props.fetchUsersByPage(this.props.usersLink, this.props.page); - } - - onPageChange = (link: string) => { - this.props.fetchUsersByLink(link); - }; - - /** - * reflect page transitions in the uri - */ - componentDidUpdate() { - const { page, list } = this.props; - if (list && (list.page || list.page === 0)) { - // backend starts paging by 0 - const statePage: number = list.page + 1; - if (page !== statePage) { - this.props.history.push(`/users/${statePage}`); - } - } - } - - render() { - const { users, loading, error, t } = this.props; - return ( - <Page - title={t("users.title")} - subtitle={t("users.subtitle")} - loading={loading || !users} - error={error} - > - <UserTable users={users} /> - {this.renderPaginator()} - {this.renderCreateButton()} - </Page> - ); - } - - renderPaginator() { - const { list } = this.props; - if (list) { - return <Paginator collection={list} onPageChange={this.onPageChange} />; - } - return null; - } - - renderCreateButton() { - if (this.props.canAddUsers) { - return <CreateUserButton />; - } else { - return; - } - } -} - -const getPageFromProps = props => { - let page = props.match.params.page; - if (page) { - page = parseInt(page, 10); - } else { - page = 1; - } - return page; -}; - -const mapStateToProps = (state, ownProps) => { - const users = getUsersFromState(state); - const loading = isFetchUsersPending(state); - const error = getFetchUsersFailure(state); - - const usersLink = getUsersLink(state); - - const page = getPageFromProps(ownProps); - const canAddUsers = isPermittedToCreateUsers(state); - const list = selectListAsCollection(state); - - return { - users, - loading, - error, - canAddUsers, - list, - page, - usersLink - }; -}; - -const mapDispatchToProps = dispatch => { - return { - fetchUsersByPage: (link: string, page: number) => { - dispatch(fetchUsersByPage(link, page)); - }, - fetchUsersByLink: (link: string) => { - dispatch(fetchUsersByLink(link)); - } - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("users")(Users)); +// @flow +import React from "react"; +import type { History } from "history"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; + +import { + fetchUsersByPage, + fetchUsersByLink, + getUsersFromState, + selectListAsCollection, + isPermittedToCreateUsers, + isFetchUsersPending, + getFetchUsersFailure +} from "../modules/users"; + +import { Page, CreateButton, Paginator } from "@scm-manager/ui-components"; +import { UserTable } from "./../components/table"; +import type { User, PagedCollection } from "@scm-manager/ui-types"; +import { getUsersLink } from "../../modules/indexResource"; + +type Props = { + users: User[], + loading: boolean, + error: Error, + canAddUsers: boolean, + list: PagedCollection, + page: number, + usersLink: string, + + // context objects + t: string => string, + history: History, + + // dispatch functions + fetchUsersByPage: (link: string, page: number) => void, + fetchUsersByLink: (link: string) => void +}; + +class Users extends React.Component<Props> { + componentDidMount() { + this.props.fetchUsersByPage(this.props.usersLink, this.props.page); + } + + onPageChange = (link: string) => { + this.props.fetchUsersByLink(link); + }; + + /** + * reflect page transitions in the uri + */ + componentDidUpdate() { + const { page, list } = this.props; + if (list && (list.page || list.page === 0)) { + // backend starts paging by 0 + const statePage: number = list.page + 1; + if (page !== statePage) { + this.props.history.push(`/users/${statePage}`); + } + } + } + + render() { + const { users, loading, error, t } = this.props; + return ( + <Page + title={t("users.title")} + subtitle={t("users.subtitle")} + loading={loading || !users} + error={error} + > + <UserTable users={users} /> + {this.renderPaginator()} + {this.renderCreateButton()} + </Page> + ); + } + + renderPaginator() { + const { list } = this.props; + if (list) { + return <Paginator collection={list} onPageChange={this.onPageChange} />; + } + return null; + } + + renderCreateButton() { + const { t } = this.props; + if (this.props.canAddUsers) { + return <CreateButton label={t("users.createButton")} link="/users/add" />; + } else { + return; + } + } +} + +const getPageFromProps = props => { + let page = props.match.params.page; + if (page) { + page = parseInt(page, 10); + } else { + page = 1; + } + return page; +}; + +const mapStateToProps = (state, ownProps) => { + const users = getUsersFromState(state); + const loading = isFetchUsersPending(state); + const error = getFetchUsersFailure(state); + + const usersLink = getUsersLink(state); + + const page = getPageFromProps(ownProps); + const canAddUsers = isPermittedToCreateUsers(state); + const list = selectListAsCollection(state); + + return { + users, + loading, + error, + canAddUsers, + list, + page, + usersLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchUsersByPage: (link: string, page: number) => { + dispatch(fetchUsersByPage(link, page)); + }, + fetchUsersByLink: (link: string) => { + dispatch(fetchUsersByLink(link)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("users")(Users)); diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index be708d1bea..d3c29a6fad 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -94,14 +94,6 @@ $fa-font-path: "webfonts"; } } -//border around options -.has-border-around { - border-top: 1px solid #eee; - border-left: 1px solid #eee; - border-right: 1px solid #eee; - border-bottom: 1px solid #eee; -} - // multiline Columns .columns.is-multiline { .column.is-half { @@ -201,6 +193,33 @@ $fa-font-path: "webfonts"; } } +// panels +.panel { + .panel-heading > .field { + margin-bottom: 0; // replace selector margin + } + .panel-block { + display: block; + } + + .panel-footer { + background-color: whitesmoke; + border-radius: 0 0 4px 4px; + color: #363636; + font-size: 1.25em; + font-weight: 300; + line-height: 1.25; + padding: 0.5em 0.75em; + + border-left: 1px solid #dbdbdb; + border-right: 1px solid #dbdbdb; + + &:last-child { + border-bottom: 1px solid #dbdbdb; + } + } +} + // forms .field:not(.is-grouped) { margin-bottom: 1rem; @@ -283,34 +302,76 @@ $fa-font-path: "webfonts"; } .menu-list { a { - border-radius: 0; color: #333; padding: 1rem; - border-top: 1px solid #eee; - border-left: 1px solid #eee; - border-right: 1px solid #eee; &.is-active { color: $blue; background-color: #fff; - - &:before { - position: relative; - content: " "; - background: $blue; - height: 53px; - width: 2px; - display: block; - left: -17px; - float: left; - top: -16px; - } } } - > li:first-child > a { + + > li { + ul { + margin: 0; + border-top: 1px solid #eee; + + li { + border-right: none; + } + li:last-child { + border-bottom: none; + } + } + + > a.is-active:before { + position: relative; + content: " "; + background: $blue; + height: 53px; + width: 2px; + display: block; + left: -17px; + float: left; + top: -16px; + } + + border-radius: 0; + border-top: 1px solid #eee; + border-left: 1px solid #eee; + border-right: 1px solid #eee; + } + > li:first-child { border-top: none; } - li:last-child > a { + li:last-child { border-bottom: 1px solid #eee; } + div { + margin-bottom: 0; + } +} +.sub-menu li { + line-height: 1; + + a { + padding: 0.75rem 1rem; + } + + a:before { + font-family: "Font Awesome 5 Free"; + font-weight: 900; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + content: "\f105"; + padding-right: 5px; + } + + i { + display: none; + } } diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index c83cea36d5..45da9fec49 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -115,9 +115,9 @@ </dependency> <dependency> - <groupId>javax</groupId> - <artifactId>javaee-api</artifactId> - <version>7.0</version> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-json-provider</artifactId> + <version>${jackson.version}</version> </dependency> <!-- rest api --> @@ -158,6 +158,24 @@ <version>${resteasy.version}</version> </dependency> + <dependency> + <groupId>org.hibernate</groupId> + <artifactId>hibernate-validator</artifactId> + <version>5.3.6.Final</version> + </dependency> + + <dependency> + <groupId>javax.el</groupId> + <artifactId>javax.el-api</artifactId> + <version>2.2.4</version> + </dependency> + + <dependency> + <groupId>org.glassfish.web</groupId> + <artifactId>javax.el</artifactId> + <version>2.2.4</version> + </dependency> + <!-- injection --> <dependency> @@ -203,18 +221,6 @@ <version>1.4.01</version> </dependency> - <!-- only for BeanComparator, replace with own implementation --> - - <dependency> - <groupId>commons-beanutils</groupId> - <artifactId>commons-beanutils</artifactId> - </dependency> - - <dependency> - <groupId>commons-collections</groupId> - <artifactId>commons-collections</artifactId> - </dependency> - <!-- fix installation of httpasswd-plugin https://groups.google.com/d/topic/scmmanager/eN7UtG8TwW8/discussion @@ -255,19 +261,19 @@ <dependency> <groupId>com.github.sdorra</groupId> <artifactId>web-resources</artifactId> - <version>1.0.2</version> + <version>1.0.4</version> </dependency> <dependency> <groupId>com.github.sdorra</groupId> <artifactId>spotter-core</artifactId> - <version>1.1.0</version> + <version>1.2.1</version> </dependency> <dependency> <groupId>org.apache.tika</groupId> <artifactId>tika-core</artifactId> - <version>1.18</version> + <version>1.20</version> </dependency> <!-- test scope --> @@ -561,8 +567,6 @@ <selenium.version>2.53.1</selenium.version> <wagon.version>1.0</wagon.version> <mustache.version>0.8.17</mustache.version> - <resteasy.version>3.1.4.Final</resteasy.version> - <jackson.version>2.8.9</jackson.version> <netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server> <sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria> <sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey> diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/NotAllowedExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/NotAllowedExceptionMapper.java new file mode 100644 index 0000000000..1d268b6855 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/NotAllowedExceptionMapper.java @@ -0,0 +1,12 @@ +package sonia.scm.api.rest; + +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class NotAllowedExceptionMapper extends StatusExceptionMapper<NotAllowedException> { + public NotAllowedExceptionMapper() { + super(NotAllowedException.class, Response.Status.METHOD_NOT_ALLOWED); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java index 4253c456fb..dfc0bd2a5d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java @@ -37,7 +37,6 @@ package sonia.scm.api.rest.resources; import com.google.common.annotations.VisibleForTesting; import com.google.common.net.UrlEscapers; -import org.apache.commons.beanutils.BeanComparator; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +46,7 @@ import sonia.scm.ModelObject; import sonia.scm.PageResult; import sonia.scm.api.rest.RestExceptionResult; import sonia.scm.util.AssertUtil; +import sonia.scm.util.Comparables; import sonia.scm.util.Util; import javax.ws.rs.core.CacheControl; @@ -56,15 +56,10 @@ import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.util.Arrays; +import java.net.URI; import java.util.Collection; import java.util.Comparator; import java.util.Date; -import java.net.URI; //~--- JDK imports ------------------------------------------------------------ @@ -510,21 +505,11 @@ public abstract class AbstractManagerResource<T extends ModelObject> { return builder.build(); } - @SuppressWarnings("unchecked") - private Comparator<T> createComparator(String sortBy, boolean desc) - { - checkSortByField(sortBy); - Comparator comparator; - - if (desc) - { - comparator = new BeanReverseComparator(sortBy); + private Comparator<T> createComparator(String sortBy, boolean desc) { + Comparator<T> comparator = Comparables.comparator(type, sortBy); + if (desc) { + comparator = comparator.reversed(); } - else - { - comparator = new BeanComparator(sortBy); - } - return comparator; } @@ -558,21 +543,6 @@ public abstract class AbstractManagerResource<T extends ModelObject> { return items; } - // We have to handle IntrospectionException here, because it's a checked exception - // It shouldn't occur really - so creating a new unchecked exception would be over-engineered here - @SuppressWarnings("squid:S00112") - private void checkSortByField(String sortBy) { - try { - BeanInfo info = Introspector.getBeanInfo(type); - PropertyDescriptor[] pds = info.getPropertyDescriptors(); - if (Arrays.stream(pds).noneMatch(p -> p.getName().equals(sortBy))) { - throw new IllegalArgumentException("sortBy"); - } - } catch (IntrospectionException e) { - throw new RuntimeException("error introspecting model type " + type.getName(), e); - } - } - protected PageResult<T> fetchPage(String sortBy, boolean desc, int pageNumber, int pageSize) { AssertUtil.assertPositive(pageNumber); @@ -608,51 +578,4 @@ public abstract class AbstractManagerResource<T extends ModelObject> { return lastModified; } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 11/06/09 - * @author Enter your name here... - */ - private static class BeanReverseComparator extends BeanComparator - { - - /** Field description */ - private static final long serialVersionUID = -8535047820348790009L; - - //~--- constructors ------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param sortby - */ - private BeanReverseComparator(String sortby) - { - super(sortby); - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param o1 - * @param o2 - * - * @return - */ - @Override - @SuppressWarnings("unchecked") - public int compare(Object o1, Object o2) - { - return super.compare(o1, o2) * -1; - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java index d2ce744c19..79b5dbc2ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java @@ -6,8 +6,6 @@ import sonia.scm.repository.api.CatCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.util.IOUtil; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.OutputStream; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java index d177e05a5e..b7f994b967 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java @@ -42,7 +42,6 @@ import sonia.scm.repository.api.RepositoryService; import sonia.scm.util.IOUtil; import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.OutputStream; 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 658abbded8..71b1127ad8 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 @@ -26,7 +26,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; -import java.util.List; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java similarity index 92% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index 16e33aac5f..479a43aef1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -24,7 +24,7 @@ import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @Mapper -public abstract class ChangesetToChangesetDtoMapper extends HalAppenderMapper implements InstantAttributeMapper { +public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMapper implements InstantAttributeMapper, ChangesetToChangesetDtoMapper{ @Inject private RepositoryServiceFactory serviceFactory; @@ -68,6 +68,7 @@ public abstract class ChangesetToChangesetDtoMapper extends HalAppenderMapper im Links.Builder linksBuilder = linkingTo() .self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), source.getId())) .single(link("diff", resourceLinks.diff().self(namespace, name, source.getId()))) + .single(link("sources", resourceLinks.source().self(namespace, name, source.getId()))) .single(link("modifications", resourceLinks.modifications().self(namespace, name, source.getId()))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java index bd889d5de5..d155fbede6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.Setter; import sonia.scm.ContextEntry; -import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 4c111e6707..6c13dc33a5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -10,7 +10,6 @@ import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java index 2ccfcff38e..a150570316 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java @@ -7,7 +7,6 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.Pattern; import java.time.Instant; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java index be1aca5814..3812f700da 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDtoToGroupMapper.java @@ -4,8 +4,6 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import sonia.scm.group.Group; -import java.time.Instant; - @Mapper public abstract class GroupDtoToGroupMapper extends BaseDtoMapper { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 859a6481f6..c74f16ad70 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -28,7 +28,7 @@ public class MapperModule extends AbstractModule { bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass()); bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); - bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass()); + bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(DefaultChangesetToChangesetDtoMapper.class).getClass()); bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java index 87d1aeca9f..31269d468e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -1,7 +1,6 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; -import org.mapstruct.Context; import sonia.scm.security.PermissionDescriptor; import sonia.scm.security.PermissionPermissions; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index a9bd5c2424..e1e1260a4d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -23,7 +23,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; public class RepositoryCollectionResource { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index 09683db488..fe8c2c19b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -9,7 +8,6 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.validator.constraints.NotEmpty; -import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import java.util.Collection; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index a4fe9adb94..a7442a2262 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -11,7 +11,6 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index afaa28bfe8..be5a1e7ac2 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -33,12 +33,9 @@ package sonia.scm.boot; //~--- non-JDK imports -------------------------------------------------------- -import com.github.legman.Subscribe; - import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; -import com.google.inject.servlet.GuiceFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java index b77a927a2d..fc52ef4eff 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java @@ -42,7 +42,6 @@ import org.slf4j.MDC; import sonia.scm.SCMContext; import sonia.scm.security.DefaultKeyGenerator; -import sonia.scm.security.KeyGenerator; import sonia.scm.web.filter.HttpFilter; //~--- JDK imports ------------------------------------------------------------ diff --git a/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java index 508e804d1f..e7d020ae18 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java @@ -51,8 +51,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; - //~--- JDK imports ------------------------------------------------------------ /** diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java index 64e26405a1..5b895a34fa 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java @@ -35,7 +35,6 @@ import io.jsonwebtoken.Claims; import java.util.Collections; import java.util.Date; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java index 6b6b06aa9c..1fab500d79 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java +++ b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java @@ -1,7 +1,5 @@ package sonia.scm.security; -import org.apache.commons.collections.CollectionUtils; - import java.util.Collection; import java.util.Collections; import java.util.Objects; @@ -33,8 +31,9 @@ public class RepositoryRole { if (this == o) return true; if (!(o instanceof RepositoryRole)) return false; RepositoryRole that = (RepositoryRole) o; - return name.equals(that.name) && - CollectionUtils.isEqualCollection(this.verbs, that.verbs); + return name.equals(that.name) + && this.verbs.containsAll(that.verbs) + && this.verbs.size() == that.verbs.size(); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java index 77683bd6be..e58945a346 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java @@ -17,9 +17,6 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Set; -import static sonia.scm.util.HttpUtil.AUTHENTICATION_REALM; -import static sonia.scm.util.HttpUtil.HEADER_WWW_AUTHENTICATE; - @Priority(Filters.PRIORITY_AUTHENTICATION) @WebElement(value = HttpProtocolServlet.PATTERN) public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter { diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index 696174d6e0..fd9745be83 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -21,7 +21,7 @@ import java.util.Comparator; import static java.util.Collections.emptyList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java index b60775a73b..8bdbacafc2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java @@ -3,7 +3,6 @@ package sonia.scm.api.v2; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.google.common.collect.Lists; import com.google.common.io.Resources; import org.junit.Test; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 4994c11b08..9216922e19 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -82,7 +82,7 @@ public class BranchRootResourceTest extends RepositoryTestBase { @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java index 69695279e6..7653fbe122 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import org.assertj.core.api.Assertions; import org.junit.Test; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -17,7 +16,7 @@ public class ChangesetCollectionToDtoMapperTest { public static final Repository REPOSITORY = new Repository("", "git", "space", "name"); public static final Changeset CHANGESET = new Changeset(); - private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper = mock(ChangesetToChangesetDtoMapper.class); + private final DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper = mock(DefaultChangesetToChangesetDtoMapperImpl.class); private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, ResourceLinksMock.createMock(URI.create("/"))); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index d5c0f91f81..952c8504f6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -65,7 +65,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private ChangesetRootResource changesetRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 4fb25d2371..66689c6ac6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -18,6 +18,7 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -68,7 +69,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldGetGlobalConfig() throws URISyntaxException { + public void shouldGetGlobalConfig() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java index 52c9a434c0..a8b3c15158 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -66,7 +66,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase { private FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private FileHistoryRootResource fileHistoryRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 646e9d0839..3e2d0f9663 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -24,6 +24,7 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -100,7 +101,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetGroup() throws URISyntaxException { + public void shouldGetGroup() throws URISyntaxException, UnsupportedEncodingException { Group group = createDummyGroup(); when(groupManager.get("admin")).thenReturn(group); @@ -305,7 +306,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetAll() throws URISyntaxException { + public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -317,7 +318,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetPermissionLink() throws URISyntaxException { + public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); MockHttpResponse response = new MockHttpResponse(); @@ -329,7 +330,7 @@ public class GroupRootResourceTest { } @Test - public void shouldGetPermissions() throws URISyntaxException { + public void shouldGetPermissions() throws URISyntaxException, UnsupportedEncodingException { when(permissionAssigner.readPermissionsForGroup("admin")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions"); MockHttpResponse response = new MockHttpResponse(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java index e4495a0455..b965c2f2c3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java @@ -74,7 +74,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase { private IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper; @InjectMocks - private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; private IncomingRootResource incomingRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index 052a059959..cd2a172c1b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -22,12 +22,12 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @@ -78,7 +78,7 @@ public class MeResourceTest { } @Test - public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException { + public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException, UnsupportedEncodingException { applyUserToSubject(originalUser); MockHttpRequest request = MockHttpRequest.get("/" + MeResource.ME_PATH_V2); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index 2795562b14..4472acb2c5 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -36,6 +36,7 @@ import sonia.scm.repository.RepositoryPermission; import sonia.scm.web.VndMediaType; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -53,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -214,7 +215,12 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { .expectedResponseStatus(200) .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) .responseValidator((response) -> { - String body = response.getContentAsString(); + String body = null; + try { + body = response.getContentAsString(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } ObjectMapper mapper = new ObjectMapper(); try { RepositoryPermissionDto actualRepositoryPermissionDto = mapper.readValue(body, RepositoryPermissionDto.class); @@ -268,13 +274,21 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestPOSTPermission .content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : true}") .expectedResponseStatus(201) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("POST response has no body") .isBlank()) ); assertGettingExpectedPermissions(expectedPermissions, PERMISSION_WRITE); } + private String getContentAsString(MockHttpResponse response) { + try { + return response.getContentAsString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("could not get content from response", e); + } + } + @Test public void shouldNotAddExistingPermission() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); @@ -296,7 +310,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"verbs\" : [\"*\"], \"groupPermission\" : false}") .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("PUT response has no body") .isBlank()) ); @@ -312,7 +326,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("DELETE response has no body") .isBlank()) ); @@ -327,7 +341,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("DELETE response has no body") .isBlank()) ); @@ -335,7 +349,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) - .responseValidator(response -> assertThat(response.getContentAsString()) + .responseValidator(response -> assertThat(getContentAsString(response)) .as("DELETE response has no body") .isBlank()) ); @@ -346,7 +360,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200) .responseValidator((response) -> { - String body = response.getContentAsString(); + String body = getContentAsString(response); ObjectMapper mapper = new ObjectMapper(); try { HalRepresentation halRepresentation = mapper.readValue(body, HalRepresentation.class); 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 bf4366f0b2..1f6ed6b3a7 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 @@ -27,6 +27,7 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -41,9 +42,9 @@ import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -120,7 +121,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { } @Test - public void shouldFindExistingRepository() throws URISyntaxException { + public void shouldFindExistingRepository() throws URISyntaxException, UnsupportedEncodingException { mockRepository("space", "repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); @@ -133,7 +134,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { } @Test - public void shouldMapProperties() throws URISyntaxException { + public void shouldMapProperties() throws URISyntaxException, UnsupportedEncodingException { Repository repository = mockRepository("space", "repo"); repository.setProperty("testKey", "testValue"); @@ -146,7 +147,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { } @Test - public void shouldGetAll() throws URISyntaxException { + public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException { PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); 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 9df7273680..4b02508ae8 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 @@ -23,7 +23,7 @@ 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.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java index 99a1435923..b2dafc8cfe 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java @@ -16,6 +16,7 @@ import sonia.scm.plugin.*; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; @@ -87,7 +88,7 @@ public class UIRootResourceTest { } @Test - public void shouldReturnPlugin() throws URISyntaxException { + public void shouldReturnPlugin() throws URISyntaxException, UnsupportedEncodingException { mockPlugins(mockPlugin("awesome", "Awesome", createPluginResources("my/awesome.bundle.js"))); MockHttpRequest request = MockHttpRequest.get("/v2/ui/plugins/awesome"); @@ -101,7 +102,7 @@ public class UIRootResourceTest { } @Test - public void shouldReturnPlugins() throws URISyntaxException { + public void shouldReturnPlugins() throws URISyntaxException, UnsupportedEncodingException { mockPlugins( mockPlugin("awesome", "Awesome", createPluginResources("my/awesome.bundle.js")), mockPlugin("special", "Special", createPluginResources("my/special.bundle.js")) @@ -120,7 +121,7 @@ public class UIRootResourceTest { } @Test - public void shouldNotReturnPluginsWithoutResources() throws URISyntaxException { + public void shouldNotReturnPluginsWithoutResources() throws URISyntaxException, UnsupportedEncodingException { mockPlugins( mockPlugin("awesome", "Awesome", createPluginResources("my/awesome.bundle.js")), mockPlugin("special") diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java index 19f247b3b2..552009b73f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java @@ -10,7 +10,6 @@ import sonia.scm.user.User; import java.time.Instant; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class UserDtoToUserMapperTest { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 88142e4d50..4047dfadd2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -14,7 +14,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Spy; import sonia.scm.ContextEntry; import sonia.scm.NotFoundException; import sonia.scm.PageResult; @@ -26,6 +25,7 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -35,8 +35,8 @@ import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -76,7 +76,7 @@ public class UserRootResourceTest { private User originalUser; @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { initMocks(this); originalUser = createDummyUser("Neo"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); @@ -97,7 +97,7 @@ public class UserRootResourceTest { } @Test - public void shouldCreateFullResponseForAdmin() throws URISyntaxException { + public void shouldCreateFullResponseForAdmin() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpResponse response = new MockHttpResponse(); @@ -137,7 +137,7 @@ public class UserRootResourceTest { @Test @SubjectAware(username = "unpriv") - public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException { + public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpResponse response = new MockHttpResponse(); @@ -331,7 +331,7 @@ public class UserRootResourceTest { } @Test - public void shouldCreatePageForOnePageOnly() throws URISyntaxException { + public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException { PageResult<User> singletonPageResult = createSingletonPageResult(1); when(userManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2); @@ -347,7 +347,7 @@ public class UserRootResourceTest { } @Test - public void shouldCreatePageForMultiplePages() throws URISyntaxException { + public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException { PageResult<User> singletonPageResult = createSingletonPageResult(3); when(userManager.getPage(any(), eq(1), eq(1))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1"); @@ -365,7 +365,7 @@ public class UserRootResourceTest { } @Test - public void shouldGetPermissionLink() throws URISyntaxException { + public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpResponse response = new MockHttpResponse(); @@ -377,7 +377,7 @@ public class UserRootResourceTest { } @Test - public void shouldGetPermissions() throws URISyntaxException { + public void shouldGetPermissions() throws URISyntaxException, UnsupportedEncodingException { when(permissionAssigner.readPermissionsForUser("Neo")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions"); MockHttpResponse response = new MockHttpResponse(); diff --git a/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java index b8b538c82b..eac4a12340 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java @@ -2,7 +2,6 @@ package sonia.scm.boot; import com.github.legman.Subscribe; import com.google.common.base.Charsets; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; diff --git a/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java index a26cf3b215..c9d8c594b4 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java @@ -13,7 +13,6 @@ import java.util.Enumeration; import java.util.Set; import java.util.Vector; -import static org.junit.Assert.*; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java index a60c884b64..399f20cd3f 100644 --- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java @@ -41,6 +41,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.io.ByteSource; +import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,8 +50,6 @@ import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; -import static org.hamcrest.Matchers.*; - import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -136,7 +135,7 @@ public class DefaultAdvancedHttpResponseTest connection, 200, "OK"); Multimap<String, String> headers = response.getHeaders(); - assertThat(headers.get("Test"), contains("One", "Two")); + assertThat(headers.get("Test"), Matchers.contains("One", "Two")); assertTrue(headers.get("Test-2").isEmpty()); } @@ -144,8 +143,7 @@ public class DefaultAdvancedHttpResponseTest /** Field description */ private final DefaultAdvancedHttpClient client = - new DefaultAdvancedHttpClient(new ScmConfiguration(), - new HashSet<ContentTransformer>(), new SSLContextProvider()); + new DefaultAdvancedHttpClient(new ScmConfiguration(), new HashSet<>(), new SSLContextProvider()); /** Field description */ @Mock diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java index ae65f5c1ae..df31977de1 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/MultiParentClassLoaderTest.java @@ -29,9 +29,6 @@ package sonia.scm.plugin; -import com.google.common.base.Enums; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; import java.io.IOException; import java.net.URL; import java.util.Arrays; diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java index efaeb702fe..baf4c659cc 100644 --- a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java +++ b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java @@ -32,12 +32,11 @@ package sonia.scm.schedule; import org.junit.Test; -import static org.junit.Assert.*; + import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; + import org.junit.Before; import org.junit.runner.RunWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.quartz.JobKey; diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index 5c7aa08f37..c2d75358fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -33,18 +33,13 @@ package sonia.scm.security; import com.google.common.collect.ImmutableSet; import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; -import org.junit.Ignore; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.stubbing.Answer; import java.util.HashMap; import java.util.Set; @@ -52,7 +47,6 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java index cce3fea2b1..f59991f2cc 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java @@ -47,7 +47,6 @@ import sonia.scm.store.ConfigurationEntryStoreFactory; import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; diff --git a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java index 8e261b75cc..ab31d751fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java @@ -45,9 +45,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import sonia.scm.NotFoundException; -import sonia.scm.repository.InitialRepositoryLocationResolver; -import sonia.scm.repository.RepositoryDAO; -import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.user.xml.XmlUserDAO; diff --git a/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java index 29c7dea358..5f95a171d2 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java @@ -3,7 +3,7 @@ package sonia.scm.web.cgi; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.servlet.http.HttpServletRequest; diff --git a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java index e028857e2c..bb3c7b5f1e 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java @@ -35,8 +35,6 @@ import java.util.Enumeration; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.Silent.class)