From 290f8466ebc71d2ac72279ff33df9afa67d15100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 8 Apr 2019 11:10:34 +0200 Subject: [PATCH 01/38] Delete old 1.x resources --- scm-webapp/src/main/doc/enunciate.xml | 72 ------ .../resources/BrowserStreamingOutput.java | 37 --- .../resources/ChangePasswordResource.java | 173 -------------- .../rest/resources/DiffStreamingOutput.java | 109 --------- .../resources/RepositoryRootResource.java | 213 ----------------- .../api/rest/resources/SearchResource.java | 216 ----------------- .../java/sonia/scm/search/SearchHandler.java | 221 ------------------ .../java/sonia/scm/search/SearchResult.java | 117 ---------- .../java/sonia/scm/search/SearchResults.java | 110 --------- 9 files changed, 1268 deletions(-) delete mode 100644 scm-webapp/src/main/doc/enunciate.xml delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/search/SearchResult.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/search/SearchResults.java diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml deleted file mode 100644 index 6e7fc218aa..0000000000 --- a/scm-webapp/src/main/doc/enunciate.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - SCM-Manager API - - - SCM-Manager API -

This page describes the RESTful Web Service API of SCM-Manager ${project.version}.

- ]]> -
- - - - - - - - - - - - - - - - - - -
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 deleted file mode 100644 index 79b5dbc2ae..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java +++ /dev/null @@ -1,37 +0,0 @@ -package sonia.scm.api.rest.resources; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.api.CatCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.util.IOUtil; - -import javax.ws.rs.core.StreamingOutput; -import java.io.IOException; -import java.io.OutputStream; - -public class BrowserStreamingOutput implements StreamingOutput { - - private static final Logger logger = - LoggerFactory.getLogger(BrowserStreamingOutput.class); - - private final CatCommandBuilder builder; - private final String path; - private final RepositoryService repositoryService; - - public BrowserStreamingOutput(RepositoryService repositoryService, - CatCommandBuilder builder, String path) { - this.repositoryService = repositoryService; - this.builder = builder; - this.path = path; - } - - @Override - public void write(OutputStream output) throws IOException { - try { - builder.retriveContent(output, path); - } finally { - IOUtil.close(repositoryService); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java deleted file mode 100644 index 828cbf8164..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authc.credential.PasswordService; -import org.apache.shiro.subject.Subject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.api.rest.RestActionResult; -import sonia.scm.security.Role; -import sonia.scm.security.ScmSecurityException; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; -import sonia.scm.util.AssertUtil; - -import javax.ws.rs.FormParam; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * Resource to change the password of the authenticated user. - * - * @author Sebastian Sdorra - */ -@Path("action/change-password") -public class ChangePasswordResource -{ - - /** the logger for ChangePasswordResource */ - private static final Logger logger = - LoggerFactory.getLogger(ChangePasswordResource.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param userManager - * @param encryptionHandler - */ - @Inject - public ChangePasswordResource(UserManager userManager, - PasswordService encryptionHandler) - { - this.userManager = userManager; - this.passwordService = encryptionHandler; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Changes the password of the current user. - * - * @param oldPassword old password of the current user - * @param newPassword new password for the current user - */ - @POST - @TypeHint(RestActionResult.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the old password is not correct"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response changePassword(@FormParam("old-password") String oldPassword, @FormParam("new-password") String newPassword) { - AssertUtil.assertIsNotEmpty(oldPassword); - AssertUtil.assertIsNotEmpty(newPassword); - - int length = newPassword.length(); - - if ((length < 6) || (length > 32)) - { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - - Response response = null; - Subject subject = SecurityUtils.getSubject(); - - if (!subject.hasRole(Role.USER)) - { - throw new ScmSecurityException("user is not authenticated"); - } - - User currentUser = subject.getPrincipals().oneByType(User.class); - - if (logger.isInfoEnabled()) - { - logger.info("password change for user {}", currentUser.getName()); - } - - // Only account of the default type can change their password - if (currentUser.getType().equals(userManager.getDefaultType())) - { - User dbUser = userManager.get(currentUser.getName()); - - if (passwordService.passwordsMatch(oldPassword, dbUser.getPassword())) - { - dbUser.setPassword(passwordService.encryptPassword(newPassword)); - userManager.modify(dbUser); - response = Response.ok(new RestActionResult(true)).build(); - } - else - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - } - else - { - //J- - logger.error( - "Only account of the default type ({}) can change their password", - userManager.getDefaultType() - ); - //J+ - response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - - return response; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final PasswordService passwordService; - - /** Field description */ - private final UserManager userManager; -} 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 deleted file mode 100644 index b7f994b967..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.api.DiffCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.util.IOUtil; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.StreamingOutput; -import java.io.IOException; -import java.io.OutputStream; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -public class DiffStreamingOutput implements StreamingOutput -{ - - /** the logger for DiffStreamingOutput */ - private static final Logger logger = - LoggerFactory.getLogger(DiffStreamingOutput.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param repositoryService - * @param builder - */ - public DiffStreamingOutput(RepositoryService repositoryService, - DiffCommandBuilder builder) - { - this.repositoryService = repositoryService; - this.builder = builder; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param output - * - * @throws IOException - * @throws WebApplicationException - */ - @Override - public void write(OutputStream output) throws IOException { - try - { - builder.retrieveContent(output); - } - finally - { - IOUtil.close(repositoryService); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final DiffCommandBuilder builder; - - /** Field description */ - private final RepositoryService repositoryService; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java deleted file mode 100644 index d5ea6c88de..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Maps; -import com.google.common.collect.Ordering; -import com.google.inject.Inject; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryTypePredicate; -import sonia.scm.template.Viewable; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import java.io.IOException; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Path("help/repository-root/{type}.html") -public class RepositoryRootResource -{ - - private static final String TEMPLATE = "/templates/repository-root.mustache"; - - private final RepositoryManager repositoryManager; - - /** - * Constructs ... - * - * @param repositoryManager - */ - @Inject - public RepositoryRootResource(RepositoryManager repositoryManager) - { - this.repositoryManager = repositoryManager; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param request - * @param type - * - * @return - * - * @throws IOException - */ - @GET - @Produces(MediaType.TEXT_HTML) - public Viewable renderRepositoriesRoot(@Context HttpServletRequest request, @PathParam("type") final String type) - { - //J- - Collection unsortedRepositories = - Collections2.transform( - Collections2.filter( - repositoryManager.getAll(), new RepositoryTypePredicate(type)) - , new RepositoryTransformFunction() - ); - - List repositories = Ordering.from( - new RepositoryTemplateElementComparator() - ).sortedCopy(unsortedRepositories); - //J+ - Map environment = Maps.newHashMap(); - - environment.put("repositories", repositories); - - return new Viewable(TEMPLATE, environment); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 12/05/28 - * @author Enter your name here... - */ - public static class RepositoryTemplateElement - { - - public RepositoryTemplateElement(Repository repository) - { - this.repository = repository; - } - - //~--- get methods -------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return repository.getName(); - } - - /** - * Method description - * - * - * @return - */ - public Repository getRepository() - { - return repository; - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private Repository repository; - - } - - - /** - * Class description - * - * - * @version Enter version here..., 12/05/29 - * @author Enter your name here... - */ - private static class RepositoryTemplateElementComparator - implements Comparator - { - - /** - * Method description - * - * - * @param left - * @param right - * - * @return - */ - @Override - public int compare(RepositoryTemplateElement left, - RepositoryTemplateElement right) - { - return left.getName().compareTo(right.getName()); - } - } - - - /** - * Class description - * - * - * @version Enter version here..., 12/05/28 - * @author Enter your name here... - */ - private static class RepositoryTransformFunction - implements Function - { - @Override - public RepositoryTemplateElement apply(Repository repository) - { - return new RepositoryTemplateElement(repository); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java deleted file mode 100644 index f22e6cf8c4..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.github.legman.Subscribe; - -import com.google.common.base.Function; -import com.google.inject.Inject; -import com.google.inject.Singleton; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; - -import sonia.scm.cache.Cache; -import sonia.scm.cache.CacheManager; -import sonia.scm.group.Group; -import sonia.scm.group.GroupEvent; -import sonia.scm.group.GroupManager; -import sonia.scm.search.SearchHandler; -import sonia.scm.search.SearchResult; -import sonia.scm.search.SearchResults; -import sonia.scm.user.User; -import sonia.scm.user.UserEvent; -import sonia.scm.user.UserManager; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; - -/** - * RESTful Web Service Resource to search users and groups. This endpoint can be used to implement typeahead input - * fields for permissions. - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("search") -public class SearchResource -{ - - /** Field description */ - public static final String CACHE_GROUP = "sonia.cache.search.groups"; - - /** Field description */ - public static final String CACHE_USER = "sonia.cache.search.users"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param userManager - * @param groupManager - * @param cacheManager - */ - @Inject - public SearchResource(UserManager userManager, GroupManager groupManager, - CacheManager cacheManager) - { - - // create user searchhandler - Cache userCache = cacheManager.getCache(CACHE_USER); - - this.userSearchHandler = new SearchHandler(userCache, userManager); - - // create group searchhandler - Cache groupCache = - cacheManager.getCache(CACHE_GROUP); - - this.groupSearchHandler = new SearchHandler(groupCache, - groupManager); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param event - */ - @Subscribe - public void onEvent(UserEvent event) - { - if (event.getEventType().isPost()) - { - userSearchHandler.clearCache(); - } - } - - /** - * Method description - * - * - * @param event - */ - @Subscribe - public void onEvent(GroupEvent event) - { - if (event.getEventType().isPost()) - { - groupSearchHandler.clearCache(); - } - } - - /** - * Returns a list of groups found by the given search string. - * - * @param queryString the search string - * - * @return - */ - @GET - @Path("groups") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public SearchResults searchGroups(@QueryParam("query") String queryString) - { - return groupSearchHandler.search(queryString, - new Function() - { - @Override - public SearchResult apply(Group group) - { - String label = group.getName(); - String description = group.getDescription(); - - if (description != null) - { - label = label.concat(" (").concat(description).concat(")"); - } - - return new SearchResult(group.getName(), label); - } - }); - } - - /** - * Returns a list of users found by the given search string. - * - * @param queryString the search string - * - * @return - */ - @GET - @Path("users") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public SearchResults searchUsers(@QueryParam("query") String queryString) - { - return userSearchHandler.search(queryString, - new Function() - { - @Override - public SearchResult apply(User user) - { - StringBuilder label = new StringBuilder(user.getName()); - - label.append(" (").append(user.getDisplayName()).append(")"); - - return new SearchResult(user.getName(), label.toString()); - } - }); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final SearchHandler groupSearchHandler; - - /** Field description */ - private final SearchHandler userSearchHandler; -} diff --git a/scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java b/scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java deleted file mode 100644 index 1f00328246..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.search; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.Subject; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.cache.Cache; -import sonia.scm.security.ScmSecurityException; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response.Status; -import sonia.scm.security.Role; - -/** - * - * @author Sebastian Sdorra - * - * @param - */ -public class SearchHandler -{ - - /** the logger for SearchHandler */ - private static final Logger logger = - LoggerFactory.getLogger(SearchHandler.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param securityContextProvider - * @param cache - * @param searchable - */ - public SearchHandler(Cache cache, - Searchable searchable) - { - - this.cache = cache; - this.searchable = searchable; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - public void clearCache() - { - this.cache.clear(); - } - - /** - * Method description - * - * - * @param queryString - * @param function - * - * @return - */ - public SearchResults search(String queryString, - Function function) - { - Subject subject = SecurityUtils.getSubject(); - - if (!subject.hasRole(Role.USER)) - { - throw new ScmSecurityException("Authentication is required"); - } - - if (Util.isEmpty(queryString)) - { - throw new WebApplicationException(Status.BAD_REQUEST); - } - - SearchResults result = cache.get(queryString); - - if (result == null) - { - SearchRequest request = new SearchRequest(queryString, ignoreCase); - - request.setMaxResults(maxResults); - - Collection users = searchable.search(request); - - result = new SearchResults(); - - if (Util.isNotEmpty(users)) - { - Collection resultCollection = - Collections2.transform(users, function); - - result.setSuccess(true); - - // create a copy of the result collection to reduce memory - // use ArrayList instead of ImmutableList for copy, - // because the list must be mutable for decorators - result.setResults(Lists.newArrayList(resultCollection)); - cache.put(queryString, result); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug("return searchresults for {} from cache", queryString); - } - - return result; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public int getMaxResults() - { - return maxResults; - } - - /** - * Method description - * - * - * @return - */ - public boolean isIgnoreCase() - { - return ignoreCase; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param ignoreCase - */ - public void setIgnoreCase(boolean ignoreCase) - { - this.ignoreCase = ignoreCase; - } - - /** - * Method description - * - * - * @param maxResults - */ - public void setMaxResults(int maxResults) - { - this.maxResults = maxResults; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected Cache cache; - - /** Field description */ - protected Searchable searchable; - - /** Field description */ - private int maxResults = 5; - - /** Field description */ - private boolean ignoreCase = true; -} diff --git a/scm-webapp/src/main/java/sonia/scm/search/SearchResult.java b/scm-webapp/src/main/java/sonia/scm/search/SearchResult.java deleted file mode 100644 index 3862fafe8b..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/search/SearchResult.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.search; - -/** - * - * @author Sebastian Sdorra - */ -public class SearchResult -{ - - /** - * Constructs ... - * - */ - public SearchResult() {} - - /** - * Constructs ... - * - * - * @param value - * @param label - */ - public SearchResult(String value, String label) - { - this.value = value; - this.label = label; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getLabel() - { - return label; - } - - /** - * Method description - * - * - * @return - */ - public String getValue() - { - return value; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param label - */ - public void setLabel(String label) - { - this.label = label; - } - - /** - * Method description - * - * - * @param value - */ - public void setValue(String value) - { - this.value = value; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String label; - - /** Field description */ - private String value; -} diff --git a/scm-webapp/src/main/java/sonia/scm/search/SearchResults.java b/scm-webapp/src/main/java/sonia/scm/search/SearchResults.java deleted file mode 100644 index a26982052a..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/search/SearchResults.java +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.search; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; - -import javax.xml.bind.annotation.XmlRootElement; -import sonia.scm.api.rest.RestActionResult; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "search-results") -public class SearchResults extends RestActionResult -{ - - /** - * Constructs ... - * - */ - public SearchResults() {} - - /** - * Constructs ... - * - * - * @param success - */ - public SearchResults(boolean success) - { - super(success); - } - - /** - * Constructs ... - * - * - * @param results - */ - public SearchResults(Collection results) - { - super(true); - this.results = results; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Collection getResults() - { - return results; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param results - */ - public void setResults(Collection results) - { - this.results = results; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Collection results; -} From c6436da4551055bb3f0b91a6bd57acf063cac7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Apr 2019 12:14:32 +0200 Subject: [PATCH 02/38] Remove old AbstractManagerResource --- .../resources/AbstractManagerResource.java | 581 ------------------ .../CollectionResourceManagerAdapter.java | 47 +- .../SingleResourceManagerAdapter.java | 93 ++- .../AbstractManagerResourceTest.java | 164 ----- .../sonia/scm/api/rest/resources/Simple.java | 52 ++ .../CollectionResourceManagerAdapterTest.java | 62 ++ 6 files changed, 221 insertions(+), 778 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java 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 deleted file mode 100644 index dfc0bd2a5d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ /dev/null @@ -1,581 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.net.UrlEscapers; -import org.apache.shiro.authz.AuthorizationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.LastModifiedAware; -import sonia.scm.Manager; -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; -import javax.ws.rs.core.EntityTag; -import javax.ws.rs.core.GenericEntity; -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.net.URI; -import java.util.Collection; -import java.util.Comparator; -import java.util.Date; - -//~--- JDK imports ------------------------------------------------------------ - -public abstract class AbstractManagerResource { - - /** the logger for AbstractManagerResource */ - private static final Logger logger = - LoggerFactory.getLogger(AbstractManagerResource.class); - - protected final Manager manager; - private final Class type; - - protected int cacheMaxAge = 0; - protected boolean disableCache = false; - - public AbstractManagerResource(Manager manager, Class type) { - this.manager = manager; - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param items - * - * @return - */ - protected abstract GenericEntity> createGenericEntity( - Collection items); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param item - * - * @return - */ - protected abstract String getId(T item); - - /** - * Method description - * - * - * @return - */ - protected abstract String getPathPart(); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param uriInfo - * @param item - * - * @return - */ - public Response create(UriInfo uriInfo, T item) - { - preCreate(item); - - Response response; - - try - { - manager.create(item); - - String id = getId(item); - response = Response.created(location(uriInfo, id)).build(); - } - catch (AuthorizationException ex) - { - logger.warn("create is not allowd", ex); - response = Response.status(Status.FORBIDDEN).build(); - } - catch (Exception ex) - { - logger.error("error during create", ex); - response = createErrorResponse(ex); - } - - return response; - } - - @VisibleForTesting - URI location(UriInfo uriInfo, String id) { - String escaped = UrlEscapers.urlPathSegmentEscaper().escape(id); - return uriInfo.getAbsolutePath().resolve(getPathPart().concat("/").concat(escaped)); - } - - /** - * Method description - * - * - * @param name - * - * @return - */ - public Response delete(String name) - { - Response response = null; - T item = manager.get(name); - - if (item != null) - { - preDelete(item); - - try - { - manager.delete(item); - response = Response.noContent().build(); - } - catch (AuthorizationException ex) - { - logger.warn("delete not allowd", ex); - response = Response.status(Response.Status.FORBIDDEN).build(); - } - catch (Exception ex) - { - logger.error("error during delete", ex); - response = createErrorResponse(ex); - } - } - - return response; - } - - /** - * Method description - * - * - * - * - * @param name - * @param item - * - * - * @return - */ - public Response update(String name, T item) - { - Response response = null; - - preUpdate(item); - - try - { - manager.modify(item); - response = Response.noContent().build(); - } - catch (AuthorizationException ex) - { - logger.warn("update not allowed", ex); - response = Response.status(Response.Status.FORBIDDEN).build(); - } - catch (Exception ex) - { - logger.error("error during update", ex); - response = createErrorResponse(ex); - } - - return response; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * - * @param request - * @param id - * - * @return - */ - public Response get(Request request, String id) - { - Response response; - T item = manager.get(id); - - if (item != null) - { - prepareForReturn(item); - - if (disableCache) - { - response = Response.ok(item).build(); - } - else - { - response = createCacheResponse(request, item, item); - } - } - else - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - - return response; - } - - /** - * Method description - * - * - * - * @param request - * @param start - * @param limit - * @param sortby - * @param desc - * @return - */ - public Response getAll(Request request, int start, int limit, String sortby, - boolean desc) - { - Collection items = fetchItems(sortby, desc, start, limit); - - if (Util.isNotEmpty(items)) - { - items = prepareForReturn(items); - } - - Response response = null; - Object entity = createGenericEntity(items); - - if (disableCache) - { - response = Response.ok(entity).build(); - } - else - { - response = createCacheResponse(request, manager, items, entity); - } - - return response; - } - - /** - * Method description - * - * - * @return - */ - public int getCacheMaxAge() - { - return cacheMaxAge; - } - - /** - * Method description - * - * - * @return - */ - public boolean isDisableCache() - { - return disableCache; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param cacheMaxAge - */ - public void setCacheMaxAge(int cacheMaxAge) - { - this.cacheMaxAge = cacheMaxAge; - } - - /** - * Method description - * - * - * @param disableCache - */ - public void setDisableCache(boolean disableCache) - { - this.disableCache = disableCache; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param throwable - * - * @return - */ - protected Response createErrorResponse(Throwable throwable) - { - return createErrorResponse(Status.INTERNAL_SERVER_ERROR, - throwable.getMessage(), throwable); - } - - /** - * Method description - * - * - * @param status - * @param message - * @param throwable - * - * @return - */ - protected Response createErrorResponse(Status status, String message, - Throwable throwable) - { - return Response.status(status).entity(new RestExceptionResult(message, - throwable)).build(); - } - - /** - * Method description - * - * - * @param item - */ - protected void preCreate(T item) {} - - /** - * Method description - * - * - * @param item - */ - protected void preDelete(T item) {} - - /** - * Method description - * - * - * @param item - */ - protected void preUpdate(T item) {} - - /** - * Method description - * - * - * @param item - * - * @return - */ - protected T prepareForReturn(T item) - { - return item; - } - - /** - * Method description - * - * - * @param items - * - * @return - */ - protected Collection prepareForReturn(Collection items) - { - return items; - } - - /** - * Method description - * - * - * @param rb - */ - private void addCacheControl(Response.ResponseBuilder rb) - { - CacheControl cc = new CacheControl(); - - cc.setMaxAge(cacheMaxAge); - rb.cacheControl(cc); - } - - /** - * Method description - * - * - * @param request - * @param timeItem - * @param item - * @param - * - * @return - */ - private Response createCacheResponse(Request request, - LastModifiedAware timeItem, I item) - { - return createCacheResponse(request, timeItem, item, item); - } - - /** - * Method description - * - * - * @param request - * @param timeItem - * @param entityItem - * @param item - * @param - * - * @return - */ - private Response createCacheResponse(Request request, - LastModifiedAware timeItem, Object entityItem, I item) - { - Response.ResponseBuilder builder = null; - Date lastModified = getLastModified(timeItem); - EntityTag e = new EntityTag(Integer.toString(entityItem.hashCode())); - - if (lastModified != null) - { - builder = request.evaluatePreconditions(lastModified, e); - } - else - { - builder = request.evaluatePreconditions(e); - } - - if (builder == null) - { - builder = Response.ok(item).tag(e).lastModified(lastModified); - } - - addCacheControl(builder); - - return builder.build(); - } - - private Comparator createComparator(String sortBy, boolean desc) { - Comparator comparator = Comparables.comparator(type, sortBy); - if (desc) { - comparator = comparator.reversed(); - } - return comparator; - } - - private Collection fetchItems(String sortBy, boolean desc, int start, - int limit) - { - AssertUtil.assertPositive(start); - - Collection items = null; - - if (limit > 0) - { - if (Util.isEmpty(sortBy)) - { - - // replace with something useful - sortBy = "id"; - } - - items = manager.getAll(createComparator(sortBy, desc), start, limit); - } - else if (Util.isNotEmpty(sortBy)) - { - items = manager.getAll(createComparator(sortBy, desc)); - } - else - { - items = manager.getAll(); - } - - return items; - } - - protected PageResult fetchPage(String sortBy, boolean desc, int pageNumber, - int pageSize) { - AssertUtil.assertPositive(pageNumber); - AssertUtil.assertPositive(pageSize); - - if (Util.isEmpty(sortBy)) { - // replace with something useful - sortBy = "id"; - } - - return manager.getPage(createComparator(sortBy, desc), pageNumber, pageSize); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param item - * - * @return - */ - private Date getLastModified(LastModifiedAware item) - { - Date lastModified = null; - Long l = item.getLastModified(); - - if (l != null) - { - lastModified = new Date(l); - } - - return lastModified; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index 052bf771b1..c9eb6a9d26 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -4,12 +4,13 @@ import de.otto.edison.hal.HalRepresentation; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.PageResult; -import sonia.scm.api.rest.resources.AbstractManagerResource; +import sonia.scm.util.AssertUtil; +import sonia.scm.util.Comparables; +import sonia.scm.util.Util; -import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.net.URI; -import java.util.Collection; +import java.util.Comparator; import java.util.function.Function; import java.util.function.Supplier; @@ -27,10 +28,14 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class CollectionResourceManagerAdapter extends AbstractManagerResource { + DTO extends HalRepresentation>{ + + protected final Manager manager; + protected final Class type; CollectionResourceManagerAdapter(Manager manager, Class type) { - super(manager, type); + this.manager = manager; + this.type = type; } /** @@ -42,6 +47,27 @@ class CollectionResourceManagerAdapter fetchPage(String sortBy, boolean desc, int pageNumber, + int pageSize) { + AssertUtil.assertPositive(pageNumber); + AssertUtil.assertPositive(pageSize); + + if (Util.isEmpty(sortBy)) { + // replace with something useful + sortBy = "id"; + } + + return manager.getPage(createComparator(sortBy, desc), pageNumber, pageSize); + } + + private Comparator createComparator(String sortBy, boolean desc) { + Comparator comparator = Comparables.comparator(type, sortBy); + if (desc) { + comparator = comparator.reversed(); + } + return comparator; + } + /** * Creates a model object for the given dto and returns a corresponding http response. * This handles all corner cases, eg. no conflicts or missing privileges. @@ -55,18 +81,7 @@ class CollectionResourceManagerAdapter> createGenericEntity(Collection modelObjects) { - throw new UnsupportedOperationException(); - } - - @Override protected String getId(MODEL_OBJECT item) { return item.getId(); } - - @Override - protected String getPathPart() { - throw new UnsupportedOperationException(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 72507562dd..082064902a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -1,15 +1,16 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; +import org.apache.shiro.authz.AuthorizationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.ConcurrentModificationException; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.NotFoundException; -import sonia.scm.api.rest.resources.AbstractManagerResource; +import sonia.scm.api.rest.RestExceptionResult; -import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; -import java.util.Collection; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -29,10 +30,13 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class SingleResourceManagerAdapter extends AbstractManagerResource { + DTO extends HalRepresentation> { + + private static final Logger LOG = LoggerFactory.getLogger(SingleResourceManagerAdapter.class); private final Function> errorHandler; - private final Class type; + protected final Manager manager; + protected final Class type; SingleResourceManagerAdapter(Manager manager, Class type) { this(manager, type, e -> Optional.empty()); @@ -42,7 +46,7 @@ class SingleResourceManagerAdapter manager, Class type, Function> errorHandler) { - super(manager, type); + this.manager = manager; this.errorHandler = errorHandler; this.type = type; } @@ -75,6 +79,33 @@ class SingleResourceManagerAdapter updated.getLastModified()); @@ -89,23 +120,51 @@ class SingleResourceManagerAdapter> createGenericEntity(Collection modelObjects) { - throw new UnsupportedOperationException(); + protected Response createErrorResponse(Response.Status status, String message, + Throwable throwable) + { + return Response.status(status).entity(new RestExceptionResult(message, + throwable)).build(); } - @Override protected String getId(MODEL_OBJECT item) { return item.getId(); } - - @Override - protected String getPathPart() { - throw new UnsupportedOperationException(); - } } 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 deleted file mode 100644 index fd9745be83..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package sonia.scm.api.rest.resources; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.Manager; -import sonia.scm.ModelObject; - -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; -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.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class AbstractManagerResourceTest { - - @Mock - private Manager manager; - - @Mock - private Request request; - - @Mock - private UriInfo uriInfo; - - @Captor - private ArgumentCaptor> comparatorCaptor; - - private AbstractManagerResource abstractManagerResource; - - @Before - public void captureComparator() { - when(manager.getAll(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(emptyList()); - abstractManagerResource = new SimpleManagerResource(); - } - - @Test - public void shouldAcceptDefaultSortByParameter() { - abstractManagerResource.getAll(request, 0, 1, null, true); - - Comparator comparator = comparatorCaptor.getValue(); - assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); - } - - @Test - public void shouldAcceptValidSortByParameter() { - abstractManagerResource.getAll(request, 0, 1, "data", true); - - Comparator comparator = comparatorCaptor.getValue(); - assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); - } - - @Test(expected = IllegalArgumentException.class) - public void shouldFailForIllegalSortByParameter() { - abstractManagerResource.getAll(request, 0, 1, "x", true); - } - - @Test - public void testLocation() throws URISyntaxException { - URI uri = location("special-item"); - assertEquals(new URI("https://scm.scm-manager.org/simple/special-item"), uri); - } - - @Test - public void testLocationWithSpaces() throws URISyntaxException { - URI uri = location("Scm Special Group"); - assertEquals(new URI("https://scm.scm-manager.org/simple/Scm%20Special%20Group"), uri); - } - - private URI location(String id) throws URISyntaxException { - URI base = new URI("https://scm.scm-manager.org/"); - when(uriInfo.getAbsolutePath()).thenReturn(base); - - return abstractManagerResource.location(uriInfo, id); - } - - private class SimpleManagerResource extends AbstractManagerResource { - - { - disableCache = true; - } - - private SimpleManagerResource() { - super(AbstractManagerResourceTest.this.manager, Simple.class); - } - - @Override - protected GenericEntity> createGenericEntity(Collection items) { - return null; - } - - @Override - protected String getId(Simple item) { - return null; - } - - @Override - protected String getPathPart() { - return "simple"; - } - } - - public static class Simple implements ModelObject { - - private String id; - private String data; - - Simple(String id, String data) { - this.id = id; - this.data = data; - } - - public String getData() { - return data; - } - - @Override - public String getId() { - return id; - } - - @Override - public void setLastModified(Long timestamp) { - - } - - @Override - public Long getCreationDate() { - return null; - } - - @Override - public void setCreationDate(Long timestamp) { - - } - - @Override - public Long getLastModified() { - return null; - } - - @Override - public String getType() { - return null; - } - @Override - public boolean isValid() { - return false; - } - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java new file mode 100644 index 0000000000..3887bc4a9c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java @@ -0,0 +1,52 @@ +package sonia.scm.api.rest.resources; + +import sonia.scm.ModelObject; + +public class Simple implements ModelObject { + + private String id; + private String data; + + public Simple(String id, String data) { + this.id = id; + this.data = data; + } + + public String getData() { + return data; + } + + @Override + public String getId() { + return id; + } + + @Override + public void setLastModified(Long timestamp) { + + } + + @Override + public Long getCreationDate() { + return null; + } + + @Override + public void setCreationDate(Long timestamp) { + + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public String getType() { + return null; + } + @Override + public boolean isValid() { + return false; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java new file mode 100644 index 0000000000..671f0e576c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java @@ -0,0 +1,62 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.Manager; +import sonia.scm.api.rest.resources.Simple; + +import java.util.Comparator; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CollectionResourceManagerAdapterTest { + + @Mock + private Manager manager; + @Captor + private ArgumentCaptor> comparatorCaptor; + + private CollectionResourceManagerAdapter abstractManagerResource; + + @Before + public void captureComparator() { + when(manager.getPage(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(null); + abstractManagerResource = new SimpleManagerResource(); + } + + @Test + public void shouldAcceptDefaultSortByParameter() { + abstractManagerResource.getAll(0, 1, null, true, r -> null); + + Comparator comparator = comparatorCaptor.getValue(); + assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); + } + + @Test + public void shouldAcceptValidSortByParameter() { + abstractManagerResource.getAll(0, 1, "data", true, r -> null); + + Comparator comparator = comparatorCaptor.getValue(); + assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldFailForIllegalSortByParameter() { + abstractManagerResource.getAll(0, 1, "x", true, r -> null); + } + + private class SimpleManagerResource extends CollectionResourceManagerAdapter { + private SimpleManagerResource() { + super(CollectionResourceManagerAdapterTest.this.manager, Simple.class); + } + } +} From f5933cb6d5d1d97eee76d1af8c3a98d1462b8191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Apr 2019 12:31:43 +0200 Subject: [PATCH 03/38] Clean up --- .../SingleResourceManagerAdapter.java | 80 ++++--------------- 1 file changed, 16 insertions(+), 64 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 082064902a..9c6c0300d6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -1,14 +1,10 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; -import org.apache.shiro.authz.AuthorizationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sonia.scm.ConcurrentModificationException; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.NotFoundException; -import sonia.scm.api.rest.RestExceptionResult; import javax.ws.rs.core.Response; import java.util.Optional; @@ -32,8 +28,6 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; class SingleResourceManagerAdapter { - private static final Logger LOG = LoggerFactory.getLogger(SingleResourceManagerAdapter.class); - private final Function> errorHandler; protected final Manager manager; protected final Class type; @@ -76,36 +70,18 @@ class SingleResourceManagerAdapter updated.getLastModified()); @@ -120,48 +96,24 @@ class SingleResourceManagerAdapter throwable); } protected String getId(MODEL_OBJECT item) { From fbd6f4f3c7deeb9f987ab089744da2ec167c6110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Apr 2019 13:09:16 +0200 Subject: [PATCH 04/38] Introduce filter parameter in Manager --- scm-core/src/main/java/sonia/scm/Manager.java | 9 ++++++--- .../main/java/sonia/scm/ManagerDecorator.java | 5 +++-- .../src/test/java/sonia/scm/ManagerTest.java | 18 ++++++++++-------- .../CollectionResourceManagerAdapter.java | 9 +++++---- .../v2/resources/IdResourceManagerAdapter.java | 4 ++-- .../sonia/scm/group/DefaultGroupManager.java | 7 ++++--- .../repository/DefaultRepositoryManager.java | 6 ++++-- .../sonia/scm/user/DefaultUserManager.java | 7 ++++--- 8 files changed, 38 insertions(+), 27 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 9a7f21d3ef..6e0b72295c 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -35,6 +35,7 @@ package sonia.scm; import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; /** * Base interface for all manager classes. @@ -82,11 +83,12 @@ public interface Manager * Returns all object of the store sorted by the given {@link java.util.Comparator} * * + * @param filter to filter the returned objects * @param comparator to sort the returned objects * @since 1.4 * @return all object of the store sorted by the given {@link java.util.Comparator} */ - Collection getAll(Comparator comparator); + Collection getAll(Predicate filter, Comparator comparator); /** * Returns objects from the store which are starts at the given start @@ -125,6 +127,7 @@ public interface Manager *

This default implementation reads all items, first, so you might want to adapt this * whenever reading is expensive!

* + * @param filter to filter returned objects * @param comparator to sort the returned objects * @param pageNumber the number of the page to be returned (zero based) * @param pageSize the size of the pages @@ -134,8 +137,8 @@ public interface Manager * page. If the requested page number exceeds the existing pages, an * empty page result is returned. */ - default PageResult getPage(Comparator comparator, int pageNumber, int pageSize) { - return PageResult.createPage(getAll(comparator), pageNumber, pageSize); + default PageResult getPage(Predicate filter, Comparator comparator, int pageNumber, int pageSize) { + return PageResult.createPage(getAll(filter, comparator), pageNumber, pageSize); } } diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index f6e91aeced..0c46366a56 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -37,6 +37,7 @@ package sonia.scm; import java.io.IOException; import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; /** * Basic decorator for manager classes. @@ -104,9 +105,9 @@ public class ManagerDecorator implements Manager { } @Override - public Collection getAll(Comparator comparator) + public Collection getAll(Predicate filter, Comparator comparator) { - return decorated.getAll(comparator); + return decorated.getAll(filter, comparator); } @Override diff --git a/scm-core/src/test/java/sonia/scm/ManagerTest.java b/scm-core/src/test/java/sonia/scm/ManagerTest.java index 06c8eb3ea6..b26fdb3217 100644 --- a/scm-core/src/test/java/sonia/scm/ManagerTest.java +++ b/scm-core/src/test/java/sonia/scm/ManagerTest.java @@ -5,6 +5,7 @@ import org.mockito.Mock; import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; import java.util.stream.IntStream; import static java.util.stream.Collectors.toList; @@ -18,21 +19,22 @@ public class ManagerTest { @Mock private Comparator comparator; + private Predicate predicate = x -> true; @Test(expected = IllegalArgumentException.class) public void validatesPageNumber() { - manager.getPage(comparator, -1, 5); + manager.getPage(predicate, comparator, -1, 5); } @Test(expected = IllegalArgumentException.class) public void validatesPageSize() { - manager.getPage(comparator, 2, 0); + manager.getPage(predicate, comparator, 2, 0); } @Test public void getsNoPage() { givenItemCount = 0; - PageResult singlePage = manager.getPage(comparator, 0, 5); + PageResult singlePage = manager.getPage(predicate, comparator, 0, 5); assertEquals(0, singlePage.getEntities().size()); assertEquals(givenItemCount, singlePage.getOverallCount()); } @@ -40,7 +42,7 @@ public class ManagerTest { @Test public void getsSinglePageWithoutEnoughItems() { givenItemCount = 3; - PageResult singlePage = manager.getPage(comparator, 0, 4); + PageResult singlePage = manager.getPage(predicate, comparator, 0, 4); assertEquals(3, singlePage.getEntities().size() ); assertEquals(givenItemCount, singlePage.getOverallCount()); } @@ -48,7 +50,7 @@ public class ManagerTest { @Test public void getsSinglePageWithExactCountOfItems() { givenItemCount = 3; - PageResult singlePage = manager.getPage(comparator, 0, 3); + PageResult singlePage = manager.getPage(predicate, comparator, 0, 3); assertEquals(3, singlePage.getEntities().size() ); assertEquals(givenItemCount, singlePage.getOverallCount()); } @@ -56,11 +58,11 @@ public class ManagerTest { @Test public void getsTwoPages() { givenItemCount = 3; - PageResult page1 = manager.getPage(comparator, 0, 2); + PageResult page1 = manager.getPage(predicate, comparator, 0, 2); assertEquals(2, page1.getEntities().size()); assertEquals(givenItemCount, page1.getOverallCount()); - PageResult page2 = manager.getPage(comparator, 1, 2); + PageResult page2 = manager.getPage(predicate, comparator, 1, 2); assertEquals(1, page2.getEntities().size()); assertEquals(givenItemCount, page2.getOverallCount()); } @@ -79,7 +81,7 @@ public class ManagerTest { } @Override - public Collection getAll(Comparator comparator) { return getAll(); } + public Collection getAll(Predicate filter, Comparator comparator) { return getAll(); } @Override public Collection getAll(int start, int limit) { return null; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index c9eb6a9d26..1788ca2643 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -12,6 +12,7 @@ import javax.ws.rs.core.Response; import java.net.URI; import java.util.Comparator; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; @@ -42,12 +43,12 @@ class CollectionResourceManagerAdapter, CollectionDto> mapToDto) { - PageResult pageResult = fetchPage(sortBy, desc, page, pageSize); + public Response getAll(int page, int pageSize, Predicate filter, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { + PageResult pageResult = fetchPage(filter, sortBy, desc, page, pageSize); return Response.ok(mapToDto.apply(pageResult)).build(); } - private PageResult fetchPage(String sortBy, boolean desc, int pageNumber, + private PageResult fetchPage(Predicate filter, String sortBy, boolean desc, int pageNumber, int pageSize) { AssertUtil.assertPositive(pageNumber); AssertUtil.assertPositive(pageSize); @@ -57,7 +58,7 @@ class CollectionResourceManagerAdapter createComparator(String sortBy, boolean desc) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index 2b2a4cf0ad..79a0e038d9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -45,8 +45,8 @@ class IdResourceManagerAdapter, CollectionDto> mapToDto) { - return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto); + public Response getAll(int page, int pageSize, Predicate filter, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { + return collectionAdapter.getAll(page, pageSize, filter, sortBy, desc, mapToDto); } public Response create(DTO dto, Supplier modelObjectSupplier, Function uriCreator) { diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index 3320773e57..4fd89d657e 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -59,6 +59,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; +import java.util.function.Predicate; //~--- JDK imports ------------------------------------------------------------ @@ -250,7 +251,7 @@ public class DefaultGroupManager extends AbstractGroupManager @Override public Collection getAll() { - return getAll(null); + return getAll(group -> true, null); } /** @@ -262,14 +263,14 @@ public class DefaultGroupManager extends AbstractGroupManager * @return */ @Override - public Collection getAll(Comparator comparator) + public Collection getAll(Predicate filter, Comparator comparator) { List groups = new ArrayList<>(); PermissionActionCheck check = GroupPermissions.read(); for (Group group : groupDAO.getAll()) { - if (check.isPermitted(group)) { + if (filter.test(group) && check.isPermitted(group)) { groups.add(group.clone()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 12a7f7cc51..1bcd877620 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -64,6 +64,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +import java.util.function.Predicate; import static sonia.scm.AlreadyExistsException.alreadyExists; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -253,13 +254,14 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } @Override - public Collection getAll(Comparator comparator) { + public Collection getAll(Predicate filter, Comparator comparator) { List repositories = Lists.newArrayList(); PermissionActionCheck check = RepositoryPermissions.read(); for (Repository repository : repositoryDAO.getAll()) { if (handlerMap.containsKey(repository.getType()) + && filter.test(repository) && check.isPermitted(repository)) { Repository r = repository.clone(); @@ -276,7 +278,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { @Override public Collection getAll() { - return getAll(null); + return getAll(repository -> true, null); } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index a6c40b24f4..b44db8d62a 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -62,6 +62,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.function.Predicate; /** * @@ -280,7 +281,7 @@ public class DefaultUserManager extends AbstractUserManager @Override public Collection getAll() { - return getAll(null); + return getAll(user -> true, null); } /** @@ -292,13 +293,13 @@ public class DefaultUserManager extends AbstractUserManager * @return */ @Override - public Collection getAll(Comparator comparator) + public Collection getAll(Predicate filter, Comparator comparator) { List users = new ArrayList<>(); PermissionActionCheck check = UserPermissions.read(); for (User user : userDAO.getAll()) { - if (check.isPermitted(user)) { + if (filter.test(user) && check.isPermitted(user)) { users.add(user.clone()); } } From 4797967b0dd821c15275117057be6c524dfcaa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Apr 2019 16:27:27 +0200 Subject: [PATCH 05/38] Implement simple search queries for users, groups and repositories --- .../java/sonia/scm/search/SearchUtil.java | 22 +++++------- .../v2/resources/GroupCollectionResource.java | 19 +++++++++-- .../RepositoryCollectionResource.java | 19 +++++++++-- .../v2/resources/UserCollectionResource.java | 19 +++++++++-- .../CollectionResourceManagerAdapterTest.java | 11 +++--- .../v2/resources/GroupRootResourceTest.java | 28 +++++++++++++-- .../resources/RepositoryRootResourceTest.java | 23 ++++++++++++- .../v2/resources/UserRootResourceTest.java | 34 +++++++++++++++++-- 8 files changed, 144 insertions(+), 31 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java index 281e00a517..3d86b113ff 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java @@ -92,16 +92,13 @@ public final class SearchUtil { result = true; - if (Util.isNotEmpty(other)) + for (String o : other) { - for (String o : other) + if ((o == null) ||!o.matches(query)) { - if ((o == null) ||!o.matches(query)) - { - result = false; + result = false; - break; - } + break; } } } @@ -127,16 +124,13 @@ public final class SearchUtil if (!value.matches(query)) { - if (Util.isNotEmpty(other)) + for (String o : other) { - for (String o : other) + if ((o != null) && o.matches(query)) { - if ((o != null) && o.matches(query)) - { - result = true; + result = true; - break; - } + break; } } } 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 6c13dc33a5..b49ad57297 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 @@ -7,6 +7,8 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -19,6 +21,9 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; public class GroupCollectionResource { @@ -63,8 +68,10 @@ public class GroupCollectionResource { @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, @DefaultValue("false") - @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, + @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult)); } @@ -90,4 +97,12 @@ public class GroupCollectionResource { () -> dtoToGroupMapper.map(group), g -> resourceLinks.group().self(g.getName())); } + + private Predicate createSearchPredicate(String search) { + if (isNullOrEmpty(search)) { + return group -> true; + } + SearchRequest searchRequest = new SearchRequest(search, true); + return group -> SearchUtil.matchesOne(searchRequest, group.getName(), group.getDescription()); + } } 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 e1e1260a4d..b4d148b4bf 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 @@ -9,6 +9,8 @@ import org.apache.shiro.SecurityUtils; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermission; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.user.User; import sonia.scm.web.VndMediaType; @@ -23,6 +25,9 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Collections.singletonList; public class RepositoryCollectionResource { @@ -65,8 +70,10 @@ public class RepositoryCollectionResource { public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); } @@ -106,4 +113,12 @@ public class RepositoryCollectionResource { private String currentUser() { return SecurityUtils.getSubject().getPrincipals().oneByType(User.class).getName(); } + + private Predicate createSearchPredicate(String search) { + if (isNullOrEmpty(search)) { + return user -> true; + } + SearchRequest searchRequest = new SearchRequest(search, true); + return repository -> SearchUtil.matchesOne(searchRequest, repository.getName(), repository.getNamespace(), repository.getDescription()); + } } 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 a7442a2262..3e35b8d4a6 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 @@ -6,6 +6,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -20,6 +22,9 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; public class UserCollectionResource { @@ -65,8 +70,10 @@ public class UserCollectionResource { public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); } @@ -93,4 +100,12 @@ public class UserCollectionResource { public Response create(@Valid UserDto user) { return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName())); } + + private Predicate createSearchPredicate(String search) { + if (isNullOrEmpty(search)) { + return user -> true; + } + SearchRequest searchRequest = new SearchRequest(search, true); + return user -> SearchUtil.matchesOne(searchRequest, user.getName(), user.getDisplayName(), user.getMail()); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java index 671f0e576c..1fb0dd76df 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java @@ -12,6 +12,7 @@ import sonia.scm.Manager; import sonia.scm.api.rest.resources.Simple; import java.util.Comparator; +import java.util.function.Predicate; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; @@ -24,18 +25,20 @@ public class CollectionResourceManagerAdapterTest { private Manager manager; @Captor private ArgumentCaptor> comparatorCaptor; + @Captor + private ArgumentCaptor> filterCaptor; private CollectionResourceManagerAdapter abstractManagerResource; @Before public void captureComparator() { - when(manager.getPage(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(null); + when(manager.getPage(filterCaptor.capture(), comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(null); abstractManagerResource = new SimpleManagerResource(); } @Test public void shouldAcceptDefaultSortByParameter() { - abstractManagerResource.getAll(0, 1, null, true, r -> null); + abstractManagerResource.getAll(0, 1, x -> true, null, true, r -> null); Comparator comparator = comparatorCaptor.getValue(); assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); @@ -43,7 +46,7 @@ public class CollectionResourceManagerAdapterTest { @Test public void shouldAcceptValidSortByParameter() { - abstractManagerResource.getAll(0, 1, "data", true, r -> null); + abstractManagerResource.getAll(0, 1, x -> true, "data", true, r -> null); Comparator comparator = comparatorCaptor.getValue(); assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); @@ -51,7 +54,7 @@ public class CollectionResourceManagerAdapterTest { @Test(expected = IllegalArgumentException.class) public void shouldFailForIllegalSortByParameter() { - abstractManagerResource.getAll(0, 1, "x", true, r -> null); + abstractManagerResource.getAll(0, 1, x -> true, "x", true, r -> null); } private class SimpleManagerResource extends CollectionResourceManagerAdapter { 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 3e2d0f9663..09236c0e19 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 @@ -11,6 +11,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; @@ -30,9 +31,11 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; import java.util.Collections; +import java.util.function.Predicate; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -67,8 +70,10 @@ public class GroupRootResourceTest { @InjectMocks private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; - - private ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); + @Captor + private ArgumentCaptor groupCaptor; + @Captor + private ArgumentCaptor> filterCaptor; @Before public void prepareEnvironment() { @@ -77,7 +82,7 @@ public class GroupRootResourceTest { doNothing().when(groupManager).modify(groupCaptor.capture()); Group group = createDummyGroup(); - when(groupManager.getPage(any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1)); + when(groupManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1)); when(groupManager.get("admin")).thenReturn(group); GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); @@ -317,6 +322,23 @@ public class GroupRootResourceTest { assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}")); } + @Test + public void shouldCreateFilterForSearch() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "?q=One"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + Group group = new Group("xml", "someone"); + assertTrue(filterCaptor.getValue().test(group)); + group.setName("nothing"); + group.setDescription("Someone"); + assertTrue(filterCaptor.getValue().test(group)); + group.setDescription("Nobody"); + assertFalse(filterCaptor.getValue().test(group)); + } + @Test public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); 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 d00d45bb24..55754469ec 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 @@ -13,6 +13,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; @@ -31,6 +32,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.function.Predicate; import static java.util.Collections.singletonList; import static java.util.stream.Stream.of; @@ -42,6 +44,7 @@ import static javax.servlet.http.HttpServletResponse.SC_OK; 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.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyObject; @@ -78,6 +81,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Mock private ScmPathInfo uriInfo; + @Captor + private ArgumentCaptor> filterCaptor; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -150,7 +155,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Test public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException { PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); - when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); + when(repositoryManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -161,6 +166,22 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldCreateFilterForSearch() throws URISyntaxException { + PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); + when(repositoryManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "all_repos", "x"))); + assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "x", "repository"))); + assertFalse(filterCaptor.getValue().test(new Repository("rep", "rep", "x", "x"))); + } + @Test public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); 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 4047dfadd2..06d9b89120 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 @@ -12,6 +12,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.ContextEntry; @@ -30,6 +31,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; +import java.util.function.Predicate; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; @@ -72,7 +74,11 @@ public class UserRootResourceTest { @InjectMocks private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; - private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + @Captor + private ArgumentCaptor userCaptor; + @Captor + private ArgumentCaptor> filterCaptor; + private User originalUser; @Before @@ -333,7 +339,7 @@ public class UserRootResourceTest { @Test public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException { PageResult singletonPageResult = createSingletonPageResult(1); - when(userManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); + when(userManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -349,7 +355,7 @@ public class UserRootResourceTest { @Test public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException { PageResult singletonPageResult = createSingletonPageResult(3); - when(userManager.getPage(any(), eq(1), eq(1))).thenReturn(singletonPageResult); + when(userManager.getPage(any(), any(), eq(1), eq(1))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1"); MockHttpResponse response = new MockHttpResponse(); @@ -364,6 +370,28 @@ public class UserRootResourceTest { assertTrue(response.getContentAsString().contains("\"last\":{\"href\":\"/v2/users/?page=2")); } + @Test + public void shouldCreateFilterForSearch() throws URISyntaxException { + PageResult singletonPageResult = createSingletonPageResult(1); + when(userManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); + MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?q=One"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + User user = new User("Someone I know"); + assertTrue(filterCaptor.getValue().test(user)); + user.setName("nobody"); + user.setDisplayName("Someone I know"); + assertTrue(filterCaptor.getValue().test(user)); + user.setDisplayName("nobody"); + user.setMail("me@someone.com"); + assertTrue(filterCaptor.getValue().test(user)); + user.setMail("me@nowhere.com"); + assertFalse(filterCaptor.getValue().test(user)); + } + @Test public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); From eb894059d5cdc2be02f24785ff33aab71e3c2cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Apr 2019 16:55:11 +0200 Subject: [PATCH 06/38] Re-add erroneously deleted file --- scm-webapp/src/main/doc/enunciate.xml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 scm-webapp/src/main/doc/enunciate.xml diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml new file mode 100644 index 0000000000..225d2e0a2c --- /dev/null +++ b/scm-webapp/src/main/doc/enunciate.xml @@ -0,0 +1,67 @@ + + + + + + + + SCM-Manager API + + + SCM-Manager API +

This page describes the RESTful Web Service API of SCM-Manager ${project.version}.

+ ]]> +
+ + + + + + + + + + + + + +
From 62e393cb62a2376d41dc53095d2ad72e1daecdb6 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 10 Apr 2019 14:35:30 +0200 Subject: [PATCH 07/38] ui search field done, started adding filter functionality --- .../packages/ui-components/src/layout/Page.js | 76 ++++++++++++++++--- scm-ui/src/users/containers/Users.js | 9 ++- scm-ui/src/users/modules/users.js | 5 +- scm-ui/styles/scm.scss | 29 ++++--- 4 files changed, 94 insertions(+), 25 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 655b1fe986..960d6b941f 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -9,6 +9,10 @@ import classNames from "classnames"; import PageActions from "./PageActions"; import ErrorBoundary from "../ErrorBoundary"; +type State = { + value: string +}; + type Props = { title?: string, subtitle?: string, @@ -16,19 +20,49 @@ type Props = { error?: Error, showContentOnError?: boolean, children: React.Node, + filter: string => void, // context props classes: Object }; const styles = { - spacing: { + actions: { + display: "flex", + justifyContent: "flex-end" + }, + inputField: { + float: "right", marginTop: "1.25rem", - textAlign: "right" + marginRight: "1.25rem" + }, + inputHeight: { + height: "2.5rem" + }, + button: { + float: "right", + marginTop: "1.25rem" } }; -class Page extends React.Component { +class Page extends React.Component { + constructor(props) { + super(props); + this.state = { value: "" }; + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleChange(event) { + this.setState({ value: event.target.value }); + } + + handleSubmit(event) { + this.props.filter(this.state.value); + event.preventDefault(); + } + render() { const { error } = this.props; return ( @@ -36,12 +70,11 @@ class Page extends React.Component {
{this.renderPageHeader()} - + {this.renderContent()}
- ); } @@ -53,11 +86,30 @@ class Page extends React.Component { React.Children.forEach(children, child => { if (child && child.type.name === PageActions.name) { pageActions = ( -
+
+
+
+ + + + +
+
{child} @@ -68,15 +120,15 @@ class Page extends React.Component { } }); let underline = pageActionsExists ? ( -
+
) : null; return ( <>
- - <Subtitle subtitle={subtitle}/> + <Title title={title} /> + <Subtitle subtitle={subtitle} /> </div> {pageActions} </div> @@ -92,7 +144,7 @@ class Page extends React.Component<Props> { return null; } if (loading) { - return <Loading/>; + return <Loading />; } let content = []; diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 3d88523479..e2eccc3ecf 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -39,7 +39,7 @@ type Props = { history: History, // dispatch functions - fetchUsersByPage: (link: string, page: number) => void, + fetchUsersByPage: (link: string, page: number, filter?: string) => void, fetchUsersByLink: (link: string) => void }; @@ -74,6 +74,9 @@ class Users extends React.Component<Props> { subtitle={t("users.subtitle")} loading={loading || !users} error={error} + filter={filter => { + this.props.fetchUsersByPage(this.props.usersLink, this.props.page, filter); + }} > <UserTable users={users} /> {this.renderPaginator()} @@ -152,8 +155,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchUsersByPage: (link: string, page: number) => { - dispatch(fetchUsersByPage(link, page)); + fetchUsersByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchUsersByPage(link, page, filter)); }, fetchUsersByLink: (link: string) => { dispatch(fetchUsersByLink(link)); diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index a93d082f58..c7d5412209 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -43,8 +43,11 @@ export function fetchUsers(link: string) { return fetchUsersByLink(link); } -export function fetchUsersByPage(link: string, page: number) { +export function fetchUsersByPage(link: string, page: number, filter?: string) { // backend start counting by 0 + if(filter) { + return fetchUsersByLink(link + "?page=" + (page - 1) + "&q=" + decodeURIComponent(filter)); + } return fetchUsersByLink(link + "?page=" + (page - 1)); } diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index e02a4b28e0..aafe6e0836 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -49,13 +49,22 @@ hr.header-with-actions { display: none; } } -.is-mobile-create-button-spacing { +.is-mobile-action-spacing { @media screen and (max-width: 768px) { - border: 2px solid #e9f7fd; - padding: 1em 1em; - margin-top: 0 !important; - width: 100%; - text-align: center !important; + display: flow-root !important; + + .input-field { + padding: 0; + margin: 0 0 1.25rem 0 !important; + width: 100%; + } + .input-button { + border: 2px solid #e9f7fd; + padding: 1em 1em; + margin-top: 0 !important; + width: 100%; + text-align: center !important; + } } } @@ -102,10 +111,12 @@ $fa-font-path: "webfonts"; &.is-primary { background-color: #00d1df; } - &.is-primary:hover, &.is-primary.is-hovered { + &.is-primary:hover, + &.is-primary.is-hovered { background-color: #00b9c6; } - &.is-primary:active, &.is-primary.is-active { + &.is-primary:active, + &.is-primary.is-active { background-color: #00a1ac; } &.is-primary[disabled] { @@ -144,7 +155,7 @@ $fa-font-path: "webfonts"; margin-right: 0; } - .overlay-half-column{ + .overlay-half-column { position: absolute; height: calc(120px - 0.5rem); width: calc(100% - 1.5rem); From 60af0cca304ce351f0e5e2363f41c16c2d9eaad9 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 10 Apr 2019 14:45:35 +0200 Subject: [PATCH 08/38] added filter input translation --- .../packages/ui-components/src/layout/Page.js | 30 +++++++++++-------- scm-ui/public/locales/de/commons.json | 1 + scm-ui/public/locales/en/commons.json | 1 + 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 960d6b941f..0055498864 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -1,11 +1,13 @@ //@flow import * as React from "react"; +import { compose } from "redux"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; import Loading from "./../Loading"; import ErrorNotification from "./../ErrorNotification"; import Title from "./Title"; import Subtitle from "./Subtitle"; -import injectSheet from "react-jss"; -import classNames from "classnames"; import PageActions from "./PageActions"; import ErrorBoundary from "../ErrorBoundary"; @@ -23,7 +25,8 @@ type Props = { filter: string => void, // context props - classes: Object + classes: Object, + t: string => string }; const styles = { @@ -79,7 +82,7 @@ class Page extends React.Component<Props, State> { } renderPageHeader() { - const { title, subtitle, children, classes } = this.props; + const { title, subtitle, children, classes, t } = this.props; let pageActions = null; let pageActionsExists = false; @@ -87,7 +90,10 @@ class Page extends React.Component<Props, State> { if (child && child.type.name === PageActions.name) { pageActions = ( <div - className={classNames(classes.actions, "column is-three-fifths is-mobile-action-spacing")} + className={classNames( + classes.actions, + "column is-three-fifths is-mobile-action-spacing" + )} > <form className={classNames(classes.inputField, "input-field")}> <div @@ -97,7 +103,7 @@ class Page extends React.Component<Props, State> { <input className={classNames(classes.inputHeight, "input")} type="search" - placeholder="filter text" + placeholder={t("filterEntries")} value={this.state.value} onChange={this.handleChange} /> @@ -106,12 +112,7 @@ class Page extends React.Component<Props, State> { </span> </div> </form> - <div - className={classNames( - classes.button, - "input-button control" - )} - > + <div className={classNames(classes.button, "input-button control")}> {child} </div> </div> @@ -157,4 +158,7 @@ class Page extends React.Component<Props, State> { } } -export default injectSheet(styles)(Page); +export default compose( + injectSheet(styles), + translate("commons") +)(Page); diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 51a493fc40..b9cc93b1ed 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -39,6 +39,7 @@ "groups": "Gruppen", "config": "Einstellungen" }, + "filterEntries": "Einträge filtern", "paginator": { "next": "Weiter", "previous": "Zurück" diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index cc4edde82c..b5af3e9ef9 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -39,6 +39,7 @@ "groups": "Groups", "config": "Configuration" }, + "filterEntries": "filter entries", "paginator": { "next": "Next", "previous": "Previous" From ac2c41f9a1c479e934e71720fc60aa2d2de3feeb Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 10 Apr 2019 16:40:12 +0200 Subject: [PATCH 09/38] fixed lint error --- scm-ui-components/packages/ui-components/src/layout/Page.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 0055498864..34eb57e4f8 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -1,6 +1,5 @@ //@flow import * as React from "react"; -import { compose } from "redux"; import injectSheet from "react-jss"; import classNames from "classnames"; import { translate } from "react-i18next"; @@ -158,7 +157,4 @@ class Page extends React.Component<Props, State> { } } -export default compose( - injectSheet(styles), - translate("commons") -)(Page); +export default injectSheet(styles)(translate("commons")(Page)); From f1bed2f5581e17583e3d9433520f314710ee392a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 10 Apr 2019 16:49:19 +0200 Subject: [PATCH 10/38] Fix on submit handler --- .../packages/ui-components/src/layout/Page.js | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 34eb57e4f8..466d4c9fa5 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -51,19 +51,16 @@ class Page extends React.Component<Props, State> { constructor(props) { super(props); this.state = { value: "" }; - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); } - handleChange(event) { + handleChange = event => { this.setState({ value: event.target.value }); - } + }; - handleSubmit(event) { + handleSubmit = event => { this.props.filter(this.state.value); event.preventDefault(); - } + }; render() { const { error } = this.props; @@ -94,11 +91,11 @@ class Page extends React.Component<Props, State> { "column is-three-fifths is-mobile-action-spacing" )} > - <form className={classNames(classes.inputField, "input-field")}> - <div - className="control has-icons-left" - onSubmit={this.handleSubmit} - > + <form + className={classNames(classes.inputField, "input-field")} + onSubmit={this.handleSubmit} + > + <div className="control has-icons-left"> <input className={classNames(classes.inputHeight, "input")} type="search" From 77b1bb53e82b23329efe0b75f49796011400333d Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 10 Apr 2019 17:23:55 +0200 Subject: [PATCH 11/38] fixed flow error: external prop was missing in group constructor --- scm-ui/src/groups/components/GroupForm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index d3b6799860..baa68de8ef 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -40,7 +40,8 @@ class GroupForm extends React.Component<Props, State> { }, _links: {}, members: [], - type: "" + type: "", + external: false }, nameValidationError: false }; From 99e78da67b8e8fb28a3e4d78f51e20ace70b2cd3 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 10 Apr 2019 17:24:35 +0200 Subject: [PATCH 12/38] added filter for groups --- scm-ui/src/groups/containers/Groups.js | 9 ++++++--- scm-ui/src/groups/modules/groups.js | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 78e373e261..11d60f66c4 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -39,7 +39,7 @@ type Props = { history: History, // dispatch functions - fetchGroupsByPage: (link: string, page: number) => void, + fetchGroupsByPage: (link: string, page: number, filter?: string) => void, fetchGroupsByLink: (link: string) => void }; @@ -74,6 +74,9 @@ class Groups extends React.Component<Props> { subtitle={t("groups.subtitle")} loading={loading || !groups} error={error} + filter={filter => { + this.props.fetchGroupsByPage(this.props.groupLink, this.props.page, filter); + }} > <GroupTable groups={groups} /> {this.renderPaginator()} @@ -152,8 +155,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchGroupsByPage: (link: string, page: number) => { - dispatch(fetchGroupsByPage(link, page)); + fetchGroupsByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchGroupsByPage(link, page, filter)); }, fetchGroupsByLink: (link: string) => { dispatch(fetchGroupsByLink(link)); diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index bbaccf6c4a..cb3c24aa0f 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -40,9 +40,14 @@ export function fetchGroups(link: string) { return fetchGroupsByLink(link); } -export function fetchGroupsByPage(link: string, page: number) { +export function fetchGroupsByPage(link: string, page: number, filter?: string) { // backend start counting by 0 - return fetchGroupsByLink(link + "?page=" + (page - 1)); + if (filter) { + return fetchGroupsByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); + } + return fetchGroupsByLink(`${link}?page=${page - 1}`); } export function fetchGroupsByLink(link: string) { From 3008aaaa4d30fe641c25843fa54deef19a3988da Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 10 Apr 2019 17:25:36 +0200 Subject: [PATCH 13/38] added filter for repos --- scm-ui/src/repos/containers/Overview.js | 9 ++++++--- scm-ui/src/repos/modules/repos.js | 7 ++++++- scm-ui/src/users/modules/users.js | 12 ++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 1d672aafb1..aa42e1846c 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -36,7 +36,7 @@ type Props = { // dispatched functions fetchRepos: string => void, - fetchReposByPage: (string, number) => void, + fetchReposByPage: (link: string, page: number, filter?: string) => void, fetchReposByLink: string => void, // context props @@ -71,6 +71,9 @@ class Overview extends React.Component<Props> { subtitle={t("overview.subtitle")} loading={loading} error={error} + filter={filter => { + this.props.fetchReposByPage(this.props.reposLink, this.props.page, filter); + }} > {this.renderList()} {this.renderPageActionCreateButton()} @@ -151,8 +154,8 @@ const mapDispatchToProps = dispatch => { fetchRepos: (link: string) => { dispatch(fetchRepos(link)); }, - fetchReposByPage: (link: string, page: number) => { - dispatch(fetchReposByPage(link, page)); + fetchReposByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchReposByPage(link, page, filter)); }, fetchReposByLink: (link: string) => { dispatch(fetchReposByLink(link)); diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index fa89dc42a6..cd48f89fea 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -46,7 +46,12 @@ export function fetchRepos(link: string) { return fetchReposByLink(link); } -export function fetchReposByPage(link: string, page: number) { +export function fetchReposByPage(link: string, page: number, filter?: string) { + if (filter) { + return fetchReposByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); + } return fetchReposByLink(`${link}?page=${page - 1}`); } diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index c7d5412209..c83a4d9b94 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -45,10 +45,12 @@ export function fetchUsers(link: string) { export function fetchUsersByPage(link: string, page: number, filter?: string) { // backend start counting by 0 - if(filter) { - return fetchUsersByLink(link + "?page=" + (page - 1) + "&q=" + decodeURIComponent(filter)); + if (filter) { + return fetchUsersByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); } - return fetchUsersByLink(link + "?page=" + (page - 1)); + return fetchUsersByLink(`${link}?page=${page - 1}`); } export function fetchUsersByLink(link: string) { @@ -156,9 +158,7 @@ export function createUser(link: string, user: User, callback?: () => void) { callback(); } }) - .catch(error => - dispatch(createUserFailure(error)) - ); + .catch(error => dispatch(createUserFailure(error))); }; } From 52257aec2659a1d6bd8f3691989a3024be5e6de5 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 11:40:14 +0200 Subject: [PATCH 14/38] added filter to LinkPaginator --- .../ui-components/src/LinkPaginator.js | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/LinkPaginator.js b/scm-ui-components/packages/ui-components/src/LinkPaginator.js index d09306e04c..900107f4c1 100644 --- a/scm-ui-components/packages/ui-components/src/LinkPaginator.js +++ b/scm-ui-components/packages/ui-components/src/LinkPaginator.js @@ -1,18 +1,26 @@ //@flow import React from "react"; -import {translate} from "react-i18next"; -import type {PagedCollection} from "@scm-manager/ui-types"; -import {Button} from "./buttons"; +import { translate } from "react-i18next"; +import type { PagedCollection } from "@scm-manager/ui-types"; +import { Button } from "./buttons"; type Props = { collection: PagedCollection, page: number, + filter?: any, // context props t: string => string }; class LinkPaginator extends React.Component<Props> { + addFilterToLink(link: string) { + const { filter } = this.props; + if (filter) { + return `${link}?q=${filter}`; + } + return link; + } renderFirstButton() { return ( @@ -20,7 +28,7 @@ class LinkPaginator extends React.Component<Props> { className={"pagination-link"} label={"1"} disabled={false} - link={"1"} + link={this.addFilterToLink("1")} /> ); } @@ -34,7 +42,7 @@ class LinkPaginator extends React.Component<Props> { className={className} label={label ? label : previousPage.toString()} disabled={!this.hasLink("prev")} - link={`${previousPage}`} + link={this.addFilterToLink(`${previousPage}`)} /> ); } @@ -52,7 +60,7 @@ class LinkPaginator extends React.Component<Props> { className={className} label={label ? label : nextPage.toString()} disabled={!this.hasLink("next")} - link={`${nextPage}`} + link={this.addFilterToLink(`${nextPage}`)} /> ); } @@ -64,7 +72,7 @@ class LinkPaginator extends React.Component<Props> { className={"pagination-link"} label={`${collection.pageTotal}`} disabled={false} - link={`${collection.pageTotal}`} + link={this.addFilterToLink(`${collection.pageTotal}`)} /> ); } @@ -118,7 +126,10 @@ class LinkPaginator extends React.Component<Props> { const { t } = this.props; return ( <nav className="pagination is-centered" aria-label="pagination"> - {this.renderPreviousButton("pagination-previous", t("paginator.previous"))} + {this.renderPreviousButton( + "pagination-previous", + t("paginator.previous") + )} <ul className="pagination-list"> {this.pageLinks().map((link, index) => { return <li key={index}>{link}</li>; From bc3d9388e6963e5bda4e4911fe0b35eff434c3b7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 12:05:44 +0200 Subject: [PATCH 15/38] added slash at the end of the url to correctly link for pagination --- .../src/navigation/PrimaryNavigation.js | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) 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 897c63138e..ce0c2d75dc 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js +++ b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js @@ -7,12 +7,11 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { t: string => string, - links: Links, + links: Links }; class PrimaryNavigation extends React.Component<Props> { - - createNavigationAppender = (navigationItems) => { + createNavigationAppender = navigationItems => { const { t, links } = this.props; return (to: string, match: string, label: string, linkName: string) => { @@ -24,8 +23,8 @@ class PrimaryNavigation extends React.Component<Props> { match={match} label={t(label)} key={linkName} - />) - ; + /> + ); navigationItems.push(navigationItem); } }; @@ -63,16 +62,26 @@ class PrimaryNavigation extends React.Component<Props> { <ExtensionPoint name="primary-navigation.first-menu" props={props} /> ); } - append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories"); - append("/users", "/(user|users)", "primary-navigation.users", "users"); - append("/groups", "/(group|groups)", "primary-navigation.groups", "groups"); + append( + "/repos/", + "/(repo|repos)", + "primary-navigation.repositories", + "repositories" + ); + append("/users/", "/(user|users)", "primary-navigation.users", "users"); + append( + "/groups/", + "/(group|groups)", + "primary-navigation.groups", + "groups" + ); append("/config", "/config", "primary-navigation.config", "config"); navigationItems.push( <ExtensionPoint name="primary-navigation" renderAll={true} - props={{links: this.props.links}} + props={{ links: this.props.links }} /> ); @@ -86,9 +95,7 @@ class PrimaryNavigation extends React.Component<Props> { return ( <nav className="tabs is-boxed"> - <ul> - {navigationItems} - </ul> + <ul>{navigationItems}</ul> </nav> ); } From 1e845afd84ce9b6303120cc175514fc9eeba7b3e Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 12:07:43 +0200 Subject: [PATCH 16/38] added error check so that the search and create button are not displayed when users are logged out --- scm-ui-components/packages/ui-components/src/layout/Page.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 466d4c9fa5..2f9add611c 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -78,12 +78,12 @@ class Page extends React.Component<Props, State> { } renderPageHeader() { - const { title, subtitle, children, classes, t } = this.props; + const { error, title, subtitle, children, classes, t } = this.props; let pageActions = null; let pageActionsExists = false; React.Children.forEach(children, child => { - if (child && child.type.name === PageActions.name) { + if (child && child.type.name === PageActions.name && !error) { pageActions = ( <div className={classNames( From 8b7f2142eb3e8e24be6d7ce4b09bc6d91bf61fe9 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 12:13:04 +0200 Subject: [PATCH 17/38] improved filter/search function and used new pagination --- scm-ui/src/groups/containers/Groups.js | 118 ++++++++++++++----------- scm-ui/src/groups/modules/groups.js | 2 +- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 11d60f66c4..ea8f2c6cd3 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -5,11 +5,13 @@ import { translate } from "react-i18next"; import type { Group } from "@scm-manager/ui-types"; import type { PagedCollection } from "@scm-manager/ui-types"; import type { History } from "history"; +import queryString from "query-string"; import { Page, PageActions, Button, - Paginator + LinkPaginator, + getPageFromMatch } from "@scm-manager/ui-components"; import { GroupTable } from "./../components/table"; import CreateGroupButton from "../components/buttons/CreateGroupButton"; @@ -24,6 +26,7 @@ import { selectListAsCollection } from "../modules/groups"; import { getGroupsLink } from "../../modules/indexResource"; +import { compose } from "redux"; type Props = { groups: Group[], @@ -37,37 +40,47 @@ type Props = { // context objects t: string => string, history: History, + location: any, // dispatch functions - fetchGroupsByPage: (link: string, page: number, filter?: string) => void, + fetchGroupsByPage: (link: string, page: number, filter?: any) => void, fetchGroupsByLink: (link: string) => void }; -class Groups extends React.Component<Props> { - componentDidMount() { - this.props.fetchGroupsByPage(this.props.groupLink, this.props.page); +type State = { + page: number +}; + +class Groups extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + page: -1 + }; } - onPageChange = (link: string) => { - this.props.fetchGroupsByLink(link); - }; + componentDidMount() { + const { fetchGroupsByPage, groupLink, page } = this.props; + fetchGroupsByPage(groupLink, page, this.getQueryString()); + this.setState({ page: page }); + } - /** - * reflect page transitions in the uri - */ componentDidUpdate = (prevProps: Props) => { - const { page, list } = this.props; - if (list.page >= 0) { - // backend starts paging by 0 - const statePage: number = list.page + 1; - if (page !== statePage) { - this.props.history.push(`/groups/${statePage}`); + const { list, page, location, fetchGroupsByPage, groupLink } = this.props; + if (list && page) { + if ( + page !== this.state.page || + prevProps.location.search !== location.search + ) { + fetchGroupsByPage(groupLink, page, this.getQueryString()); + this.setState({ page: page }); } } }; render() { - const { groups, loading, error, t } = this.props; + const { groups, loading, error, history, t } = this.props; return ( <Page title={t("groups.title")} @@ -75,7 +88,7 @@ class Groups extends React.Component<Props> { loading={loading || !groups} error={error} filter={filter => { - this.props.fetchGroupsByPage(this.props.groupLink, this.props.page, filter); + history.push("/groups/?q=" + filter); }} > <GroupTable groups={groups} /> @@ -86,60 +99,57 @@ class Groups extends React.Component<Props> { ); } - renderPaginator() { - const { list } = this.props; + renderPaginator = () => { + const { list, page } = this.props; if (list) { - return <Paginator collection={list} onPageChange={this.onPageChange} />; + return ( + <LinkPaginator + collection={list} + page={page} + filter={this.getQueryString()} + /> + ); + } + return null; + }; + + renderCreateButton() { + if (this.props.canAddGroups) { + return <CreateGroupButton />; } return null; } - renderCreateButton() { - if (this.props.canAddGroups) { - return ( - <CreateGroupButton /> - ); - } else { - return; - } - } - renderPageActionCreateButton() { - if (this.props.canAddGroups) { + const { canAddGroups, t } = this.props; + if (canAddGroups) { return ( <PageActions> <Button - label={this.props.t("create-group-button.label")} + label={t("create-group-button.label")} link="/groups/add" color="primary" /> </PageActions> ); - } else { - return; } + return null; } + + getQueryString = () => { + const { location } = this.props; + return location.search ? queryString.parse(location.search).q : null; + }; } -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 { match } = ownProps; const groups = getGroupsFromState(state); const loading = isFetchGroupsPending(state); const error = getFetchGroupsFailure(state); - - const page = getPageFromProps(ownProps); + const page = getPageFromMatch(match); const canAddGroups = isPermittedToCreateGroups(state); const list = selectListAsCollection(state); - const groupLink = getGroupsLink(state); return { @@ -155,7 +165,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchGroupsByPage: (link: string, page: number, filter?: string) => { + fetchGroupsByPage: (link: string, page: number, filter?: any) => { dispatch(fetchGroupsByPage(link, page, filter)); }, fetchGroupsByLink: (link: string) => { @@ -164,7 +174,9 @@ const mapDispatchToProps = dispatch => { }; }; -export default connect( - mapStateToProps, - mapDispatchToProps +export default compose( + connect( + mapStateToProps, + mapDispatchToProps + ) )(translate("groups")(Groups)); diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index cb3c24aa0f..17d34f9335 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -40,7 +40,7 @@ export function fetchGroups(link: string) { return fetchGroupsByLink(link); } -export function fetchGroupsByPage(link: string, page: number, filter?: string) { +export function fetchGroupsByPage(link: string, page: number, filter?: any) { // backend start counting by 0 if (filter) { return fetchGroupsByLink( From cc8be054d174e0402b5fbf3d6604e680a9aed69b Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 12:19:17 +0200 Subject: [PATCH 18/38] added query-string to package.json and run prettier --- scm-ui/package.json | 5 +++-- scm-ui/src/containers/Main.js | 14 ++++++------- scm-ui/yarn.lock | 37 +++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/scm-ui/package.json b/scm-ui/package.json index 6844ea3ec7..851a385235 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -20,9 +20,10 @@ "moment": "^2.22.2", "node-sass": "^4.9.3", "postcss-easy-import": "^3.0.0", - "react": "^16.4.2", + "react": "^16.8.6", + "query-string": "5", "react-diff-view": "^1.8.1", - "react-dom": "^16.4.2", + "react-dom": "^16.8.6", "react-i18next": "^7.9.0", "react-jss": "^8.6.0", "react-redux": "^5.0.7", diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 570455d18d..48debaf5b8 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -2,15 +2,15 @@ import React from "react"; import { Redirect, Route, Switch, withRouter } from "react-router-dom"; -import type {Links} from "@scm-manager/ui-types"; +import type { Links } from "@scm-manager/ui-types"; import Overview from "../repos/containers/Overview"; import Users from "../users/containers/Users"; import Login from "../containers/Login"; import Logout from "../containers/Logout"; -import {ProtectedRoute} from "@scm-manager/ui-components"; -import {binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ProtectedRoute } from "@scm-manager/ui-components"; +import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import AddUser from "../users/containers/AddUser"; import SingleUser from "../users/containers/SingleUser"; @@ -33,14 +33,14 @@ 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){ + let url = "/repos"; + if (redirectUrlFactory) { url = redirectUrlFactory(this.props); } return ( <div className="main"> <Switch> - <Redirect exact from="/" to={url}/> + <Redirect exact from="/" to={url} /> <Route exact path="/login" component={Login} /> <Route path="/logout" component={Logout} /> <ProtectedRoute @@ -125,7 +125,7 @@ class Main extends React.Component<Props> { <ExtensionPoint name="main.route" renderAll={true} - props={{authenticated, links}} + props={{ authenticated, links }} /> </Switch> </div> diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 5b6ec89ef3..ac7c7b0122 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -6831,6 +6831,14 @@ qs@~6.5.1, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +query-string@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@~0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -6920,6 +6928,15 @@ react-dom@^16.4.2: prop-types "^15.6.2" schedule "^0.5.0" +react-dom@^16.8.6: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.13.6" + react-i18next@^7.9.0: version "7.13.0" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.13.0.tgz#a6f64fd749215ec70400f90da6cbde2a9c5b1588" @@ -7050,6 +7067,15 @@ react@^16.4.2: prop-types "^15.6.2" schedule "^0.5.0" +react@^16.8.6: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.13.6" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -7562,6 +7588,13 @@ schedule@^0.5.0: dependencies: object-assign "^4.1.1" +scheduler@^0.13.6: + version "0.13.6" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scss-tokenizer@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" @@ -7990,6 +8023,10 @@ stream-throttle@^0.1.3: commander "^2.2.0" limiter "^1.0.5" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" From fc48e1bbd799fcb0b87c0eb1c304ebcd83c518a3 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 12:37:48 +0200 Subject: [PATCH 19/38] improved filter/search function and used new pagination --- scm-ui/src/repos/containers/Overview.js | 106 +++++++++++------- scm-ui/src/repos/containers/RepositoryRoot.js | 32 +++--- scm-ui/src/repos/modules/repos.js | 2 +- 3 files changed, 84 insertions(+), 56 deletions(-) diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index aa42e1846c..aa0114e10c 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -19,12 +19,14 @@ import { PageActions, Button, CreateButton, - Paginator + LinkPaginator, + getPageFromMatch } from "@scm-manager/ui-components"; import RepositoryList from "../components/list"; import { withRouter } from "react-router-dom"; import type { History } from "history"; import { getRepositoriesLink } from "../../modules/indexResource"; +import queryString from "query-string"; type Props = { page: number, @@ -34,37 +36,57 @@ type Props = { showCreateButton: boolean, reposLink: string, - // dispatched functions - fetchRepos: string => void, - fetchReposByPage: (link: string, page: number, filter?: string) => void, - fetchReposByLink: string => void, - // context props t: string => string, - history: History + history: History, + location: any, + + // dispatched functions + fetchRepos: string => void, + fetchReposByPage: (link: string, page: number, filter?: any) => void, + fetchReposByLink: string => void }; -class Overview extends React.Component<Props> { - componentDidMount() { - this.props.fetchReposByPage(this.props.reposLink, this.props.page); +type State = { + page: number +}; + +class Overview extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + page: -1 + }; } - /** - * reflect page transitions in the uri - */ - componentDidUpdate() { - const { page, collection } = this.props; - if (collection) { - // backend starts paging by 0 - const statePage: number = collection.page + 1; - if (page !== statePage) { - this.props.history.push(`/repos/${statePage}`); + componentDidMount() { + const { fetchReposByPage, reposLink, page } = this.props; + fetchReposByPage(reposLink, page, this.getQueryString()); + this.setState({ page: page }); + } + + componentDidUpdate = (prevProps: Props) => { + const { + collection, + page, + location, + fetchReposByPage, + reposLink + } = this.props; + if (collection && page) { + if ( + page !== this.state.page || + prevProps.location.search !== location.search + ) { + fetchReposByPage(reposLink, page, this.getQueryString()); + this.setState({ page: page }); } } - } + }; render() { - const { error, loading, t } = this.props; + const { error, loading, history, t } = this.props; return ( <Page title={t("overview.title")} @@ -72,7 +94,7 @@ class Overview extends React.Component<Props> { loading={loading} error={error} filter={filter => { - this.props.fetchReposByPage(this.props.reposLink, this.props.page, filter); + history.push("/repos/?q=" + filter); }} > {this.renderList()} @@ -82,14 +104,18 @@ class Overview extends React.Component<Props> { } renderList() { - const { collection, fetchReposByLink } = this.props; + const { collection, page } = this.props; if (collection) { return ( - <div> + <> <RepositoryList repositories={collection._embedded.repositories} /> - <Paginator collection={collection} onPageChange={fetchReposByLink} /> + <LinkPaginator + collection={collection} + page={page} + filter={this.getQueryString()} + /> {this.renderCreateButton()} - </div> + </> ); } return null; @@ -120,32 +146,28 @@ class Overview extends React.Component<Props> { } return null; } + + getQueryString = () => { + const { location } = this.props; + return location.search ? queryString.parse(location.search).q : null; + }; } -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 page = getPageFromProps(ownProps); + const { match } = ownProps; const collection = getRepositoryCollection(state); const loading = isFetchReposPending(state); const error = getFetchReposFailure(state); + const page = getPageFromMatch(match); const showCreateButton = isAbleToCreateRepos(state); const reposLink = getRepositoriesLink(state); return { - reposLink, - page, collection, loading, error, - showCreateButton + page, + showCreateButton, + reposLink }; }; @@ -154,7 +176,7 @@ const mapDispatchToProps = dispatch => { fetchRepos: (link: string) => { dispatch(fetchRepos(link)); }, - fetchReposByPage: (link: string, page: number, filter?: string) => { + fetchReposByPage: (link: string, page: number, filter?: any) => { dispatch(fetchReposByPage(link, page, filter)); }, fetchReposByLink: (link: string) => { diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 62c1800100..ed107a3ef1 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -8,7 +8,7 @@ import { } from "../modules/repos"; import { connect } from "react-redux"; -import {Redirect, Route, Switch} from "react-router-dom"; +import { Redirect, Route, Switch } from "react-router-dom"; import type { Repository } from "@scm-manager/ui-types"; import { @@ -18,7 +18,8 @@ import { SubNavigation, NavLink, Page, - Section, ErrorPage + Section, + ErrorPage } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; import RepositoryDetails from "../components/RepositoryDetails"; @@ -33,8 +34,8 @@ import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; -import {getLinks, getRepositoriesLink} from "../../modules/indexResource"; -import {binder, ExtensionPoint} from "@scm-manager/ui-extensions"; +import { getLinks, getRepositoriesLink } from "../../modules/indexResource"; +import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { namespace: string, @@ -82,11 +83,13 @@ class RepositoryRoot extends React.Component<Props> { const { loading, error, indexLinks, repository, t } = this.props; if (error) { - return <ErrorPage - title={t("repositoryRoot.errorTitle")} - subtitle={t("repositoryRoot.errorSubtitle")} - error={error} - /> + return ( + <ErrorPage + title={t("repositoryRoot.errorTitle")} + subtitle={t("repositoryRoot.errorSubtitle")} + error={error} + /> + ); } if (!repository || loading) { @@ -101,11 +104,14 @@ class RepositoryRoot extends React.Component<Props> { indexLinks }; - const redirectUrlFactory = binder.getExtension("repository.redirect", this.props); + const redirectUrlFactory = binder.getExtension( + "repository.redirect", + this.props + ); let redirectedUrl; - if (redirectUrlFactory){ + if (redirectUrlFactory) { redirectedUrl = url + redirectUrlFactory(this.props); - }else{ + } else { redirectedUrl = url + "/info"; } @@ -114,7 +120,7 @@ class RepositoryRoot extends React.Component<Props> { <div className="columns"> <div className="column is-three-quarters is-clipped"> <Switch> - <Redirect exact from={this.props.match.url} to={redirectedUrl}/> + <Redirect exact from={this.props.match.url} to={redirectedUrl} /> <Route path={`${url}/info`} exact diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index cd48f89fea..0ce2113e15 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -46,7 +46,7 @@ export function fetchRepos(link: string) { return fetchReposByLink(link); } -export function fetchReposByPage(link: string, page: number, filter?: string) { +export function fetchReposByPage(link: string, page: number, filter?: any) { if (filter) { return fetchReposByLink( `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` From 1ed805f16ec461eeb155c70f44eaa5c7a7fe3808 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 12:50:06 +0200 Subject: [PATCH 20/38] improved filter/search function and used new pagination for Users --- scm-ui/src/groups/containers/Groups.js | 24 +++-- scm-ui/src/users/containers/Users.js | 121 +++++++++++++------------ scm-ui/src/users/modules/users.js | 2 +- 3 files changed, 77 insertions(+), 70 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index ea8f2c6cd3..a11ff4eeaf 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -2,20 +2,10 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; -import type { Group } from "@scm-manager/ui-types"; -import type { PagedCollection } from "@scm-manager/ui-types"; +import { compose } from "redux"; import type { History } from "history"; import queryString from "query-string"; -import { - Page, - PageActions, - Button, - LinkPaginator, - getPageFromMatch -} from "@scm-manager/ui-components"; -import { GroupTable } from "./../components/table"; -import CreateGroupButton from "../components/buttons/CreateGroupButton"; - +import type { Group, PagedCollection } from "@scm-manager/ui-types"; import { fetchGroupsByPage, fetchGroupsByLink, @@ -25,8 +15,16 @@ import { isPermittedToCreateGroups, selectListAsCollection } from "../modules/groups"; +import { + Page, + PageActions, + Button, + LinkPaginator, + getPageFromMatch +} from "@scm-manager/ui-components"; +import { GroupTable } from "./../components/table"; +import CreateGroupButton from "../components/buttons/CreateGroupButton"; import { getGroupsLink } from "../../modules/indexResource"; -import { compose } from "redux"; type Props = { groups: Group[], diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index e2eccc3ecf..546eb635cb 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,9 +1,10 @@ // @flow import React from "react"; -import type { History } from "history"; import { connect } from "react-redux"; import { translate } from "react-i18next"; - +import type { History } from "history"; +import queryString from "query-string"; +import type { User, PagedCollection } from "@scm-manager/ui-types"; import { fetchUsersByPage, fetchUsersByLink, @@ -13,16 +14,15 @@ import { isFetchUsersPending, getFetchUsersFailure } from "../modules/users"; - import { Page, PageActions, Button, CreateButton, - Paginator + LinkPaginator, + getPageFromMatch } 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 = { @@ -37,37 +37,47 @@ type Props = { // context objects t: string => string, history: History, + location: any, // dispatch functions - fetchUsersByPage: (link: string, page: number, filter?: string) => void, + fetchUsersByPage: (link: string, page: number, filter?: any) => void, fetchUsersByLink: (link: string) => void }; -class Users extends React.Component<Props> { - componentDidMount() { - this.props.fetchUsersByPage(this.props.usersLink, this.props.page); +type State = { + page: number +}; + +class Users extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + page: -1 + }; } - onPageChange = (link: string) => { - this.props.fetchUsersByLink(link); - }; + componentDidMount() { + const { fetchUsersByPage, usersLink, page } = this.props; + fetchUsersByPage(usersLink, page, this.getQueryString()); + this.setState({ page: page }); + } - /** - * 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}`); + componentDidUpdate = (prevProps: Props) => { + const { list, page, location, fetchUsersByPage, usersLink } = this.props; + if (list && page) { + if ( + page !== this.state.page || + prevProps.location.search !== location.search + ) { + fetchUsersByPage(usersLink, page, this.getQueryString()); + this.setState({ page: page }); } } - } + }; render() { - const { users, loading, error, t } = this.props; + const { users, loading, error, history, t } = this.props; return ( <Page title={t("users.title")} @@ -75,7 +85,7 @@ class Users extends React.Component<Props> { loading={loading || !users} error={error} filter={filter => { - this.props.fetchUsersByPage(this.props.usersLink, this.props.page, filter); + history.push("/users/?q=" + filter); }} > <UserTable users={users} /> @@ -86,26 +96,31 @@ class Users extends React.Component<Props> { ); } - renderPaginator() { - const { list } = this.props; + renderPaginator = () => { + const { list, page } = this.props; if (list) { - return <Paginator collection={list} onPageChange={this.onPageChange} />; + return ( + <LinkPaginator + collection={list} + page={page} + filter={this.getQueryString()} + /> + ); + } + return null; + }; + + renderCreateButton() { + const { canAddUsers, t } = this.props; + if (canAddUsers) { + return <CreateButton label={t("users.createButton")} link="/users/add" />; } return null; } - renderCreateButton() { - const { t } = this.props; - if (this.props.canAddUsers) { - return <CreateButton label={t("users.createButton")} link="/users/add" />; - } else { - return; - } - } - renderPageActionCreateButton() { - const { t } = this.props; - if (this.props.canAddUsers) { + const { canAddUsers, t } = this.props; + if (canAddUsers) { return ( <PageActions> <Button @@ -115,32 +130,26 @@ class Users extends React.Component<Props> { /> </PageActions> ); - } else { - return; } + + return null; } + + getQueryString = () => { + const { location } = this.props; + return location.search ? queryString.parse(location.search).q : null; + }; } -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 { match } = ownProps; const users = getUsersFromState(state); const loading = isFetchUsersPending(state); const error = getFetchUsersFailure(state); - - const usersLink = getUsersLink(state); - - const page = getPageFromProps(ownProps); + const page = getPageFromMatch(match); const canAddUsers = isPermittedToCreateUsers(state); const list = selectListAsCollection(state); + const usersLink = getUsersLink(state); return { users, @@ -155,7 +164,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchUsersByPage: (link: string, page: number, filter?: string) => { + fetchUsersByPage: (link: string, page: number, filter?: any) => { dispatch(fetchUsersByPage(link, page, filter)); }, fetchUsersByLink: (link: string) => { diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index c83a4d9b94..c9b3571b02 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -43,7 +43,7 @@ export function fetchUsers(link: string) { return fetchUsersByLink(link); } -export function fetchUsersByPage(link: string, page: number, filter?: string) { +export function fetchUsersByPage(link: string, page: number, filter?: any) { // backend start counting by 0 if (filter) { return fetchUsersByLink( From 53ce955f66d623b5e38f030ac14d185d381560e9 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 15:46:49 +0200 Subject: [PATCH 21/38] removed unnecessary local page state --- scm-ui/src/groups/containers/Groups.js | 35 +++++-------------------- scm-ui/src/repos/containers/Overview.js | 34 +++++++----------------- scm-ui/src/users/containers/Users.js | 22 ++++------------ 3 files changed, 21 insertions(+), 70 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index daa9f62275..24a881f501 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -2,7 +2,6 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; -import { compose } from "redux"; import type { History } from "history"; import queryString from "query-string"; import type { Group, PagedCollection } from "@scm-manager/ui-types"; @@ -46,41 +45,21 @@ type Props = { fetchGroupsByLink: (link: string) => void }; -type State = { - page: number -}; - -class Groups extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - page: -1 - }; - } - +class Groups extends React.Component<Props> { componentDidMount() { const { fetchGroupsByPage, groupLink, page } = this.props; fetchGroupsByPage(groupLink, page, this.getQueryString()); - this.setState({ page: page }); } - onPageChange = (link: string) => { - this.props.fetchGroupsByLink(link); - }; - - /** - * reflect page transitions in the uri - */ componentDidUpdate = (prevProps: Props) => { - const { list, page, location, fetchGroupsByPage, groupLink } = this.props; - if (list && page) { + const { list, page, loading, location, fetchGroupsByPage, groupLink } = this.props; + if (list && page && !loading) { + const statePage: number = list.page + 1; if ( - page !== this.state.page || + page !== statePage || prevProps.location.search !== location.search ) { fetchGroupsByPage(groupLink, page, this.getQueryString()); - this.setState({ page: page }); } } }; @@ -192,9 +171,7 @@ const mapDispatchToProps = dispatch => { }; }; -export default compose( - connect( +export default connect( mapStateToProps, mapDispatchToProps - ) )(translate("groups")(Groups)); diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 00acf2c832..b3274351d1 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -1,9 +1,11 @@ // @flow import React from "react"; - -import type { RepositoryCollection } from "@scm-manager/ui-types"; - import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import type { History } from "history"; +import queryString from "query-string"; +import { withRouter } from "react-router-dom"; +import type { RepositoryCollection } from "@scm-manager/ui-types"; import { fetchRepos, fetchReposByLink, @@ -13,7 +15,6 @@ import { isAbleToCreateRepos, isFetchReposPending } from "../modules/repos"; -import { translate } from "react-i18next"; import { Page, PageActions, @@ -24,10 +25,7 @@ import { getPageFromMatch } from "@scm-manager/ui-components"; import RepositoryList from "../components/list"; -import { withRouter } from "react-router-dom"; -import type { History } from "history"; import { getRepositoriesLink } from "../../modules/indexResource"; -import queryString from "query-string"; type Props = { page: number, @@ -48,40 +46,28 @@ type Props = { fetchReposByLink: string => void }; -type State = { - page: number -}; - -class Overview extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - page: -1 - }; - } - +class Overview extends React.Component<Props> { componentDidMount() { const { fetchReposByPage, reposLink, page } = this.props; fetchReposByPage(reposLink, page, this.getQueryString()); - this.setState({ page: page }); } componentDidUpdate = (prevProps: Props) => { const { collection, page, + loading, location, fetchReposByPage, reposLink } = this.props; - if (collection && page) { + if (collection && page && !loading) { + const statePage: number = collection.page + 1; if ( - page !== this.state.page || + page !== statePage || prevProps.location.search !== location.search ) { fetchReposByPage(reposLink, page, this.getQueryString()); - this.setState({ page: page }); } } }; diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 68d0864d59..32814b2645 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -45,34 +45,22 @@ type Props = { fetchUsersByLink: (link: string) => void }; -type State = { - page: number -}; - -class Users extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - page: -1 - }; - } +class Users extends React.Component<Props> { componentDidMount() { const { fetchUsersByPage, usersLink, page } = this.props; fetchUsersByPage(usersLink, page, this.getQueryString()); - this.setState({ page: page }); } componentDidUpdate = (prevProps: Props) => { - const { list, page, location, fetchUsersByPage, usersLink } = this.props; - if (list && page) { + const { list, page, loading, location, fetchUsersByPage, usersLink } = this.props; + if (list && page && !loading) { + const statePage: number = list.page + 1; if ( - page !== this.state.page || + page !== statePage || prevProps.location.search !== location.search ) { fetchUsersByPage(usersLink, page, this.getQueryString()); - this.setState({ page: page }); } } }; From b3de87a3437d5b0635dff1084d94767291447191 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 16:12:22 +0200 Subject: [PATCH 22/38] removed FilterInput functionality from page and added new ui-component --- .../ui-components/src/forms/FilterInput.js | 69 +++++++++++++++++++ .../packages/ui-components/src/forms/index.js | 1 + .../packages/ui-components/src/layout/Page.js | 61 ++-------------- 3 files changed, 75 insertions(+), 56 deletions(-) create mode 100644 scm-ui-components/packages/ui-components/src/forms/FilterInput.js diff --git a/scm-ui-components/packages/ui-components/src/forms/FilterInput.js b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js new file mode 100644 index 0000000000..8e41a177a7 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js @@ -0,0 +1,69 @@ +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; + +type Props = { + filter: string => void, + + // context props + classes: Object, + t: string => string +}; + +type State = { + value: string +}; + +const styles = { + inputField: { + float: "right", + marginTop: "1.25rem", + marginRight: "1.25rem" + }, + inputHeight: { + height: "2.5rem" + } +}; + +class FilterInput extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { value: "" }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + }; + + handleSubmit = event => { + this.props.filter(this.state.value); + event.preventDefault(); + }; + + render() { + const { classes, t } = this.props; + return ( + <form + className={classNames(classes.inputField, "input-field")} + onSubmit={this.handleSubmit} + > + <div className="control has-icons-left"> + <input + className={classNames(classes.inputHeight, "input")} + type="search" + placeholder={t("filterEntries")} + value={this.state.value} + onChange={this.handleChange} + /> + <span className="icon is-small is-left"> + <i className="fas fa-search" /> + </span> + </div> + </form> + ); + } +} + +export default injectSheet(styles)(translate("commons")(FilterInput)); diff --git a/scm-ui-components/packages/ui-components/src/forms/index.js b/scm-ui-components/packages/ui-components/src/forms/index.js index ef4d7a1ae4..aed38d44bc 100644 --- a/scm-ui-components/packages/ui-components/src/forms/index.js +++ b/scm-ui-components/packages/ui-components/src/forms/index.js @@ -5,6 +5,7 @@ export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEn export { default as MemberNameTable } from "./MemberNameTable.js"; export { default as Checkbox } from "./Checkbox.js"; export { default as Radio } from "./Radio.js"; +export { default as FilterInput } from "./FilterInput.js"; export { default as InputField } from "./InputField.js"; export { default as Select } from "./Select.js"; export { default as Textarea } from "./Textarea.js"; diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 2f9add611c..c6f5d132a5 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -2,7 +2,6 @@ import * as React from "react"; import injectSheet from "react-jss"; import classNames from "classnames"; -import { translate } from "react-i18next"; import Loading from "./../Loading"; import ErrorNotification from "./../ErrorNotification"; import Title from "./Title"; @@ -10,10 +9,6 @@ import Subtitle from "./Subtitle"; import PageActions from "./PageActions"; import ErrorBoundary from "../ErrorBoundary"; -type State = { - value: string -}; - type Props = { title?: string, subtitle?: string, @@ -21,46 +16,19 @@ type Props = { error?: Error, showContentOnError?: boolean, children: React.Node, - filter: string => void, // context props - classes: Object, - t: string => string + classes: Object }; const styles = { actions: { display: "flex", justifyContent: "flex-end" - }, - inputField: { - float: "right", - marginTop: "1.25rem", - marginRight: "1.25rem" - }, - inputHeight: { - height: "2.5rem" - }, - button: { - float: "right", - marginTop: "1.25rem" } }; -class Page extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { value: "" }; - } - - handleChange = event => { - this.setState({ value: event.target.value }); - }; - - handleSubmit = event => { - this.props.filter(this.state.value); - event.preventDefault(); - }; +class Page extends React.Component<Props> { render() { const { error } = this.props; @@ -78,7 +46,7 @@ class Page extends React.Component<Props, State> { } renderPageHeader() { - const { error, title, subtitle, children, classes, t } = this.props; + const { error, title, subtitle, children, classes } = this.props; let pageActions = null; let pageActionsExists = false; @@ -91,26 +59,7 @@ class Page extends React.Component<Props, State> { "column is-three-fifths is-mobile-action-spacing" )} > - <form - className={classNames(classes.inputField, "input-field")} - onSubmit={this.handleSubmit} - > - <div className="control has-icons-left"> - <input - className={classNames(classes.inputHeight, "input")} - type="search" - placeholder={t("filterEntries")} - value={this.state.value} - onChange={this.handleChange} - /> - <span className="icon is-small is-left"> - <i className="fas fa-search" /> - </span> - </div> - </form> - <div className={classNames(classes.button, "input-button control")}> - {child} - </div> + {child} </div> ); pageActionsExists = true; @@ -154,4 +103,4 @@ class Page extends React.Component<Props, State> { } } -export default injectSheet(styles)(translate("commons")(Page)); +export default injectSheet(styles)(Page); From 6f975acd21f22afab76991f9640cc899bd7b0d9f Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 16:35:17 +0200 Subject: [PATCH 23/38] added value prop to reflect changes to the q parameter in the url, used new FilterInput component in pageActions --- .../ui-components/src/forms/FilterInput.js | 3 +- scm-ui/src/groups/containers/Groups.js | 58 +++++++++++++------ scm-ui/src/repos/containers/Overview.js | 45 +++++++++----- scm-ui/src/users/containers/Users.js | 57 ++++++++++++------ 4 files changed, 108 insertions(+), 55 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/forms/FilterInput.js b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js index 8e41a177a7..72a90255f9 100644 --- a/scm-ui-components/packages/ui-components/src/forms/FilterInput.js +++ b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js @@ -6,6 +6,7 @@ import { translate } from "react-i18next"; type Props = { filter: string => void, + value?: string, // context props classes: Object, @@ -30,7 +31,7 @@ const styles = { class FilterInput extends React.Component<Props, State> { constructor(props) { super(props); - this.state = { value: "" }; + this.state = { value: this.props.value ? this.props.value : "" }; } handleChange = event => { diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 24a881f501..d8f2945ef5 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -1,6 +1,7 @@ //@flow import React from "react"; import { connect } from "react-redux"; +import classNames from "classnames"; import { translate } from "react-i18next"; import type { History } from "history"; import queryString from "query-string"; @@ -17,6 +18,7 @@ import { import { Page, PageActions, + FilterInput, Button, Notification, LinkPaginator, @@ -25,6 +27,7 @@ import { import { GroupTable } from "./../components/table"; import CreateGroupButton from "../components/buttons/CreateGroupButton"; import { getGroupsLink } from "../../modules/indexResource"; +import injectSheet from "react-jss"; type Props = { groups: Group[], @@ -36,6 +39,7 @@ type Props = { groupLink: string, // context objects + classes: Object, t: string => string, history: History, location: any, @@ -45,6 +49,13 @@ type Props = { fetchGroupsByLink: (link: string) => void }; +const styles = { + button: { + float: "right", + marginTop: "1.25rem" + } +}; + class Groups extends React.Component<Props> { componentDidMount() { const { fetchGroupsByPage, groupLink, page } = this.props; @@ -52,33 +63,34 @@ class Groups extends React.Component<Props> { } componentDidUpdate = (prevProps: Props) => { - const { list, page, loading, location, fetchGroupsByPage, groupLink } = this.props; + const { + list, + page, + loading, + location, + fetchGroupsByPage, + groupLink + } = this.props; if (list && page && !loading) { const statePage: number = list.page + 1; - if ( - page !== statePage || - prevProps.location.search !== location.search - ) { + if (page !== statePage || prevProps.location.search !== location.search) { fetchGroupsByPage(groupLink, page, this.getQueryString()); } } }; render() { - const { groups, loading, error, history, t } = this.props; + const { groups, loading, error, t } = this.props; return ( <Page title={t("groups.title")} subtitle={t("groups.subtitle")} loading={loading || !groups} error={error} - filter={filter => { - history.push("/groups/?q=" + filter); - }} > {this.renderGroupTable()} {this.renderCreateButton()} - {this.renderPageActionCreateButton()} + {this.renderPageActions()} </Page> ); } @@ -117,16 +129,24 @@ class Groups extends React.Component<Props> { return null; } - renderPageActionCreateButton() { - const { canAddGroups, t } = this.props; + renderPageActions() { + const { canAddGroups, history, classes, t } = this.props; if (canAddGroups) { return ( <PageActions> - <Button - label={t("create-group-button.label")} - link="/groups/add" - color="primary" + <FilterInput + value={this.getQueryString()} + filter={filter => { + history.push("/groups/?q=" + filter); + }} /> + <div className={classNames(classes.button, "input-button control")}> + <Button + label={t("create-group-button.label")} + link="/groups/add" + color="primary" + /> + </div> </PageActions> ); } @@ -172,6 +192,6 @@ const mapDispatchToProps = dispatch => { }; export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("groups")(Groups)); + mapStateToProps, + mapDispatchToProps +)(injectSheet(styles)(translate("groups")(Groups))); diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index b3274351d1..0b39ee367a 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -1,6 +1,8 @@ // @flow import React from "react"; import { connect } from "react-redux"; +import classNames from "classnames"; +import injectSheet from "react-jss"; import { translate } from "react-i18next"; import type { History } from "history"; import queryString from "query-string"; @@ -18,6 +20,7 @@ import { import { Page, PageActions, + FilterInput, Button, CreateButton, Notification, @@ -36,6 +39,7 @@ type Props = { reposLink: string, // context props + classes: Object, t: string => string, history: History, location: any, @@ -46,6 +50,13 @@ type Props = { fetchReposByLink: string => void }; +const styles = { + button: { + float: "right", + marginTop: "1.25rem" + } +}; + class Overview extends React.Component<Props> { componentDidMount() { const { fetchReposByPage, reposLink, page } = this.props; @@ -63,29 +74,23 @@ class Overview extends React.Component<Props> { } = this.props; if (collection && page && !loading) { const statePage: number = collection.page + 1; - if ( - page !== statePage || - prevProps.location.search !== location.search - ) { + if (page !== statePage || prevProps.location.search !== location.search) { fetchReposByPage(reposLink, page, this.getQueryString()); } } }; render() { - const { error, loading, history, t } = this.props; + const { error, loading, t } = this.props; return ( <Page title={t("overview.title")} subtitle={t("overview.subtitle")} loading={loading} error={error} - filter={filter => { - history.push("/repos/?q=" + filter); - }} > {this.renderOverview()} - {this.renderPageActionCreateButton()} + {this.renderPageActions()} </Page> ); } @@ -133,16 +138,24 @@ class Overview extends React.Component<Props> { return null; } - renderPageActionCreateButton() { - const { showCreateButton, t } = this.props; + renderPageActions() { + const { showCreateButton, history, classes, t } = this.props; if (showCreateButton) { return ( <PageActions> - <Button - label={t("overview.createButton")} - link="/repos/create" - color="primary" + <FilterInput + value={this.getQueryString()} + filter={filter => { + history.push("/repos/?q=" + filter); + }} /> + <div className={classNames(classes.button, "input-button control")}> + <Button + label={t("overview.createButton")} + link="/repos/create" + color="primary" + /> + </div> </PageActions> ); } @@ -189,4 +202,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("repos")(withRouter(Overview))); +)(injectSheet(styles)(translate("repos")(withRouter(Overview)))); diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 32814b2645..0316a1ee5f 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,6 +1,8 @@ // @flow import React from "react"; import { connect } from "react-redux"; +import classNames from "classnames"; +import injectSheet from "react-jss"; import { translate } from "react-i18next"; import type { History } from "history"; import queryString from "query-string"; @@ -21,7 +23,8 @@ import { CreateButton, Notification, LinkPaginator, - getPageFromMatch + getPageFromMatch, + FilterInput } from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; import { getUsersLink } from "../../modules/indexResource"; @@ -36,6 +39,7 @@ type Props = { usersLink: string, // context objects + classes: Object, t: string => string, history: History, location: any, @@ -45,41 +49,48 @@ type Props = { fetchUsersByLink: (link: string) => void }; -class Users extends React.Component<Props> { +const styles = { + button: { + float: "right", + marginTop: "1.25rem" + } +}; +class Users extends React.Component<Props> { componentDidMount() { const { fetchUsersByPage, usersLink, page } = this.props; fetchUsersByPage(usersLink, page, this.getQueryString()); } componentDidUpdate = (prevProps: Props) => { - const { list, page, loading, location, fetchUsersByPage, usersLink } = this.props; + const { + list, + page, + loading, + location, + fetchUsersByPage, + usersLink + } = this.props; if (list && page && !loading) { const statePage: number = list.page + 1; - if ( - page !== statePage || - prevProps.location.search !== location.search - ) { + if (page !== statePage || prevProps.location.search !== location.search) { fetchUsersByPage(usersLink, page, this.getQueryString()); } } }; render() { - const { users, loading, error, history, t } = this.props; + const { users, loading, error, t } = this.props; return ( <Page title={t("users.title")} subtitle={t("users.subtitle")} loading={loading || !users} error={error} - filter={filter => { - history.push("/users/?q=" + filter); - }} > {this.renderUserTable()} {this.renderCreateButton()} - {this.renderPageActionCreateButton()} + {this.renderPageActions()} </Page> ); } @@ -119,16 +130,24 @@ class Users extends React.Component<Props> { return null; } - renderPageActionCreateButton() { - const { canAddUsers, t } = this.props; + renderPageActions() { + const { canAddUsers, history, classes, t } = this.props; if (canAddUsers) { return ( <PageActions> - <Button - label={t("users.createButton")} - link="/users/add" - color="primary" + <FilterInput + value={this.getQueryString()} + filter={filter => { + history.push("/users/?q=" + filter); + }} /> + <div className={classNames(classes.button, "input-button control")}> + <Button + label={t("users.createButton")} + link="/users/add" + color="primary" + /> + </div> </PageActions> ); } @@ -177,4 +196,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("users")(Users)); +)(injectSheet(styles)(translate("users")(Users))); From 83edce141011c7a0447799aca5e3ea33799999d6 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 16:42:27 +0200 Subject: [PATCH 24/38] replaced type any with string --- .../packages/ui-components/src/LinkPaginator.js | 2 +- scm-ui/src/groups/containers/Groups.js | 6 +++--- scm-ui/src/groups/modules/groups.js | 2 +- scm-ui/src/repos/containers/Overview.js | 6 +++--- scm-ui/src/repos/modules/repos.js | 2 +- scm-ui/src/users/containers/Users.js | 6 +++--- scm-ui/src/users/modules/users.js | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/LinkPaginator.js b/scm-ui-components/packages/ui-components/src/LinkPaginator.js index 900107f4c1..51dc75f03c 100644 --- a/scm-ui-components/packages/ui-components/src/LinkPaginator.js +++ b/scm-ui-components/packages/ui-components/src/LinkPaginator.js @@ -7,7 +7,7 @@ import { Button } from "./buttons"; type Props = { collection: PagedCollection, page: number, - filter?: any, + filter?: string, // context props t: string => string diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index d8f2945ef5..d4fa58e0c9 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -45,7 +45,7 @@ type Props = { location: any, // dispatch functions - fetchGroupsByPage: (link: string, page: number, filter?: any) => void, + fetchGroupsByPage: (link: string, page: number, filter?: string) => void, fetchGroupsByLink: (link: string) => void }; @@ -155,7 +155,7 @@ class Groups extends React.Component<Props> { getQueryString = () => { const { location } = this.props; - return location.search ? queryString.parse(location.search).q : null; + return location.search ? queryString.parse(location.search).q : undefined; }; } @@ -182,7 +182,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchGroupsByPage: (link: string, page: number, filter?: any) => { + fetchGroupsByPage: (link: string, page: number, filter?: string) => { dispatch(fetchGroupsByPage(link, page, filter)); }, fetchGroupsByLink: (link: string) => { diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index 17d34f9335..cb3c24aa0f 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -40,7 +40,7 @@ export function fetchGroups(link: string) { return fetchGroupsByLink(link); } -export function fetchGroupsByPage(link: string, page: number, filter?: any) { +export function fetchGroupsByPage(link: string, page: number, filter?: string) { // backend start counting by 0 if (filter) { return fetchGroupsByLink( diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 0b39ee367a..4b0b7ed9b3 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -46,7 +46,7 @@ type Props = { // dispatched functions fetchRepos: string => void, - fetchReposByPage: (link: string, page: number, filter?: any) => void, + fetchReposByPage: (link: string, page: number, filter?: string) => void, fetchReposByLink: string => void }; @@ -164,7 +164,7 @@ class Overview extends React.Component<Props> { getQueryString = () => { const { location } = this.props; - return location.search ? queryString.parse(location.search).q : null; + return location.search ? queryString.parse(location.search).q : undefined; }; } @@ -191,7 +191,7 @@ const mapDispatchToProps = dispatch => { fetchRepos: (link: string) => { dispatch(fetchRepos(link)); }, - fetchReposByPage: (link: string, page: number, filter?: any) => { + fetchReposByPage: (link: string, page: number, filter?: string) => { dispatch(fetchReposByPage(link, page, filter)); }, fetchReposByLink: (link: string) => { diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index 0ce2113e15..cd48f89fea 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -46,7 +46,7 @@ export function fetchRepos(link: string) { return fetchReposByLink(link); } -export function fetchReposByPage(link: string, page: number, filter?: any) { +export function fetchReposByPage(link: string, page: number, filter?: string) { if (filter) { return fetchReposByLink( `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 0316a1ee5f..5a2bc3b5f5 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -45,7 +45,7 @@ type Props = { location: any, // dispatch functions - fetchUsersByPage: (link: string, page: number, filter?: any) => void, + fetchUsersByPage: (link: string, page: number, filter?: string) => void, fetchUsersByLink: (link: string) => void }; @@ -157,7 +157,7 @@ class Users extends React.Component<Props> { getQueryString = () => { const { location } = this.props; - return location.search ? queryString.parse(location.search).q : null; + return location.search ? queryString.parse(location.search).q : undefined; }; } @@ -184,7 +184,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchUsersByPage: (link: string, page: number, filter?: any) => { + fetchUsersByPage: (link: string, page: number, filter?: string) => { dispatch(fetchUsersByPage(link, page, filter)); }, fetchUsersByLink: (link: string) => { diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index c9b3571b02..c83a4d9b94 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -43,7 +43,7 @@ export function fetchUsers(link: string) { return fetchUsersByLink(link); } -export function fetchUsersByPage(link: string, page: number, filter?: any) { +export function fetchUsersByPage(link: string, page: number, filter?: string) { // backend start counting by 0 if (filter) { return fetchUsersByLink( From 8533dfca4be543b5b65e619f44c9a134b4579324 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 17 Apr 2019 18:59:58 +0200 Subject: [PATCH 25/38] fixed eslint failures --- scm-ui/src/modules/auth.js | 5 ++--- scm-ui/src/repos/branches/modules/branches.js | 13 ++++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index 73ba013fa1..fdc3c83c7f 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -97,14 +97,14 @@ export const logoutPending = () => { export const logoutSuccess = () => { return { - type: LOGOUT_SUCCESS, + type: LOGOUT_SUCCESS }; }; export const redirectAfterLogout = () => { return { type: LOGOUT_REDIRECT - } + }; }; export const logoutFailure = (error: Error) => { @@ -277,4 +277,3 @@ export const getLogoutFailure = (state: Object) => { export const isRedirecting = (state: Object) => { return !!stateAuth(state).redirecting; }; - diff --git a/scm-ui/src/repos/branches/modules/branches.js b/scm-ui/src/repos/branches/modules/branches.js index 7f8c45932a..39d303b74d 100644 --- a/scm-ui/src/repos/branches/modules/branches.js +++ b/scm-ui/src/repos/branches/modules/branches.js @@ -15,7 +15,7 @@ import type { import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; -import memoizeOne from 'memoize-one'; +import memoizeOne from "memoize-one"; export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES"; export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`; @@ -111,9 +111,7 @@ export function createBranch( // Selectors function collectBranches(repoState) { - return repoState.list._embedded.branches.map( - name => repoState.byName[name] - ); + return repoState.list._embedded.branches.map(name => repoState.byName[name]); } const memoizedBranchCollector = memoizeOne(collectBranches); @@ -127,7 +125,12 @@ export function getBranches(state: Object, repository: Repository) { export function getBranchCreateLink(state: Object, repository: Repository) { const repoState = getRepoState(state, repository); - if (repoState && repoState.list && repoState.list._links && repoState.list._links.create) { + if ( + repoState && + repoState.list && + repoState.list._links && + repoState.list._links.create + ) { return repoState.list._links.create.href; } } From bfd59c1bac346a9b08f5c17ce02bf3f7639727b2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 18 Apr 2019 10:02:22 +0200 Subject: [PATCH 26/38] remove unused functions --- scm-ui/src/groups/containers/Groups.js | 7 +------ scm-ui/src/repos/containers/Overview.js | 12 +----------- scm-ui/src/users/containers/Users.js | 7 +------ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index d4fa58e0c9..6fc163d546 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -8,7 +8,6 @@ import queryString from "query-string"; import type { Group, PagedCollection } from "@scm-manager/ui-types"; import { fetchGroupsByPage, - fetchGroupsByLink, getGroupsFromState, isFetchGroupsPending, getFetchGroupsFailure, @@ -45,8 +44,7 @@ type Props = { location: any, // dispatch functions - fetchGroupsByPage: (link: string, page: number, filter?: string) => void, - fetchGroupsByLink: (link: string) => void + fetchGroupsByPage: (link: string, page: number, filter?: string) => void }; const styles = { @@ -184,9 +182,6 @@ const mapDispatchToProps = dispatch => { return { fetchGroupsByPage: (link: string, page: number, filter?: string) => { dispatch(fetchGroupsByPage(link, page, filter)); - }, - fetchGroupsByLink: (link: string) => { - dispatch(fetchGroupsByLink(link)); } }; }; diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 4b0b7ed9b3..e1fb7cd05f 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -9,8 +9,6 @@ import queryString from "query-string"; import { withRouter } from "react-router-dom"; import type { RepositoryCollection } from "@scm-manager/ui-types"; import { - fetchRepos, - fetchReposByLink, fetchReposByPage, getFetchReposFailure, getRepositoryCollection, @@ -45,9 +43,7 @@ type Props = { location: any, // dispatched functions - fetchRepos: string => void, - fetchReposByPage: (link: string, page: number, filter?: string) => void, - fetchReposByLink: string => void + fetchReposByPage: (link: string, page: number, filter?: string) => void }; const styles = { @@ -188,14 +184,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchRepos: (link: string) => { - dispatch(fetchRepos(link)); - }, fetchReposByPage: (link: string, page: number, filter?: string) => { dispatch(fetchReposByPage(link, page, filter)); - }, - fetchReposByLink: (link: string) => { - dispatch(fetchReposByLink(link)); } }; }; diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 5a2bc3b5f5..9dad2c318d 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -9,7 +9,6 @@ import queryString from "query-string"; import type { User, PagedCollection } from "@scm-manager/ui-types"; import { fetchUsersByPage, - fetchUsersByLink, getUsersFromState, selectListAsCollection, isPermittedToCreateUsers, @@ -45,8 +44,7 @@ type Props = { location: any, // dispatch functions - fetchUsersByPage: (link: string, page: number, filter?: string) => void, - fetchUsersByLink: (link: string) => void + fetchUsersByPage: (link: string, page: number, filter?: string) => void }; const styles = { @@ -186,9 +184,6 @@ const mapDispatchToProps = dispatch => { return { fetchUsersByPage: (link: string, page: number, filter?: string) => { dispatch(fetchUsersByPage(link, page, filter)); - }, - fetchUsersByLink: (link: string) => { - dispatch(fetchUsersByLink(link)); } }; }; From 37a53c295bb768b76fb093c63c236999259195ff Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 18 Apr 2019 11:42:09 +0200 Subject: [PATCH 27/38] move getQueryStringFromLocation to ui-components --- scm-plugins/scm-git-plugin/package.json | 2 +- scm-plugins/scm-git-plugin/yarn.lock | 6 ++--- scm-plugins/scm-hg-plugin/package.json | 2 +- scm-plugins/scm-hg-plugin/yarn.lock | 6 ++--- scm-plugins/scm-svn-plugin/package.json | 2 +- scm-plugins/scm-svn-plugin/yarn.lock | 6 ++--- .../packages/ui-components/package.json | 3 ++- .../packages/ui-components/src/index.js | 1 + .../packages/ui-components/src/urls.js | 6 +++++ .../packages/ui-components/src/urls.test.js | 27 ++++++++++++++++++- .../packages/ui-components/yarn.lock | 18 ++++++++++--- .../packages/ui-types/package.json | 4 +-- scm-ui-components/packages/ui-types/yarn.lock | 6 ++--- scm-ui/package.json | 2 +- scm-ui/src/groups/containers/Groups.js | 23 +++++++--------- scm-ui/src/repos/containers/Overview.js | 24 +++++++---------- scm-ui/src/users/containers/Users.js | 23 +++++++--------- scm-ui/yarn.lock | 6 ++--- 18 files changed, 98 insertions(+), 69 deletions(-) diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 3b66c4f2c1..1e37876316 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -12,6 +12,6 @@ "@scm-manager/ui-extensions": "^0.1.2" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" } } diff --git a/scm-plugins/scm-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock index a45145d157..13b567b95c 100644 --- a/scm-plugins/scm-git-plugin/yarn.lock +++ b/scm-plugins/scm-git-plugin/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index d723751912..6bf1b18a07 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.1.2" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" } } diff --git a/scm-plugins/scm-hg-plugin/yarn.lock b/scm-plugins/scm-hg-plugin/yarn.lock index c1e19efede..ce15b701db 100644 --- a/scm-plugins/scm-hg-plugin/yarn.lock +++ b/scm-plugins/scm-hg-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index 10439dba66..e933145c67 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.1.2" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" } } diff --git a/scm-plugins/scm-svn-plugin/yarn.lock b/scm-plugins/scm-svn-plugin/yarn.lock index c1e19efede..ce15b701db 100644 --- a/scm-plugins/scm-svn-plugin/yarn.lock +++ b/scm-plugins/scm-svn-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 991142e338..8b02d2e732 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -14,7 +14,7 @@ "eslint-fix": "eslint src --fix" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26", + "@scm-manager/ui-bundler": "^0.0.28", "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", @@ -30,6 +30,7 @@ "@scm-manager/ui-types": "2.0.0-SNAPSHOT", "classnames": "^2.2.6", "moment": "^2.22.2", + "query-string": "5", "react": "^16.8.6", "react-dom": "^16.8.6", "react-diff-view": "^1.8.1", diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 2268f96e05..a144fbc072 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -22,6 +22,7 @@ export { default as ProtectedRoute } from "./ProtectedRoute.js"; export { default as Help } from "./Help"; export { default as HelpIcon } from "./HelpIcon"; export { default as Tooltip } from "./Tooltip"; +// TODO do we need this? getPageFromMatch is already exported by urls export { getPageFromMatch } from "./urls"; export { default as Autocomplete} from "./Autocomplete"; export { default as BranchSelector } from "./BranchSelector"; diff --git a/scm-ui-components/packages/ui-components/src/urls.js b/scm-ui-components/packages/ui-components/src/urls.js index dd8888d7a3..54b1c4294f 100644 --- a/scm-ui-components/packages/ui-components/src/urls.js +++ b/scm-ui-components/packages/ui-components/src/urls.js @@ -1,4 +1,6 @@ // @flow +import queryString from "query-string"; + export const contextPath = window.ctxPath || ""; export function withContextPath(path: string) { @@ -27,3 +29,7 @@ export function getPageFromMatch(match: any) { } return page; } + +export function getQueryStringFromLocation(location: any) { + return location.search ? queryString.parse(location.search).q : undefined; +} diff --git a/scm-ui-components/packages/ui-components/src/urls.test.js b/scm-ui-components/packages/ui-components/src/urls.test.js index 60f27510b8..7eae3a0331 100644 --- a/scm-ui-components/packages/ui-components/src/urls.test.js +++ b/scm-ui-components/packages/ui-components/src/urls.test.js @@ -1,5 +1,5 @@ // @flow -import {concat, getPageFromMatch, withEndingSlash} from "./urls"; +import { concat, getPageFromMatch, getQueryStringFromLocation, withEndingSlash } from "./urls"; describe("tests for withEndingSlash", () => { @@ -47,3 +47,28 @@ describe("tests for getPageFromMatch", () => { expect(getPageFromMatch(match)).toBe(42); }); }); + +describe("tests for getQueryStringFromLocation", () => { + + function createLocation(search: string) { + return { + search + }; + } + + it("should return the query string", () => { + const location = createLocation("?q=abc"); + expect(getQueryStringFromLocation(location)).toBe("abc"); + }); + + it("should return query string from multiple parameters", () => { + const location = createLocation("?x=a&y=b&q=abc&z=c"); + expect(getQueryStringFromLocation(location)).toBe("abc"); + }); + + it("should return undefined if q is not available", () => { + const location = createLocation("?x=a&y=b&z=c"); + expect(getQueryStringFromLocation(location)).toBeUndefined(); + }); + +}); diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index bce98a17e4..e0a601e53f 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -693,9 +693,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -6530,6 +6530,14 @@ qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +query-string@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@~0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -7631,6 +7639,10 @@ stream-throttle@^0.1.3: commander "^2.2.0" limiter "^1.0.5" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" diff --git a/scm-ui-components/packages/ui-types/package.json b/scm-ui-components/packages/ui-types/package.json index 471faf87be..27c7ff80b6 100644 --- a/scm-ui-components/packages/ui-types/package.json +++ b/scm-ui-components/packages/ui-types/package.json @@ -14,7 +14,7 @@ "check": "flow check" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" }, "browserify": { "transform": [ @@ -33,4 +33,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/scm-ui-components/packages/ui-types/yarn.lock b/scm-ui-components/packages/ui-types/yarn.lock index 2d47639005..3a67b6d419 100644 --- a/scm-ui-components/packages/ui-types/yarn.lock +++ b/scm-ui-components/packages/ui-types/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui/package.json b/scm-ui/package.json index 3c730540ae..d157b2a772 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -54,7 +54,7 @@ "pre-commit": "jest && flow && eslint src" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.27", + "@scm-manager/ui-bundler": "^0.0.28", "concat": "^1.0.3", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 6fc163d546..1d5d896a96 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -4,7 +4,6 @@ import { connect } from "react-redux"; import classNames from "classnames"; import { translate } from "react-i18next"; import type { History } from "history"; -import queryString from "query-string"; import type { Group, PagedCollection } from "@scm-manager/ui-types"; import { fetchGroupsByPage, @@ -21,7 +20,7 @@ import { Button, Notification, LinkPaginator, - getPageFromMatch + urls } from "@scm-manager/ui-components"; import { GroupTable } from "./../components/table"; import CreateGroupButton from "../components/buttons/CreateGroupButton"; @@ -56,8 +55,8 @@ const styles = { class Groups extends React.Component<Props> { componentDidMount() { - const { fetchGroupsByPage, groupLink, page } = this.props; - fetchGroupsByPage(groupLink, page, this.getQueryString()); + const { fetchGroupsByPage, groupLink, page, location } = this.props; + fetchGroupsByPage(groupLink, page, urls.getQueryStringFromLocation(location)); } componentDidUpdate = (prevProps: Props) => { @@ -72,7 +71,7 @@ class Groups extends React.Component<Props> { if (list && page && !loading) { const statePage: number = list.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchGroupsByPage(groupLink, page, this.getQueryString()); + fetchGroupsByPage(groupLink, page, urls.getQueryStringFromLocation(location)); } } }; @@ -107,13 +106,13 @@ class Groups extends React.Component<Props> { } renderPaginator = () => { - const { list, page } = this.props; + const { list, page, location } = this.props; if (list) { return ( <LinkPaginator collection={list} page={page} - filter={this.getQueryString()} + filter={urls.getQueryStringFromLocation(location)} /> ); } @@ -128,12 +127,12 @@ class Groups extends React.Component<Props> { } renderPageActions() { - const { canAddGroups, history, classes, t } = this.props; + const { canAddGroups, history, location, classes, t } = this.props; if (canAddGroups) { return ( <PageActions> <FilterInput - value={this.getQueryString()} + value={urls.getQueryStringFromLocation(location)} filter={filter => { history.push("/groups/?q=" + filter); }} @@ -151,10 +150,6 @@ class Groups extends React.Component<Props> { return null; } - getQueryString = () => { - const { location } = this.props; - return location.search ? queryString.parse(location.search).q : undefined; - }; } const mapStateToProps = (state, ownProps) => { @@ -162,7 +157,7 @@ const mapStateToProps = (state, ownProps) => { const groups = getGroupsFromState(state); const loading = isFetchGroupsPending(state); const error = getFetchGroupsFailure(state); - const page = getPageFromMatch(match); + const page = urls.getPageFromMatch(match); const canAddGroups = isPermittedToCreateGroups(state); const list = selectListAsCollection(state); const groupLink = getGroupsLink(state); diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index e1fb7cd05f..ec88ede9ad 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -5,7 +5,6 @@ import classNames from "classnames"; import injectSheet from "react-jss"; import { translate } from "react-i18next"; import type { History } from "history"; -import queryString from "query-string"; import { withRouter } from "react-router-dom"; import type { RepositoryCollection } from "@scm-manager/ui-types"; import { @@ -23,7 +22,7 @@ import { CreateButton, Notification, LinkPaginator, - getPageFromMatch + urls } from "@scm-manager/ui-components"; import RepositoryList from "../components/list"; import { getRepositoriesLink } from "../../modules/indexResource"; @@ -55,8 +54,8 @@ const styles = { class Overview extends React.Component<Props> { componentDidMount() { - const { fetchReposByPage, reposLink, page } = this.props; - fetchReposByPage(reposLink, page, this.getQueryString()); + const { fetchReposByPage, reposLink, page, location } = this.props; + fetchReposByPage(reposLink, page, urls.getQueryStringFromLocation(location)); } componentDidUpdate = (prevProps: Props) => { @@ -71,7 +70,7 @@ class Overview extends React.Component<Props> { if (collection && page && !loading) { const statePage: number = collection.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchReposByPage(reposLink, page, this.getQueryString()); + fetchReposByPage(reposLink, page, urls.getQueryStringFromLocation(location)); } } }; @@ -92,7 +91,7 @@ class Overview extends React.Component<Props> { } renderRepositoryList() { - const { collection, page, t } = this.props; + const { collection, page, location, t } = this.props; if (collection._embedded && collection._embedded.repositories.length > 0) { return ( @@ -101,7 +100,7 @@ class Overview extends React.Component<Props> { <LinkPaginator collection={collection} page={page} - filter={this.getQueryString()} + filter={urls.getQueryStringFromLocation(location)} /> </> ); @@ -135,12 +134,12 @@ class Overview extends React.Component<Props> { } renderPageActions() { - const { showCreateButton, history, classes, t } = this.props; + const { showCreateButton, history, location, classes, t } = this.props; if (showCreateButton) { return ( <PageActions> <FilterInput - value={this.getQueryString()} + value={urls.getQueryStringFromLocation(location)} filter={filter => { history.push("/repos/?q=" + filter); }} @@ -157,11 +156,6 @@ class Overview extends React.Component<Props> { } return null; } - - getQueryString = () => { - const { location } = this.props; - return location.search ? queryString.parse(location.search).q : undefined; - }; } const mapStateToProps = (state, ownProps) => { @@ -169,7 +163,7 @@ const mapStateToProps = (state, ownProps) => { const collection = getRepositoryCollection(state); const loading = isFetchReposPending(state); const error = getFetchReposFailure(state); - const page = getPageFromMatch(match); + const page = urls.getPageFromMatch(match); const showCreateButton = isAbleToCreateRepos(state); const reposLink = getRepositoriesLink(state); return { diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 9dad2c318d..bcf8af0a99 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -5,7 +5,6 @@ import classNames from "classnames"; import injectSheet from "react-jss"; import { translate } from "react-i18next"; import type { History } from "history"; -import queryString from "query-string"; import type { User, PagedCollection } from "@scm-manager/ui-types"; import { fetchUsersByPage, @@ -22,7 +21,7 @@ import { CreateButton, Notification, LinkPaginator, - getPageFromMatch, + urls, FilterInput } from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; @@ -56,8 +55,8 @@ const styles = { class Users extends React.Component<Props> { componentDidMount() { - const { fetchUsersByPage, usersLink, page } = this.props; - fetchUsersByPage(usersLink, page, this.getQueryString()); + const { fetchUsersByPage, usersLink, page, location } = this.props; + fetchUsersByPage(usersLink, page, urls.getQueryStringFromLocation(location)); } componentDidUpdate = (prevProps: Props) => { @@ -72,7 +71,7 @@ class Users extends React.Component<Props> { if (list && page && !loading) { const statePage: number = list.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchUsersByPage(usersLink, page, this.getQueryString()); + fetchUsersByPage(usersLink, page, urls.getQueryStringFromLocation(location)); } } }; @@ -107,13 +106,13 @@ class Users extends React.Component<Props> { } renderPaginator = () => { - const { list, page } = this.props; + const { list, page, location } = this.props; if (list) { return ( <LinkPaginator collection={list} page={page} - filter={this.getQueryString()} + filter={urls.getQueryStringFromLocation(location)} /> ); } @@ -129,12 +128,12 @@ class Users extends React.Component<Props> { } renderPageActions() { - const { canAddUsers, history, classes, t } = this.props; + const { canAddUsers, history, classes, location, t } = this.props; if (canAddUsers) { return ( <PageActions> <FilterInput - value={this.getQueryString()} + value={urls.getQueryStringFromLocation(location)} filter={filter => { history.push("/users/?q=" + filter); }} @@ -153,10 +152,6 @@ class Users extends React.Component<Props> { return null; } - getQueryString = () => { - const { location } = this.props; - return location.search ? queryString.parse(location.search).q : undefined; - }; } const mapStateToProps = (state, ownProps) => { @@ -164,7 +159,7 @@ const mapStateToProps = (state, ownProps) => { const users = getUsersFromState(state); const loading = isFetchUsersPending(state); const error = getFetchUsersFailure(state); - const page = getPageFromMatch(match); + const page = urls.getPageFromMatch(match); const canAddUsers = isPermittedToCreateUsers(state); const list = selectListAsCollection(state); const usersLink = getUsersLink(state); diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 3986930a35..f56b234d2c 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -698,9 +698,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.27": - version "0.0.27" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" From 8aa8af4644f5778c920bfd7cbe2dccc2171a6f7c Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 19 Apr 2019 13:18:49 +0200 Subject: [PATCH 28/38] added OverviewPageActions component for Repos, Users and Group Overview --- .../ui-components/src/OverviewPageActions.js | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 scm-ui-components/packages/ui-components/src/OverviewPageActions.js diff --git a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js new file mode 100644 index 0000000000..f4861fccc0 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js @@ -0,0 +1,63 @@ +// @flow +import React from "react"; +import type { History } from "history"; +import { withRouter } from "react-router-dom"; +import classNames from "classnames"; +import injectSheet from "react-jss"; +import { PageActions } from "./layout"; +import { FilterInput } from "./forms"; +import { Button, urls } from "./index"; + +type Props = { + showCreateButton: boolean, + link: string, + label?: string, + + // context props + classes: Object, + history: History, + location: any +}; + +const styles = { + button: { + float: "right", + marginTop: "1.25rem", + marginLeft: "1.25rem" + } +}; + +class OverviewPageActions extends React.Component<Props> { + render() { + const { history, location, link } = this.props; + return ( + <PageActions> + <FilterInput + value={urls.getQueryStringFromLocation(location)} + filter={filter => { + history.push(`/${link}/?q=${filter}`); + }} + /> + {this.renderCreateButton()} + </PageActions> + ); + } + + renderCreateButton() { + const { showCreateButton, classes, link, label } = this.props; + if (showCreateButton) { + return ( + <div className={classNames(classes.button, "input-button control")}> + <Button + label={label} //t("overview.createButton") + link={`/${link}/create`} + color="primary" + /> + </div> + ); + } + return null; + } +} + +export default injectSheet(styles)(withRouter(OverviewPageActions)); From 9ba4b05ef79dcbb6ee78b92e66dd93e9114356e6 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 19 Apr 2019 13:20:08 +0200 Subject: [PATCH 29/38] moved margin from button to inputfield to handle case in with only search field is displayed --- .../packages/ui-components/src/forms/FilterInput.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/forms/FilterInput.js b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js index 72a90255f9..dc97b4856e 100644 --- a/scm-ui-components/packages/ui-components/src/forms/FilterInput.js +++ b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js @@ -20,8 +20,7 @@ type State = { const styles = { inputField: { float: "right", - marginTop: "1.25rem", - marginRight: "1.25rem" + marginTop: "1.25rem" }, inputHeight: { height: "2.5rem" From f15e400be6e669f8b265a1205e84f164f961c154 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 19 Apr 2019 13:20:57 +0200 Subject: [PATCH 30/38] exported new OverviewPageActions component --- scm-ui-components/packages/ui-components/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index a144fbc072..954b0b0955 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -29,6 +29,7 @@ export { default as BranchSelector } from "./BranchSelector"; export { default as MarkdownView } from "./MarkdownView"; export { default as SyntaxHighlighter } from "./SyntaxHighlighter"; export { default as ErrorBoundary } from "./ErrorBoundary"; +export { default as OverviewPageActions } from "./OverviewPageActions.js"; export { apiClient } from "./apiclient.js"; export * from "./errors"; From eb346829d583b67e2087c27a4642bfdf8c9611a1 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 19 Apr 2019 13:22:17 +0200 Subject: [PATCH 31/38] prevent code duplications in overviews with new component --- scm-ui/src/groups/containers/Groups.js | 89 ++++++++---------------- scm-ui/src/repos/containers/Overview.js | 70 +++++++------------ scm-ui/src/users/containers/Users.js | 92 ++++++++----------------- 3 files changed, 78 insertions(+), 173 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 1d5d896a96..4d9a56989b 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -1,7 +1,6 @@ //@flow import React from "react"; import { connect } from "react-redux"; -import classNames from "classnames"; import { translate } from "react-i18next"; import type { History } from "history"; import type { Group, PagedCollection } from "@scm-manager/ui-types"; @@ -15,9 +14,7 @@ import { } from "../modules/groups"; import { Page, - PageActions, - FilterInput, - Button, + OverviewPageActions, Notification, LinkPaginator, urls @@ -25,7 +22,6 @@ import { import { GroupTable } from "./../components/table"; import CreateGroupButton from "../components/buttons/CreateGroupButton"; import { getGroupsLink } from "../../modules/indexResource"; -import injectSheet from "react-jss"; type Props = { groups: Group[], @@ -37,7 +33,6 @@ type Props = { groupLink: string, // context objects - classes: Object, t: string => string, history: History, location: any, @@ -46,38 +41,39 @@ type Props = { fetchGroupsByPage: (link: string, page: number, filter?: string) => void }; -const styles = { - button: { - float: "right", - marginTop: "1.25rem" - } -}; - class Groups extends React.Component<Props> { componentDidMount() { const { fetchGroupsByPage, groupLink, page, location } = this.props; - fetchGroupsByPage(groupLink, page, urls.getQueryStringFromLocation(location)); + fetchGroupsByPage( + groupLink, + page, + urls.getQueryStringFromLocation(location) + ); } componentDidUpdate = (prevProps: Props) => { const { + loading, list, page, - loading, + groupLink, location, - fetchGroupsByPage, - groupLink + fetchGroupsByPage } = this.props; if (list && page && !loading) { const statePage: number = list.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchGroupsByPage(groupLink, page, urls.getQueryStringFromLocation(location)); + fetchGroupsByPage( + groupLink, + page, + urls.getQueryStringFromLocation(location) + ); } } }; render() { - const { groups, loading, error, t } = this.props; + const { groups, loading, error, canAddGroups, t } = this.props; return ( <Page title={t("groups.title")} @@ -87,69 +83,38 @@ class Groups extends React.Component<Props> { > {this.renderGroupTable()} {this.renderCreateButton()} - {this.renderPageActions()} + <OverviewPageActions + showCreateButton={canAddGroups} + link="groups" + label={t("create-group-button.label")} + /> </Page> ); } renderGroupTable() { - const { groups, t } = this.props; + const { groups, list, page, location, t } = this.props; if (groups && groups.length > 0) { return ( <> <GroupTable groups={groups} /> - {this.renderPaginator()} + <LinkPaginator + collection={list} + page={page} + filter={urls.getQueryStringFromLocation(location)} + /> </> ); } return <Notification type="info">{t("groups.noGroups")}</Notification>; } - renderPaginator = () => { - const { list, page, location } = this.props; - if (list) { - return ( - <LinkPaginator - collection={list} - page={page} - filter={urls.getQueryStringFromLocation(location)} - /> - ); - } - return null; - }; - renderCreateButton() { if (this.props.canAddGroups) { return <CreateGroupButton />; } return null; } - - renderPageActions() { - const { canAddGroups, history, location, classes, t } = this.props; - if (canAddGroups) { - return ( - <PageActions> - <FilterInput - value={urls.getQueryStringFromLocation(location)} - filter={filter => { - history.push("/groups/?q=" + filter); - }} - /> - <div className={classNames(classes.button, "input-button control")}> - <Button - label={t("create-group-button.label")} - link="/groups/add" - color="primary" - /> - </div> - </PageActions> - ); - } - return null; - } - } const mapStateToProps = (state, ownProps) => { @@ -184,4 +149,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(injectSheet(styles)(translate("groups")(Groups))); +)(translate("groups")(Groups)); diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index ec88ede9ad..855dcdb240 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -1,8 +1,6 @@ // @flow import React from "react"; import { connect } from "react-redux"; -import classNames from "classnames"; -import injectSheet from "react-jss"; import { translate } from "react-i18next"; import type { History } from "history"; import { withRouter } from "react-router-dom"; @@ -16,9 +14,7 @@ import { } from "../modules/repos"; import { Page, - PageActions, - FilterInput, - Button, + OverviewPageActions, CreateButton, Notification, LinkPaginator, @@ -28,15 +24,14 @@ import RepositoryList from "../components/list"; import { getRepositoriesLink } from "../../modules/indexResource"; type Props = { - page: number, - collection: RepositoryCollection, loading: boolean, error: Error, showCreateButton: boolean, + collection: RepositoryCollection, + page: number, reposLink: string, // context props - classes: Object, t: string => string, history: History, location: any, @@ -45,38 +40,39 @@ type Props = { fetchReposByPage: (link: string, page: number, filter?: string) => void }; -const styles = { - button: { - float: "right", - marginTop: "1.25rem" - } -}; - class Overview extends React.Component<Props> { componentDidMount() { const { fetchReposByPage, reposLink, page, location } = this.props; - fetchReposByPage(reposLink, page, urls.getQueryStringFromLocation(location)); + fetchReposByPage( + reposLink, + page, + urls.getQueryStringFromLocation(location) + ); } componentDidUpdate = (prevProps: Props) => { const { + loading, collection, page, - loading, + reposLink, location, - fetchReposByPage, - reposLink + fetchReposByPage } = this.props; if (collection && page && !loading) { const statePage: number = collection.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchReposByPage(reposLink, page, urls.getQueryStringFromLocation(location)); + fetchReposByPage( + reposLink, + page, + urls.getQueryStringFromLocation(location) + ); } } }; render() { - const { error, loading, t } = this.props; + const { error, loading, showCreateButton, t } = this.props; return ( <Page title={t("overview.title")} @@ -85,7 +81,11 @@ class Overview extends React.Component<Props> { error={error} > {this.renderOverview()} - {this.renderPageActions()} + <OverviewPageActions + showCreateButton={showCreateButton} + link="repos" + label={t("overview.createButton")} + /> </Page> ); } @@ -132,30 +132,6 @@ class Overview extends React.Component<Props> { } return null; } - - renderPageActions() { - const { showCreateButton, history, location, classes, t } = this.props; - if (showCreateButton) { - return ( - <PageActions> - <FilterInput - value={urls.getQueryStringFromLocation(location)} - filter={filter => { - history.push("/repos/?q=" + filter); - }} - /> - <div className={classNames(classes.button, "input-button control")}> - <Button - label={t("overview.createButton")} - link="/repos/create" - color="primary" - /> - </div> - </PageActions> - ); - } - return null; - } } const mapStateToProps = (state, ownProps) => { @@ -186,4 +162,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(injectSheet(styles)(translate("repos")(withRouter(Overview)))); +)(translate("repos")(withRouter(Overview))); diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index bcf8af0a99..f52af3e301 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,8 +1,6 @@ // @flow import React from "react"; import { connect } from "react-redux"; -import classNames from "classnames"; -import injectSheet from "react-jss"; import { translate } from "react-i18next"; import type { History } from "history"; import type { User, PagedCollection } from "@scm-manager/ui-types"; @@ -16,13 +14,11 @@ import { } from "../modules/users"; import { Page, - PageActions, - Button, + OverviewPageActions, CreateButton, Notification, LinkPaginator, - urls, - FilterInput + urls } from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; import { getUsersLink } from "../../modules/indexResource"; @@ -37,7 +33,6 @@ type Props = { usersLink: string, // context objects - classes: Object, t: string => string, history: History, location: any, @@ -46,38 +41,39 @@ type Props = { fetchUsersByPage: (link: string, page: number, filter?: string) => void }; -const styles = { - button: { - float: "right", - marginTop: "1.25rem" - } -}; - class Users extends React.Component<Props> { componentDidMount() { const { fetchUsersByPage, usersLink, page, location } = this.props; - fetchUsersByPage(usersLink, page, urls.getQueryStringFromLocation(location)); + fetchUsersByPage( + usersLink, + page, + urls.getQueryStringFromLocation(location) + ); } componentDidUpdate = (prevProps: Props) => { const { + loading, list, page, - loading, + usersLink, location, - fetchUsersByPage, - usersLink + fetchUsersByPage } = this.props; if (list && page && !loading) { const statePage: number = list.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchUsersByPage(usersLink, page, urls.getQueryStringFromLocation(location)); + fetchUsersByPage( + usersLink, + page, + urls.getQueryStringFromLocation(location) + ); } } }; render() { - const { users, loading, error, t } = this.props; + const { users, loading, error, canAddUsers, t } = this.props; return ( <Page title={t("users.title")} @@ -87,38 +83,32 @@ class Users extends React.Component<Props> { > {this.renderUserTable()} {this.renderCreateButton()} - {this.renderPageActions()} + <OverviewPageActions + showCreateButton={canAddUsers} + link="users" + label={t("users.createButton")} + /> </Page> ); } renderUserTable() { - const { users, t } = this.props; + const { users, list, page, location, t } = this.props; if (users && users.length > 0) { return ( <> <UserTable users={users} /> - {this.renderPaginator()} + <LinkPaginator + collection={list} + page={page} + filter={urls.getQueryStringFromLocation(location)} + /> </> ); } return <Notification type="info">{t("users.noUsers")}</Notification>; } - renderPaginator = () => { - const { list, page, location } = this.props; - if (list) { - return ( - <LinkPaginator - collection={list} - page={page} - filter={urls.getQueryStringFromLocation(location)} - /> - ); - } - return null; - }; - renderCreateButton() { const { canAddUsers, t } = this.props; if (canAddUsers) { @@ -126,32 +116,6 @@ class Users extends React.Component<Props> { } return null; } - - renderPageActions() { - const { canAddUsers, history, classes, location, t } = this.props; - if (canAddUsers) { - return ( - <PageActions> - <FilterInput - value={urls.getQueryStringFromLocation(location)} - filter={filter => { - history.push("/users/?q=" + filter); - }} - /> - <div className={classNames(classes.button, "input-button control")}> - <Button - label={t("users.createButton")} - link="/users/add" - color="primary" - /> - </div> - </PageActions> - ); - } - - return null; - } - } const mapStateToProps = (state, ownProps) => { @@ -186,4 +150,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(injectSheet(styles)(translate("users")(Users))); +)(translate("users")(Users)); From f209ca665d4dd499e038da4bbb85fdbd241a2d3c Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 19 Apr 2019 13:23:57 +0200 Subject: [PATCH 32/38] moved check from calling component to paginator --- .../ui-components/src/LinkPaginator.js | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/LinkPaginator.js b/scm-ui-components/packages/ui-components/src/LinkPaginator.js index 51dc75f03c..a662272b11 100644 --- a/scm-ui-components/packages/ui-components/src/LinkPaginator.js +++ b/scm-ui-components/packages/ui-components/src/LinkPaginator.js @@ -123,21 +123,25 @@ class LinkPaginator extends React.Component<Props> { } render() { - const { t } = this.props; - return ( - <nav className="pagination is-centered" aria-label="pagination"> - {this.renderPreviousButton( - "pagination-previous", - t("paginator.previous") - )} - <ul className="pagination-list"> - {this.pageLinks().map((link, index) => { - return <li key={index}>{link}</li>; - })} - </ul> - {this.renderNextButton("pagination-next", t("paginator.next"))} - </nav> - ); + const { collection, t } = this.props; + + if(collection) { + return ( + <nav className="pagination is-centered" aria-label="pagination"> + {this.renderPreviousButton( + "pagination-previous", + t("paginator.previous") + )} + <ul className="pagination-list"> + {this.pageLinks().map((link, index) => { + return <li key={index}>{link}</li>; + })} + </ul> + {this.renderNextButton("pagination-next", t("paginator.next"))} + </nav> + ); + } + return null; } } From ef35a2050ceba42ccc8f8d9e4d165769c3e5e684 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Fri, 19 Apr 2019 13:27:12 +0200 Subject: [PATCH 33/38] added check for new component to ensure correct representation, in my opinion still unpleasant --- .../packages/ui-components/src/layout/Page.js | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index c6f5d132a5..6c44112074 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -7,6 +7,7 @@ import ErrorNotification from "./../ErrorNotification"; import Title from "./Title"; import Subtitle from "./Subtitle"; import PageActions from "./PageActions"; +import OverviewPageActions from "../OverviewPageActions"; import ErrorBoundary from "../ErrorBoundary"; type Props = { @@ -29,7 +30,6 @@ const styles = { }; class Page extends React.Component<Props> { - render() { const { error } = this.props; return ( @@ -51,17 +51,21 @@ class Page extends React.Component<Props> { let pageActions = null; let pageActionsExists = false; React.Children.forEach(children, child => { - if (child && child.type.name === PageActions.name && !error) { - pageActions = ( - <div - className={classNames( - classes.actions, - "column is-three-fifths is-mobile-action-spacing" - )} - > - {child} - </div> - ); + if (child && !error) { + if ( + child.type.name === PageActions.name || + child.type.name === OverviewPageActions.name + ) + pageActions = ( + <div + className={classNames( + classes.actions, + "column is-three-fifths is-mobile-action-spacing" + )} + > + {child} + </div> + ); pageActionsExists = true; } }); @@ -95,8 +99,13 @@ class Page extends React.Component<Props> { let content = []; React.Children.forEach(children, child => { - if (child && child.type.name !== PageActions.name) { - content.push(child); + if (child) { + if ( + child.type.name !== PageActions.name && + child.type.name !== OverviewPageActions.name + ) { + content.push(child); + } } }); return content; From 5a4fc6a96577f62b33bd4c51a7b77ad1ae33f0e1 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Tue, 23 Apr 2019 15:55:26 +0200 Subject: [PATCH 34/38] modified transferred link to include add/create information --- scm-ui/src/groups/containers/Groups.js | 2 +- scm-ui/src/repos/containers/Overview.js | 2 +- scm-ui/src/users/containers/Users.js | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 4d9a56989b..158440cd96 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -85,7 +85,7 @@ class Groups extends React.Component<Props> { {this.renderCreateButton()} <OverviewPageActions showCreateButton={canAddGroups} - link="groups" + link="groups/add" label={t("create-group-button.label")} /> </Page> diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 855dcdb240..adf25a2325 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -83,7 +83,7 @@ class Overview extends React.Component<Props> { {this.renderOverview()} <OverviewPageActions showCreateButton={showCreateButton} - link="repos" + link="repos/create" label={t("overview.createButton")} /> </Page> diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index f52af3e301..c2c73c2517 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -15,12 +15,12 @@ import { import { Page, OverviewPageActions, - CreateButton, Notification, LinkPaginator, urls } from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; +import CreateUserButton from "../components/buttons/CreateUserButton"; import { getUsersLink } from "../../modules/indexResource"; type Props = { @@ -85,7 +85,7 @@ class Users extends React.Component<Props> { {this.renderCreateButton()} <OverviewPageActions showCreateButton={canAddUsers} - link="users" + link="users/add" label={t("users.createButton")} /> </Page> @@ -110,9 +110,8 @@ class Users extends React.Component<Props> { } renderCreateButton() { - const { canAddUsers, t } = this.props; - if (canAddUsers) { - return <CreateButton label={t("users.createButton")} link="/users/add" />; + if (this.props.canAddUsers) { + return <CreateUserButton />; } return null; } From d56dc22efc51bb0f3ddc5b00efa75c47f53a1681 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Tue, 23 Apr 2019 15:57:23 +0200 Subject: [PATCH 35/38] modified transferred link to include add/create information, added new CreateUserButton component --- .../ui-components/src/OverviewPageActions.js | 9 +++------ .../components/buttons/CreateUserButton.js | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 scm-ui/src/users/components/buttons/CreateUserButton.js diff --git a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js index f4861fccc0..96c0be72f8 100644 --- a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js +++ b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js @@ -30,12 +30,13 @@ const styles = { class OverviewPageActions extends React.Component<Props> { render() { const { history, location, link } = this.props; + let directory = link.substring(0, link.indexOf("/")); return ( <PageActions> <FilterInput value={urls.getQueryStringFromLocation(location)} filter={filter => { - history.push(`/${link}/?q=${filter}`); + history.push(`/${directory}/?q=${filter}`); }} /> {this.renderCreateButton()} @@ -48,11 +49,7 @@ class OverviewPageActions extends React.Component<Props> { if (showCreateButton) { return ( <div className={classNames(classes.button, "input-button control")}> - <Button - label={label} //t("overview.createButton") - link={`/${link}/create`} - color="primary" - /> + <Button label={label} link={`/${link}`} color="primary" /> </div> ); } diff --git a/scm-ui/src/users/components/buttons/CreateUserButton.js b/scm-ui/src/users/components/buttons/CreateUserButton.js new file mode 100644 index 0000000000..36ab9c137d --- /dev/null +++ b/scm-ui/src/users/components/buttons/CreateUserButton.js @@ -0,0 +1,19 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import { CreateButton } from "@scm-manager/ui-components"; + +type Props = { + t: string => string +}; + +class CreateUserButton extends React.Component<Props> { + render() { + const { t } = this.props; + return ( + <CreateButton label={t("users.createButton")} link="/users/add" /> + ); + } +} + +export default translate("users")(CreateUserButton); From 7450c9bea296c168e7f8b943e415f1b633bcc137 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 24 Apr 2019 09:46:22 +0200 Subject: [PATCH 36/38] pulled out PageActions from OverviewPageActions because otherwise you can not check for name of OverviewPageActions because the component exported injectSheet -> jss simplified CreateUser/GroupButton by not being used as external component --- .../ui-components/src/OverviewPageActions.js | 4 +-- .../packages/ui-components/src/layout/Page.js | 11 ++------ .../components/buttons/CreateGroupButton.js | 19 ------------- scm-ui/src/groups/containers/Groups.js | 27 ++++++++++++------- scm-ui/src/repos/containers/Overview.js | 13 +++++---- .../components/buttons/CreateUserButton.js | 19 ------------- scm-ui/src/users/containers/Users.js | 22 ++++++++------- 7 files changed, 43 insertions(+), 72 deletions(-) delete mode 100644 scm-ui/src/groups/components/buttons/CreateGroupButton.js delete mode 100644 scm-ui/src/users/components/buttons/CreateUserButton.js diff --git a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js index 96c0be72f8..972560fb1a 100644 --- a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js +++ b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js @@ -32,7 +32,7 @@ class OverviewPageActions extends React.Component<Props> { const { history, location, link } = this.props; let directory = link.substring(0, link.indexOf("/")); return ( - <PageActions> + <> <FilterInput value={urls.getQueryStringFromLocation(location)} filter={filter => { @@ -40,7 +40,7 @@ class OverviewPageActions extends React.Component<Props> { }} /> {this.renderCreateButton()} - </PageActions> + </> ); } diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 6c44112074..ef4363cd8f 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -7,7 +7,6 @@ import ErrorNotification from "./../ErrorNotification"; import Title from "./Title"; import Subtitle from "./Subtitle"; import PageActions from "./PageActions"; -import OverviewPageActions from "../OverviewPageActions"; import ErrorBoundary from "../ErrorBoundary"; type Props = { @@ -52,10 +51,7 @@ class Page extends React.Component<Props> { let pageActionsExists = false; React.Children.forEach(children, child => { if (child && !error) { - if ( - child.type.name === PageActions.name || - child.type.name === OverviewPageActions.name - ) + if (child.type.name === PageActions.name) pageActions = ( <div className={classNames( @@ -100,10 +96,7 @@ class Page extends React.Component<Props> { let content = []; React.Children.forEach(children, child => { if (child) { - if ( - child.type.name !== PageActions.name && - child.type.name !== OverviewPageActions.name - ) { + if (child.type.name !== PageActions.name) { content.push(child); } } diff --git a/scm-ui/src/groups/components/buttons/CreateGroupButton.js b/scm-ui/src/groups/components/buttons/CreateGroupButton.js deleted file mode 100644 index 73e0eddd63..0000000000 --- a/scm-ui/src/groups/components/buttons/CreateGroupButton.js +++ /dev/null @@ -1,19 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { CreateButton } from "@scm-manager/ui-components"; - -type Props = { - t: string => string -}; - -class CreateGroupButton extends React.Component<Props> { - render() { - const { t } = this.props; - return ( - <CreateButton label={t("create-group-button.label")} link="/groups/add" /> - ); - } -} - -export default translate("groups")(CreateGroupButton); diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 158440cd96..535cc0867b 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -14,13 +14,14 @@ import { } from "../modules/groups"; import { Page, + PageActions, OverviewPageActions, Notification, LinkPaginator, - urls + urls, + CreateButton } from "@scm-manager/ui-components"; import { GroupTable } from "./../components/table"; -import CreateGroupButton from "../components/buttons/CreateGroupButton"; import { getGroupsLink } from "../../modules/indexResource"; type Props = { @@ -83,11 +84,13 @@ class Groups extends React.Component<Props> { > {this.renderGroupTable()} {this.renderCreateButton()} - <OverviewPageActions - showCreateButton={canAddGroups} - link="groups/add" - label={t("create-group-button.label")} - /> + <PageActions> + <OverviewPageActions + showCreateButton={canAddGroups} + link="groups/add" + label={t("create-group-button.label")} + /> + </PageActions> </Page> ); } @@ -110,8 +113,14 @@ class Groups extends React.Component<Props> { } renderCreateButton() { - if (this.props.canAddGroups) { - return <CreateGroupButton />; + const { canAddGroups, t } = this.props; + if (canAddGroups) { + return ( + <CreateButton + label={t("create-group-button.label")} + link="/groups/add" + /> + ); } return null; } diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index adf25a2325..982b2c5d1e 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -14,6 +14,7 @@ import { } from "../modules/repos"; import { Page, + PageActions, OverviewPageActions, CreateButton, Notification, @@ -81,11 +82,13 @@ class Overview extends React.Component<Props> { error={error} > {this.renderOverview()} - <OverviewPageActions - showCreateButton={showCreateButton} - link="repos/create" - label={t("overview.createButton")} - /> + <PageActions> + <OverviewPageActions + showCreateButton={showCreateButton} + link="repos/create" + label={t("overview.createButton")} + /> + </PageActions> </Page> ); } 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 36ab9c137d..0000000000 --- a/scm-ui/src/users/components/buttons/CreateUserButton.js +++ /dev/null @@ -1,19 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { CreateButton } from "@scm-manager/ui-components"; - -type Props = { - t: string => string -}; - -class CreateUserButton extends React.Component<Props> { - render() { - const { t } = this.props; - return ( - <CreateButton label={t("users.createButton")} link="/users/add" /> - ); - } -} - -export default translate("users")(CreateUserButton); diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index c2c73c2517..cc9d2f5d16 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -14,13 +14,14 @@ import { } from "../modules/users"; import { Page, + PageActions, OverviewPageActions, Notification, LinkPaginator, - urls + urls, + CreateButton } from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; -import CreateUserButton from "../components/buttons/CreateUserButton"; import { getUsersLink } from "../../modules/indexResource"; type Props = { @@ -83,11 +84,13 @@ class Users extends React.Component<Props> { > {this.renderUserTable()} {this.renderCreateButton()} - <OverviewPageActions - showCreateButton={canAddUsers} - link="users/add" - label={t("users.createButton")} - /> + <PageActions> + <OverviewPageActions + showCreateButton={canAddUsers} + link="users/add" + label={t("users.createButton")} + /> + </PageActions> </Page> ); } @@ -110,8 +113,9 @@ class Users extends React.Component<Props> { } renderCreateButton() { - if (this.props.canAddUsers) { - return <CreateUserButton />; + const { canAddUsers, t } = this.props; + if (canAddUsers) { + return <CreateButton label={t("users.createButton")} link="/users/add" />; } return null; } From 3102af3029de205dff92c8c5f85d5e9eae455475 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 24 Apr 2019 09:59:29 +0200 Subject: [PATCH 37/38] renamed add to create --- .../ui-components/src/OverviewPageActions.js | 6 ++---- scm-ui/public/locales/de/users.json | 2 +- scm-ui/public/locales/en/users.json | 2 +- scm-ui/src/containers/Main.js | 12 ++++++------ .../containers/{AddGroup.js => CreateGroup.js} | 4 ++-- scm-ui/src/groups/containers/Groups.js | 4 ++-- scm-ui/src/repos/containers/Overview.js | 2 +- .../users/containers/{AddUser.js => CreateUser.js} | 8 ++++---- scm-ui/src/users/containers/Users.js | 6 ++++-- 9 files changed, 23 insertions(+), 23 deletions(-) rename scm-ui/src/groups/containers/{AddGroup.js => CreateGroup.js} (96%) rename scm-ui/src/users/containers/{AddUser.js => CreateUser.js} (91%) diff --git a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js index 972560fb1a..84d0713752 100644 --- a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js +++ b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js @@ -4,7 +4,6 @@ import type { History } from "history"; import { withRouter } from "react-router-dom"; import classNames from "classnames"; import injectSheet from "react-jss"; -import { PageActions } from "./layout"; import { FilterInput } from "./forms"; import { Button, urls } from "./index"; @@ -30,13 +29,12 @@ const styles = { class OverviewPageActions extends React.Component<Props> { render() { const { history, location, link } = this.props; - let directory = link.substring(0, link.indexOf("/")); return ( <> <FilterInput value={urls.getQueryStringFromLocation(location)} filter={filter => { - history.push(`/${directory}/?q=${filter}`); + history.push(`/${link}/?q=${filter}`); }} /> {this.renderCreateButton()} @@ -49,7 +47,7 @@ class OverviewPageActions extends React.Component<Props> { if (showCreateButton) { return ( <div className={classNames(classes.button, "input-button control")}> - <Button label={label} link={`/${link}`} color="primary" /> + <Button label={label} link={`/${link}/create`} color="primary" /> </div> ); } diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json index 05b5cf142f..37416c1e4f 100644 --- a/scm-ui/public/locales/de/users.json +++ b/scm-ui/public/locales/de/users.json @@ -39,7 +39,7 @@ "setPermissionsNavLink": "Berechtigungen" } }, - "addUser": { + "createUser": { "title": "Benutzer erstellen", "subtitle": "Erstellen eines neuen Benutzers" }, diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index e6fd822ead..d188f6221b 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -39,7 +39,7 @@ "setPermissionsNavLink": "Permissions" } }, - "addUser": { + "createUser": { "title": "Create User", "subtitle": "Create a new user" }, diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 48debaf5b8..42a6c54590 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -12,14 +12,14 @@ import Logout from "../containers/Logout"; import { ProtectedRoute } from "@scm-manager/ui-components"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; -import AddUser from "../users/containers/AddUser"; +import CreateUser from "../users/containers/CreateUser"; import SingleUser from "../users/containers/SingleUser"; import RepositoryRoot from "../repos/containers/RepositoryRoot"; import Create from "../repos/containers/Create"; import Groups from "../groups/containers/Groups"; import SingleGroup from "../groups/containers/SingleGroup"; -import AddGroup from "../groups/containers/AddGroup"; +import CreateGroup from "../groups/containers/CreateGroup"; import Config from "../config/containers/Config"; import Profile from "./Profile"; @@ -74,8 +74,8 @@ class Main extends React.Component<Props> { /> <ProtectedRoute authenticated={authenticated} - path="/users/add" - component={AddUser} + path="/users/create" + component={CreateUser} /> <ProtectedRoute exact @@ -102,8 +102,8 @@ class Main extends React.Component<Props> { /> <ProtectedRoute authenticated={authenticated} - path="/groups/add" - component={AddGroup} + path="/groups/create" + component={CreateGroup} /> <ProtectedRoute exact diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/CreateGroup.js similarity index 96% rename from scm-ui/src/groups/containers/AddGroup.js rename to scm-ui/src/groups/containers/CreateGroup.js index 69c1171ea9..40c2a6cb8a 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/CreateGroup.js @@ -31,7 +31,7 @@ type Props = { type State = {}; -class AddGroup extends React.Component<Props, State> { +class CreateGroup extends React.Component<Props, State> { componentDidMount() { this.props.resetForm(); } @@ -104,4 +104,4 @@ const mapStateToProps = state => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("groups")(AddGroup)); +)(translate("groups")(CreateGroup)); diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 535cc0867b..60672bf830 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -87,7 +87,7 @@ class Groups extends React.Component<Props> { <PageActions> <OverviewPageActions showCreateButton={canAddGroups} - link="groups/add" + link="groups" label={t("create-group-button.label")} /> </PageActions> @@ -118,7 +118,7 @@ class Groups extends React.Component<Props> { return ( <CreateButton label={t("create-group-button.label")} - link="/groups/add" + link="/groups/create" /> ); } diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 982b2c5d1e..9dcaf99b72 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -85,7 +85,7 @@ class Overview extends React.Component<Props> { <PageActions> <OverviewPageActions showCreateButton={showCreateButton} - link="repos/create" + link="repos" label={t("overview.createButton")} /> </PageActions> diff --git a/scm-ui/src/users/containers/AddUser.js b/scm-ui/src/users/containers/CreateUser.js similarity index 91% rename from scm-ui/src/users/containers/AddUser.js rename to scm-ui/src/users/containers/CreateUser.js index 069df04187..dd2a0dac05 100644 --- a/scm-ui/src/users/containers/AddUser.js +++ b/scm-ui/src/users/containers/CreateUser.js @@ -28,7 +28,7 @@ type Props = { history: History }; -class AddUser extends React.Component<Props> { +class CreateUser extends React.Component<Props> { componentDidMount() { this.props.resetForm(); } @@ -49,8 +49,8 @@ class AddUser extends React.Component<Props> { return ( <Page - title={t("addUser.title")} - subtitle={t("addUser.subtitle")} + title={t("createUser.title")} + subtitle={t("createUser.subtitle")} error={error} showContentOnError={true} > @@ -88,4 +88,4 @@ const mapStateToProps = (state, ownProps) => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("users")(AddUser)); +)(translate("users")(CreateUser)); diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index cc9d2f5d16..7fd279bc45 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -87,7 +87,7 @@ class Users extends React.Component<Props> { <PageActions> <OverviewPageActions showCreateButton={canAddUsers} - link="users/add" + link="users" label={t("users.createButton")} /> </PageActions> @@ -115,7 +115,9 @@ class Users extends React.Component<Props> { renderCreateButton() { const { canAddUsers, t } = this.props; if (canAddUsers) { - return <CreateButton label={t("users.createButton")} link="/users/add" />; + return ( + <CreateButton label={t("users.createButton")} link="/users/create" /> + ); } return null; } From e16bfeeb96d98df44074e7f84be8d095b0c4a497 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Wed, 24 Apr 2019 11:51:57 +0000 Subject: [PATCH 38/38] Close branch feature/search