From db540f5f0253d3ca3490e82bd65d3e92dbbd1733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 17 Feb 2020 16:48:14 +0100 Subject: [PATCH 001/111] Add limit parameter --- .../repository/api/BrowseCommandBuilder.java | 12 ++++++++++ .../repository/spi/BrowseCommandRequest.java | 24 +++++++++++++++++++ .../spi/FileBaseCommandRequest.java | 10 ++++++++ .../repository/spi/GitBrowseCommandTest.java | 13 ++++++++++ 4 files changed, 59 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index 563557f0c1..53281b52b8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -300,6 +300,18 @@ public final class BrowseCommandBuilder return this; } + /** + * Limit the number of result files to limit entries. + * + * @param limit The maximal number of files this request shall return. + * + * @since 2.0.0 + */ + public BrowseCommandBuilder setLimit(int limit) { + request.setLimit(limit); + return this; + } + private void updateCache(BrowserResult updatedResult) { if (!disableCache) { CacheKey key = new CacheKey(repository, request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 9c23fe93f2..c856421855 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -191,6 +191,17 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest this.recursive = recursive; } + /** + * Limit the number of result files to limit entries. + * + * @param limit The maximal number of files this request shall return. + * + * @since 2.0.0 + */ + public void setLimit(int limit) { + this.limit = limit; + } + //~--- get methods ---------------------------------------------------------- /** @@ -232,6 +243,15 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest return recursive; } + /** + * Returns the limit for the number of result files. + * + * @since 2.0.0 + */ + public int getLimit() { + return limit; + } + public void updateCache(BrowserResult update) { if (updater != null) { updater.accept(update); @@ -249,6 +269,10 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** browse file objects recursive */ private boolean recursive = false; + + /** Limit the number of result files to limit entries. */ + private int limit = 1000; + // WARNING / TODO: This field creates a reverse channel from the implementation to the API. This will break // whenever the API runs in a different process than the SPI (for example to run explicit hosts for git repositories). private final transient Consumer updater; diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 9f563345fd..0a2192897a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,6 +147,10 @@ public abstract class FileBaseCommandRequest this.revision = revision; } + public void setLimit(int limit) { + this.limit = limit; + } + //~--- get methods ---------------------------------------------------------- /** @@ -171,6 +175,10 @@ public abstract class FileBaseCommandRequest return revision; } + public int getLimit() { + return limit; + } + //~--- methods -------------------------------------------------------------- /** @@ -208,4 +216,6 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; + + private int limit; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index faa8f0c2fa..e45ad0d04b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -236,6 +236,19 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { .containsExactly(of(42L)); } + @Test + public void testBrowseLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + FileObject root = createCommand() + .getBrowserResult(request).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + assertThat(foList).hasSize(2); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 8a1a43fcc50fd53f657e00046670a5ea6b4179c4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 08:08:09 +0100 Subject: [PATCH 002/111] Document parameter --- .../java/sonia/scm/repository/api/BrowseCommandBuilder.java | 5 ++++- .../java/sonia/scm/repository/spi/BrowseCommandRequest.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index 53281b52b8..e088248762 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -301,7 +301,10 @@ public final class BrowseCommandBuilder } /** - * Limit the number of result files to limit entries. + * Limit the number of result files to limit entries. By default this is set to + * {@value BrowseCommandRequest#DEFAULT_REQUEST_LIMIT}. Be aware that this parameter can have + * severe performance implications. Reading a repository with thousands of files in one folder + * can generate a huge load for a longer time. * * @param limit The maximal number of files this request shall return. * diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index c856421855..e7181829a1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -49,7 +49,8 @@ import java.util.function.Consumer; public final class BrowseCommandRequest extends FileBaseCommandRequest { - /** Field description */ + public static final int DEFAULT_REQUEST_LIMIT = 1000; + private static final long serialVersionUID = 7956624623516803183L; public BrowseCommandRequest() { @@ -271,7 +272,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** Limit the number of result files to limit entries. */ - private int limit = 1000; + private int limit = DEFAULT_REQUEST_LIMIT; // WARNING / TODO: This field creates a reverse channel from the implementation to the API. This will break // whenever the API runs in a different process than the SPI (for example to run explicit hosts for git repositories). From f68423a5d80bb7728914c5e328328e1ee32c3276 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 08:22:36 +0100 Subject: [PATCH 003/111] Implement request limit for git --- .../java/sonia/scm/repository/spi/GitBrowseCommand.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index e8ef5a7a33..5158a74ecb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -110,6 +110,8 @@ public class GitBrowseCommand extends AbstractGitCommand private BrowserResult browserResult; + private int resultCount = 0; + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { super(context, repository); this.lfsBlobStoreFactory = lfsBlobStoreFactory; @@ -251,7 +253,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next()) + while (treeWalk.next() && resultCount < request.getLimit()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -262,6 +264,8 @@ public class GitBrowseCommand extends AbstractGitCommand files.add(fileObject); + ++resultCount; + if (request.isRecursive() && fileObject.isDirectory()) { treeWalk.enterSubtree(); FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); From 67e58209cfbc45989e8956659d58683ef2b6fbdb Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 09:57:57 +0100 Subject: [PATCH 004/111] Implement proceed from for git --- .../repository/api/BrowseCommandBuilder.java | 12 +++++++++++ .../repository/spi/BrowseCommandRequest.java | 21 +++++++++++++++++++ .../scm/repository/spi/GitBrowseCommand.java | 6 ++++-- .../repository/spi/GitBrowseCommandTest.java | 14 +++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index e088248762..7862fdb64a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -315,6 +315,18 @@ public final class BrowseCommandBuilder return this; } + /** + * Proceed the list from the given number on (zero based). + * + * @param proceedFrom The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. + * @since 2.0.0 + */ + public BrowseCommandBuilder setProceedFrom(int proceedFrom) { + request.setProceedFrom(proceedFrom); + return this; + } + private void updateCache(BrowserResult updatedResult) { if (!disableCache) { CacheKey key = new CacheKey(repository, request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index e7181829a1..3dbe71f322 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -52,6 +52,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public static final int DEFAULT_REQUEST_LIMIT = 1000; private static final long serialVersionUID = 7956624623516803183L; + private int proceedFrom; public BrowseCommandRequest() { this(null); @@ -203,6 +204,17 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest this.limit = limit; } + /** + * Proceed the list from the given number on (zero based). + * + * @param proceedFrom The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. + * @since 2.0.0 + */ + public void setProceedFrom(int proceedFrom) { + this.proceedFrom = proceedFrom; + } + //~--- get methods ---------------------------------------------------------- /** @@ -253,6 +265,15 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest return limit; } + /** + * The number of the entry, the result start with. All preceding entries will be omitted. + * + * @since 2.0.0 + */ + public int getProceedFrom() { + return proceedFrom; + } + public void updateCache(BrowserResult update) { if (updater != null) { updater.accept(update); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 5158a74ecb..79aeb51342 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -253,7 +253,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next() && resultCount < request.getLimit()) + while (treeWalk.next() && resultCount < request.getLimit() + request.getProceedFrom()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -262,7 +262,9 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - files.add(fileObject); + if (resultCount >= request.getProceedFrom()) { + files.add(fileObject); + } ++resultCount; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index e45ad0d04b..78aa12e8e0 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -249,6 +249,20 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList).hasSize(2); } + @Test + public void testBrowseProceedFrom() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + request.setProceedFrom(2); + FileObject root = createCommand() + .getBrowserResult(request).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").contains("c", "f.txt"); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 9e7fd52f9de15ddbacb9f9efc94dd54d7eef3ec4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 10:47:20 +0100 Subject: [PATCH 005/111] Implement request limit for hg --- .../scm/repository/spi/HgBrowseCommand.java | 3 + .../spi/javahg/HgFileviewCommand.java | 30 +++++++++- .../resources/sonia/scm/hg/ext/fileview.py | 57 ++++++++++++------- .../repository/spi/HgBrowseCommandTest.java | 28 ++++++++- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index b3e8e89a5f..6f2e519d3d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -101,6 +101,9 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand cmd.disableSubRepositoryDetection(); } + cmd.setLimit(request.getLimit()); + cmd.setProceedFrom(request.getProceedFrom()); + FileObject file = cmd.execute(); return new BrowserResult(c == null? "tip": c.getNode(), revision, file); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 4d5d5e8646..3b74f6b9cb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -141,6 +141,35 @@ public class HgFileviewCommand extends AbstractCommand return this; } + /** + * Limit the number of result files to limit entries. + * + * @param limit The maximal number of files this request shall return. + * + * @return {@code this} + * @since 2.0.0 + */ + public HgFileviewCommand setLimit(int limit) { + cmdAppend("-l", limit); + + return this; + } + + /** + * Proceed the list from the given number on (zero based). + * + * @param proceedFrom The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. + * + * @return {@code this} + * @since 2.0.0 + */ + public HgFileviewCommand setProceedFrom(int proceedFrom) { + cmdAppend("-f", proceedFrom); + + return this; + } + /** * Executes the mercurial command and parses the output. * @@ -294,5 +323,4 @@ public class HgFileviewCommand extends AbstractCommand { return HgFileviewExtension.NAME; } - } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 1871200389..ec57192666 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -197,37 +197,43 @@ def collect_sub_repositories(revCtx): class File_Printer: - def __init__(self, ui, repo, revCtx, disableLastCommit, transport): + def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, proceedFrom): self.ui = ui self.repo = repo self.revCtx = revCtx self.disableLastCommit = disableLastCommit self.transport = transport + self.result_count = -1 + self.limit = limit + self.proceedFrom = proceedFrom def print_directory(self, path): - format = '%s/\n' - if self.transport: - format = 'd%s/\0' - self.ui.write( format % path) + if self.shouldPrintResult(): + format = '%s/\n' + if self.transport: + format = 'd%s/\0' + self.ui.write( format % path) def print_file(self, path): - file = self.revCtx[path] - date = '0 0' - description = 'n/a' - if not self.disableLastCommit: - linkrev = self.repo[file.linkrev()] - date = '%d %d' % _parsedate(linkrev.date()) - description = linkrev.description() - format = '%s %i %s %s\n' - if self.transport: - format = 'f%s\n%i %s %s\0' - self.ui.write( format % (file.path(), file.size(), date, description) ) + if self.shouldPrintResult(): + file = self.revCtx[path] + date = '0 0' + description = 'n/a' + if not self.disableLastCommit: + linkrev = self.repo[file.linkrev()] + date = '%d %d' % _parsedate(linkrev.date()) + description = linkrev.description() + format = '%s %i %s %s\n' + if self.transport: + format = 'f%s\n%i %s %s\0' + self.ui.write( format % (file.path(), file.size(), date, description) ) def print_sub_repository(self, path, subrepo): - format = '%s/ %s %s\n' - if self.transport: - format = 's%s/\n%s %s\0' - self.ui.write( format % (path, subrepo.revision, subrepo.url)) + if self.shouldPrintResult(): + format = '%s/ %s %s\n' + if self.transport: + format = 's%s/\n%s %s\0' + self.ui.write( format % (path, subrepo.revision, subrepo.url)) def visit(self, file): if file.sub_repository: @@ -237,6 +243,13 @@ class File_Printer: else: self.print_file(file.path) + def shouldPrintResult(self): + # The first result is the selected path (or root if not specified). This + # always has to be printed. Therefore we start counting with -1. + self.result_count += 1 + return self.result_count == 0 or self.proceedFrom < self.result_count <= self.limit + self.proceedFrom + + class File_Viewer: def __init__(self, revCtx, visitor): self.revCtx = revCtx @@ -271,13 +284,15 @@ class File_Viewer: ('d', 'disableLastCommit', False, 'disables last commit description and date'), ('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'), ('t', 'transport', False, 'format the output for command server'), + ('l', 'limit', 1000, 'limit the number of results'), + ('f', 'proceedFrom', 0, 'proceed from the given result number (zero based)'), ]) def fileview(ui, repo, **opts): revCtx = scmutil.revsingle(repo, opts["revision"]) subrepos = {} if not opts["disableSubRepositoryDetection"]: subrepos = collect_sub_repositories(revCtx) - printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"]) + printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["proceedFrom"]) viewer = File_Viewer(revCtx, printer) viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 92a05a05a0..e6c07983bf 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -45,7 +45,6 @@ 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.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -181,6 +180,33 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertEquals(2, c.getChildren().size()); } + @Test + public void testLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + + BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); + FileObject root = result.getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + } + + @Test + public void testProceedFrom() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + request.setProceedFrom(2); + + BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); + FileObject root = result.getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + } + //~--- get methods ---------------------------------------------------------- /** From 9afc3a958073dfbde38b504cad475f366ac8331f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 12:56:20 +0100 Subject: [PATCH 006/111] Implement request limit for svn --- .../scm/repository/spi/SvnBrowseCommand.java | 11 ++- .../repository/spi/SvnBrowseCommandTest.java | 67 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index e4a32c8ca6..413c5765b0 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -52,6 +52,7 @@ import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; import java.util.Collection; +import java.util.Iterator; import static org.tmatesoft.svn.core.SVNErrorCode.FS_NO_SUCH_REVISION; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -73,6 +74,8 @@ public class SvnBrowseCommand extends AbstractSvnCommand private static final Logger logger = LoggerFactory.getLogger(SvnBrowseCommand.class); + private int resultCount = 0; + SvnBrowseCommand(SvnContext context, Repository repository) { super(context, repository); @@ -128,11 +131,13 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); - for (SVNDirEntry entry : entries) - { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getProceedFrom() && iterator.hasNext(); ++resultCount) { + SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - parent.addChild(child); + if (resultCount >= request.getProceedFrom()) { + parent.addChild(child); + } if (child.isDirectory() && request.isRecursive()) { traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 980d486b5c..0244e9dbeb 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -40,10 +40,10 @@ import sonia.scm.repository.FileObject; import java.io.IOException; import java.util.Collection; +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.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -65,7 +65,17 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase @Test public void testBrowse() { - Collection foList = getRootFromTip(new BrowseCommandRequest()); + BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest()); + + assertNotNull(result); + + Collection foList1 = result.getFile().getChildren(); + + assertNotNull(foList1); + assertFalse(foList1.isEmpty()); + assertEquals(2, foList1.size()); + + Collection foList = foList1; FileObject a = getFileObject(foList, "a.txt"); FileObject c = getFileObject(foList, "c"); @@ -140,14 +150,24 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase request.setDisableLastCommit(true); - Collection foList = getRootFromTip(request); + BrowserResult result = createCommand().getBrowserResult(request); + + assertNotNull(result); + + Collection foList1 = result.getFile().getChildren(); + + assertNotNull(foList1); + assertFalse(foList1.isEmpty()); + assertEquals(2, foList1.size()); + + Collection foList = foList1; FileObject a = getFileObject(foList, "a.txt"); assertFalse(a.getDescription().isPresent()); assertFalse(a.getCommitDate().isPresent()); } - + @Test public void testRecursive() { BrowseCommandRequest request = new BrowseCommandRequest(); @@ -168,6 +188,32 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertEquals(2, c.getChildren().size()); } + @Test + public void testLimit() { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(1); + BrowserResult result = createCommand().getBrowserResult(request); + + assertNotNull(result); + + Collection foList = result.getFile().getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + } + + @Test + public void testProceedFrom() { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setProceedFrom(1); + BrowserResult result = createCommand().getBrowserResult(request); + + assertNotNull(result); + + Collection foList = result.getFile().getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c"); + } + /** * Method description * @@ -198,17 +244,4 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase .orElseThrow(() -> new AssertionError("file " + name + " not found")); } - private Collection getRootFromTip(BrowseCommandRequest request) { - BrowserResult result = createCommand().getBrowserResult(request); - - assertNotNull(result); - - Collection foList = result.getFile().getChildren(); - - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(2, foList.size()); - - return foList; - } } From 3652a33fa001a487579e5115c6d90d2ba8e6d315 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 15:24:32 +0100 Subject: [PATCH 007/111] Add truncated flag for git --- .../main/java/sonia/scm/repository/FileObject.java | 12 +++++++++++- .../sonia/scm/repository/spi/GitBrowseCommand.java | 11 +++++++---- .../scm/repository/spi/GitBrowseCommandTest.java | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java index 8f1cf298de..e262177e19 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -87,7 +87,7 @@ public class FileObject implements LastModifiedAware, Serializable final FileObject other = (FileObject) obj; //J- - return Objects.equal(name, other.name) + return Objects.equal(name, other.name) && Objects.equal(path, other.path) && Objects.equal(directory, other.directory) && Objects.equal(description, other.description) @@ -282,6 +282,10 @@ public class FileObject implements LastModifiedAware, Serializable return computationAborted; } + public boolean isTruncated() { + return truncated; + } + //~--- set methods ---------------------------------------------------------- /** @@ -403,6 +407,10 @@ public class FileObject implements LastModifiedAware, Serializable this.children.add(child); } + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } + //~--- fields --------------------------------------------------------------- /** file description */ @@ -435,4 +443,6 @@ public class FileObject implements LastModifiedAware, Serializable /** Children of this file (aka directory). */ private Collection children = new ArrayList<>(); + + private boolean truncated; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 79aeb51342..5ececf176f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -153,6 +153,7 @@ public class GitBrowseCommand extends AbstractGitCommand fileObject.setName(""); fileObject.setPath(""); fileObject.setDirectory(true); + fileObject.setTruncated(false); return fileObject; } @@ -253,7 +254,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next() && resultCount < request.getLimit() + request.getProceedFrom()) + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getProceedFrom()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -262,12 +263,10 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - if (resultCount >= request.getProceedFrom()) { + if (resultCount > request.getProceedFrom()) { files.add(fileObject); } - ++resultCount; - if (request.isRecursive() && fileObject.isDirectory()) { treeWalk.enterSubtree(); FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); @@ -279,6 +278,10 @@ public class GitBrowseCommand extends AbstractGitCommand parent.setChildren(files); + if (resultCount > request.getLimit() + request.getProceedFrom()) { + parent.setTruncated(true); + } + return null; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 78aa12e8e0..04d2af9762 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -72,6 +72,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { BrowserResult result = createCommand().getBrowserResult(request); FileObject fileObject = result.getFile(); assertEquals("a.txt", fileObject.getName()); + assertFalse(fileObject.isTruncated()); } @Test @@ -247,6 +248,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).hasSize(2); + assertTrue(root.isTruncated()); } @Test @@ -261,6 +263,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).extracting("name").contains("c", "f.txt"); + assertFalse(root.isTruncated()); } private FileObject findFile(Collection foList, String name) { From 6eca277d65eb75a34439227e59042aa47b84ae65 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 15:35:49 +0100 Subject: [PATCH 008/111] Add truncated flag for hg --- .../scm/repository/spi/javahg/HgFileviewCommand.java | 6 +++++- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 10 ++++++++++ .../sonia/scm/repository/spi/HgBrowseCommandTest.java | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 3b74f6b9cb..9ad2995307 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -60,6 +60,7 @@ import java.util.LinkedList; public class HgFileviewCommand extends AbstractCommand { + public static final char TRUNCATED_MARK = 't'; private boolean disableLastCommit = false; private HgFileviewCommand(Repository repository) @@ -186,7 +187,7 @@ public class HgFileviewCommand extends AbstractCommand HgInputStream stream = launchStream(); FileObject last = null; - while (stream.peek() != -1) { + while (stream.peek() != -1 && stream.peek() != TRUNCATED_MARK) { FileObject file = read(stream); while (!stack.isEmpty()) { @@ -210,6 +211,9 @@ public class HgFileviewCommand extends AbstractCommand return last; } else { // if the stack is not empty, the requested path is a directory + if (stream.read() == TRUNCATED_MARK) { + stack.getLast().setTruncated(true); + } return stack.getLast(); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index ec57192666..7c36a55703 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -249,6 +249,15 @@ class File_Printer: self.result_count += 1 return self.result_count == 0 or self.proceedFrom < self.result_count <= self.limit + self.proceedFrom + def isTruncated(self): + return self.result_count > self.limit + self.proceedFrom + + def finish(self): + if self.isTruncated(): + if self.transport: + self.ui.write( "t") + else: + self.ui.write("truncated") class File_Viewer: def __init__(self, revCtx, visitor): @@ -297,3 +306,4 @@ def fileview(ui, repo, **opts): viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos viewer.view(opts["path"]) + printer.finish() diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index e6c07983bf..3168738252 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -191,6 +191,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + assertThat(root.isTruncated()).isTrue(); } @Test @@ -205,6 +206,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + assertThat(root.isTruncated()).isFalse(); } //~--- get methods ---------------------------------------------------------- From 1c8088a1c62030557af2f6f8b6c17dfbcd6d737d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 15:55:01 +0100 Subject: [PATCH 009/111] Add truncated flag for svn --- .../main/java/sonia/scm/repository/spi/SvnBrowseCommand.java | 3 +++ .../java/sonia/scm/repository/spi/SvnBrowseCommandTest.java | 1 + 2 files changed, 4 insertions(+) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 413c5765b0..7147b14a57 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -143,6 +143,9 @@ public class SvnBrowseCommand extends AbstractSvnCommand traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } } + if (resultCount >= request.getLimit() + request.getProceedFrom()) { + parent.setTruncated(true); + } } private String createBasePath(String path) diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 0244e9dbeb..ecf3ee0893 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -199,6 +199,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + assertThat(result.getFile().isTruncated()).isTrue(); } @Test From 6b3f36e7ea5fe10af8a49762016e804dd1593794 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 17:56:22 +0100 Subject: [PATCH 010/111] WIP --- .../repository/spi/BrowseCommandRequest.java | 2 +- .../src/repos/sources/components/FileTree.tsx | 47 ++++++++++++------- .../v2/resources/BaseFileObjectDtoMapper.java | 6 ++- .../BrowserResultToFileObjectDtoMapper.java | 15 ++++-- .../scm/api/v2/resources/FileObjectDto.java | 2 +- .../api/v2/resources/SourceRootResource.java | 20 ++++---- ...rowserResultToFileObjectDtoMapperTest.java | 6 +-- 7 files changed, 62 insertions(+), 36 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 3dbe71f322..e9bdde76a9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -49,7 +49,7 @@ import java.util.function.Consumer; public final class BrowseCommandRequest extends FileBaseCommandRequest { - public static final int DEFAULT_REQUEST_LIMIT = 1000; + public static final int DEFAULT_REQUEST_LIMIT = 100; private static final long serialVersionUID = 7956624623516803183L; private int proceedFrom; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 1a5a8b5b58..5163bc150a 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -9,6 +9,7 @@ import { File, Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; import { fetchSources, getFetchSourcesFailure, getSources, isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; +import queryString from "query-string"; type Props = WithTranslation & { loading: boolean; @@ -18,6 +19,7 @@ type Props = WithTranslation & { revision: string; path: string; baseUrl: string; + location: any; updateSources: () => void; @@ -85,7 +87,7 @@ class FileTree extends React.Component { } renderSourcesTable() { - const { tree, revision, path, baseUrl, t } = this.props; + const { tree, revision, path, baseUrl, t, location } = this.props; const files = []; @@ -119,6 +121,7 @@ class FileTree extends React.Component { } if (files && files.length > 0) { + let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); @@ -126,24 +129,32 @@ class FileTree extends React.Component { baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); } + const proceedFrom = queryString.parse(location.search).proceedFrom; + if (proceedFrom) { + baseUrlWithRevision += "?proceedFrom=" + proceedFrom; + } + return ( - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map(file => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
+ <> + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
+ {tree.truncated &&

TRUNCATED

} + ); } return {t("sources.noSources")}; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java index bb74bc5045..9260343065 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -10,6 +10,7 @@ import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.SubRepository; +import sonia.scm.repository.spi.BrowseCommandRequest; import javax.inject.Inject; @@ -30,7 +31,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); @ObjectFactory - FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, FileObject fileObject) { + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom, FileObject fileObject) { String path = removeFirstSlash(fileObject.getPath()); Links.Builder links = Links.linkingTo(); if (fileObject.isDirectory()) { @@ -39,6 +40,9 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); } + if (fileObject.isTruncated()) { + links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?proceedFrom=" + (proceedFrom + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); + } Embedded.Builder embeddedBuilder = embeddedBuilder(); applyEnrichers(links, embeddedBuilder, namespaceAndName, browserResult, fileObject); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java index f9304881e7..4ff1002367 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java @@ -1,14 +1,19 @@ package sonia.scm.api.v2.resources; +import com.google.common.annotations.VisibleForTesting; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.ObjectFactory; import org.mapstruct.Qualifier; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.SubRepository; +import sonia.scm.repository.spi.BrowseCommand; +import sonia.scm.repository.spi.BrowseCommandRequest; import javax.inject.Inject; import java.lang.annotation.ElementType; @@ -19,19 +24,23 @@ import java.time.Instant; import java.util.Optional; import java.util.OptionalLong; +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; + @Mapper public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectDtoMapper { - FileObjectDto map(BrowserResult browserResult, @Context NamespaceAndName namespaceAndName) { - FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult); + FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, int proceedFrom) { + FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, proceedFrom); fileObjectDto.setRevision(browserResult.getRevision()); + return fileObjectDto; } @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "children", qualifiedBy = Children.class) @Children - protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); + protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom); @Override void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java index b273f241dc..1d39c90f37 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -15,7 +15,6 @@ import java.util.OptionalLong; @Getter @Setter -@NoArgsConstructor public class FileObjectDto extends HalRepresentation { private String name; private String path; @@ -31,6 +30,7 @@ public class FileObjectDto extends HalRepresentation { private String revision; private boolean partialResult; private boolean computationAborted; + private boolean truncated; public FileObjectDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 758afd7660..10f7cfd47d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -8,11 +8,12 @@ import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.DefaultValue; 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.Response; +import javax.ws.rs.QueryParam; import java.io.IOException; import java.net.URLDecoder; @@ -34,36 +35,37 @@ public class SourceRootResource { @GET @Produces(VndMediaType.SOURCE) @Path("") - public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { - return getSource(namespace, name, "/", null); + public FileObjectDto getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { + return getSource(namespace, name, "/", null, proceedFrom); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}") - public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException { - return getSource(namespace, name, "/", revision); + public FileObjectDto getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { + return getSource(namespace, name, "/", revision, proceedFrom); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}/{path: .*}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws IOException { - return getSource(namespace, name, path, revision); + public FileObjectDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { + return getSource(namespace, name, path, revision, proceedFrom); } - private Response getSource(String namespace, String repoName, String path, String revision) throws IOException { + private FileObjectDto getSource(String namespace, String repoName, String path, String revision, int proceedFrom) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand(); browseCommand.setPath(path); + browseCommand.setProceedFrom(proceedFrom); if (revision != null && !revision.isEmpty()) { browseCommand.setRevision(URLDecoder.decode(revision, "UTF-8")); } BrowserResult browserResult = browseCommand.getBrowserResult(); if (browserResult != null) { - return Response.ok(browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName)).build(); + return browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName, proceedFrom); } else { throw notFound(entity("Source", path).in("Revision", revision).in(namespaceAndName)); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java index 273cc25018..dc264530fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java @@ -66,7 +66,7 @@ public class BrowserResultToFileObjectDtoMapperTest { public void shouldMapAttributesCorrectly() { BrowserResult browserResult = createBrowserResult(); - FileObjectDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar")); + FileObjectDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar"), 0); assertEqualAttributes(browserResult, dto); } @@ -76,7 +76,7 @@ public class BrowserResultToFileObjectDtoMapperTest { BrowserResult browserResult = createBrowserResult(); NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar"); - FileObjectDto dto = mapper.map(browserResult, namespaceAndName); + FileObjectDto dto = mapper.map(browserResult, namespaceAndName, 0); assertThat(dto.getEmbedded().getItemsBy("children")).hasSize(2); } @@ -86,7 +86,7 @@ public class BrowserResultToFileObjectDtoMapperTest { BrowserResult browserResult = createBrowserResult(); NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar"); - FileObjectDto dto = mapper.map(browserResult, namespaceAndName); + FileObjectDto dto = mapper.map(browserResult, namespaceAndName, 0); assertThat(dto.getLinks().getLinkBy("self").get().getHref()).contains("path"); } From fe1591171dd68081ca5740a2464462e370a0620a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 19 Feb 2020 09:23:23 +0100 Subject: [PATCH 011/111] Rename 'proceedFrom' to 'offset' --- .../repository/api/BrowseCommandBuilder.java | 8 ++++---- .../repository/spi/BrowseCommandRequest.java | 14 +++++++------- .../scm/repository/spi/GitBrowseCommand.java | 6 +++--- .../repository/spi/GitBrowseCommandTest.java | 4 ++-- .../scm/repository/spi/HgBrowseCommand.java | 2 +- .../spi/javahg/HgFileviewCommand.java | 8 ++++---- .../resources/sonia/scm/hg/ext/fileview.py | 12 ++++++------ .../repository/spi/HgBrowseCommandTest.java | 4 ++-- .../scm/repository/spi/SvnBrowseCommand.java | 6 +++--- .../repository/spi/SvnBrowseCommandTest.java | 4 ++-- .../src/repos/sources/components/FileTree.tsx | 6 +++--- .../v2/resources/BaseFileObjectDtoMapper.java | 4 ++-- .../BrowserResultToFileObjectDtoMapper.java | 6 +++--- .../api/v2/resources/SourceRootResource.java | 18 +++++++++--------- 14 files changed, 51 insertions(+), 51 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index 7862fdb64a..c312b20534 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -318,12 +318,12 @@ public final class BrowseCommandBuilder /** * Proceed the list from the given number on (zero based). * - * @param proceedFrom The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. * @since 2.0.0 */ - public BrowseCommandBuilder setProceedFrom(int proceedFrom) { - request.setProceedFrom(proceedFrom); + public BrowseCommandBuilder setOffset(int offset) { + request.setOffset(offset); return this; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index e9bdde76a9..70dec0980a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -52,7 +52,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public static final int DEFAULT_REQUEST_LIMIT = 100; private static final long serialVersionUID = 7956624623516803183L; - private int proceedFrom; + private int offset; public BrowseCommandRequest() { this(null); @@ -207,12 +207,12 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** * Proceed the list from the given number on (zero based). * - * @param proceedFrom The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. * @since 2.0.0 */ - public void setProceedFrom(int proceedFrom) { - this.proceedFrom = proceedFrom; + public void setOffset(int offset) { + this.offset = offset; } //~--- get methods ---------------------------------------------------------- @@ -270,8 +270,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest * * @since 2.0.0 */ - public int getProceedFrom() { - return proceedFrom; + public int getOffset() { + return offset; } public void updateCache(BrowserResult update) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 5ececf176f..f53b7ec0a4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -254,7 +254,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getProceedFrom()) + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -263,7 +263,7 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - if (resultCount > request.getProceedFrom()) { + if (resultCount > request.getOffset()) { files.add(fileObject); } @@ -278,7 +278,7 @@ public class GitBrowseCommand extends AbstractGitCommand parent.setChildren(files); - if (resultCount > request.getLimit() + request.getProceedFrom()) { + if (resultCount > request.getLimit() + request.getOffset()) { parent.setTruncated(true); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 04d2af9762..ce8214e2fa 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -252,10 +252,10 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testBrowseProceedFrom() throws IOException { + public void testBrowseOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setLimit(2); - request.setProceedFrom(2); + request.setOffset(2); FileObject root = createCommand() .getBrowserResult(request).getFile(); assertNotNull(root); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index 6f2e519d3d..63a2b0f4cd 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -102,7 +102,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand } cmd.setLimit(request.getLimit()); - cmd.setProceedFrom(request.getProceedFrom()); + cmd.setOffset(request.getOffset()); FileObject file = cmd.execute(); return new BrowserResult(c == null? "tip": c.getNode(), revision, file); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 9ad2995307..815e830c8a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -159,14 +159,14 @@ public class HgFileviewCommand extends AbstractCommand /** * Proceed the list from the given number on (zero based). * - * @param proceedFrom The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. * * @return {@code this} * @since 2.0.0 */ - public HgFileviewCommand setProceedFrom(int proceedFrom) { - cmdAppend("-f", proceedFrom); + public HgFileviewCommand setOffset(int offset) { + cmdAppend("-o", offset); return this; } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 7c36a55703..86e6175d55 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -197,7 +197,7 @@ def collect_sub_repositories(revCtx): class File_Printer: - def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, proceedFrom): + def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, offset): self.ui = ui self.repo = repo self.revCtx = revCtx @@ -205,7 +205,7 @@ class File_Printer: self.transport = transport self.result_count = -1 self.limit = limit - self.proceedFrom = proceedFrom + self.offset = offset def print_directory(self, path): if self.shouldPrintResult(): @@ -247,10 +247,10 @@ class File_Printer: # The first result is the selected path (or root if not specified). This # always has to be printed. Therefore we start counting with -1. self.result_count += 1 - return self.result_count == 0 or self.proceedFrom < self.result_count <= self.limit + self.proceedFrom + return self.result_count == 0 or self.offset < self.result_count <= self.limit + self.offset def isTruncated(self): - return self.result_count > self.limit + self.proceedFrom + return self.result_count > self.limit + self.offset def finish(self): if self.isTruncated(): @@ -294,14 +294,14 @@ class File_Viewer: ('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'), ('t', 'transport', False, 'format the output for command server'), ('l', 'limit', 1000, 'limit the number of results'), - ('f', 'proceedFrom', 0, 'proceed from the given result number (zero based)'), + ('o', 'offset', 0, 'proceed from the given result number (zero based)'), ]) def fileview(ui, repo, **opts): revCtx = scmutil.revsingle(repo, opts["revision"]) subrepos = {} if not opts["disableSubRepositoryDetection"]: subrepos = collect_sub_repositories(revCtx) - printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["proceedFrom"]) + printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["offset"]) viewer = File_Viewer(revCtx, printer) viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 3168738252..f74d036843 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -195,10 +195,10 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { } @Test - public void testProceedFrom() throws IOException { + public void testOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setLimit(2); - request.setProceedFrom(2); + request.setOffset(2); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); FileObject root = result.getFile(); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 7147b14a57..1daaf5af61 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -131,11 +131,11 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); - for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getProceedFrom() && iterator.hasNext(); ++resultCount) { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - if (resultCount >= request.getProceedFrom()) { + if (resultCount >= request.getOffset()) { parent.addChild(child); } @@ -143,7 +143,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } } - if (resultCount >= request.getLimit() + request.getProceedFrom()) { + if (resultCount >= request.getLimit() + request.getOffset()) { parent.setTruncated(true); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index ecf3ee0893..589f0d4107 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -203,9 +203,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase } @Test - public void testProceedFrom() { + public void testOffset() { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setProceedFrom(1); + request.setOffset(1); BrowserResult result = createCommand().getBrowserResult(request); assertNotNull(result); diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 5163bc150a..0c5568d09c 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -129,9 +129,9 @@ class FileTree extends React.Component { baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); } - const proceedFrom = queryString.parse(location.search).proceedFrom; - if (proceedFrom) { - baseUrlWithRevision += "?proceedFrom=" + proceedFrom; + const offset = queryString.parse(location.search).offset; + if (offset) { + baseUrlWithRevision += "?offset=" + offset; } return ( diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java index 9260343065..be9621f0b7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -31,7 +31,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); @ObjectFactory - FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom, FileObject fileObject) { + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer offset, FileObject fileObject) { String path = removeFirstSlash(fileObject.getPath()); Links.Builder links = Links.linkingTo(); if (fileObject.isDirectory()) { @@ -41,7 +41,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); } if (fileObject.isTruncated()) { - links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?proceedFrom=" + (proceedFrom + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); + links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?offset=" + (offset + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); } Embedded.Builder embeddedBuilder = embeddedBuilder(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java index 4ff1002367..0b069fcbe4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java @@ -30,8 +30,8 @@ import static de.otto.edison.hal.Link.link; @Mapper public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectDtoMapper { - FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, int proceedFrom) { - FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, proceedFrom); + FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, int offset) { + FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, offset); fileObjectDto.setRevision(browserResult.getRevision()); return fileObjectDto; @@ -40,7 +40,7 @@ public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectD @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "children", qualifiedBy = Children.class) @Children - protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom); + protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer offset); @Override void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 10f7cfd47d..d189b40b93 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -35,37 +35,37 @@ public class SourceRootResource { @GET @Produces(VndMediaType.SOURCE) @Path("") - public FileObjectDto getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { - return getSource(namespace, name, "/", null, proceedFrom); + public FileObjectDto getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("offset") int offset) throws IOException { + return getSource(namespace, name, "/", null, offset); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}") - public FileObjectDto getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { - return getSource(namespace, name, "/", revision, proceedFrom); + public FileObjectDto getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @DefaultValue("0") @QueryParam("offset") int offset) throws IOException { + return getSource(namespace, name, "/", revision, offset); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}/{path: .*}") - public FileObjectDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { - return getSource(namespace, name, path, revision, proceedFrom); + public FileObjectDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("offset") int offset) throws IOException { + return getSource(namespace, name, path, revision, offset); } - private FileObjectDto getSource(String namespace, String repoName, String path, String revision, int proceedFrom) throws IOException { + private FileObjectDto getSource(String namespace, String repoName, String path, String revision, int offset) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand(); browseCommand.setPath(path); - browseCommand.setProceedFrom(proceedFrom); + browseCommand.setOffset(offset); if (revision != null && !revision.isEmpty()) { browseCommand.setRevision(URLDecoder.decode(revision, "UTF-8")); } BrowserResult browserResult = browseCommand.getBrowserResult(); if (browserResult != null) { - return browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName, proceedFrom); + return browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName, offset); } else { throw notFound(entity("Source", path).in("Revision", revision).in(namespaceAndName)); } From 4bc3d16aa958e8d3232b6cca255d8e1a31079b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 19 Feb 2020 18:37:09 +0100 Subject: [PATCH 012/111] WIP --- scm-ui/ui-types/src/Sources.ts | 1 + .../src/repos/sources/components/FileTree.tsx | 161 +++++++++++------- .../src/repos/sources/modules/sources.ts | 91 +++++++--- 3 files changed, 165 insertions(+), 88 deletions(-) diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index dce6947622..99e3bde7ac 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -18,6 +18,7 @@ export type File = { subRepository?: SubRepository; // TODO partialResult: boolean; computationAborted: boolean; + truncated: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 0c5568d09c..55a9abdf18 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -7,28 +7,40 @@ import styled from "styled-components"; import { binder } from "@scm-manager/ui-extensions"; import { File, Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; -import { fetchSources, getFetchSourcesFailure, getSources, isFetchSourcesPending } from "../modules/sources"; +import { + fetchSources, + getFetchSourcesFailure, + getHunkCount, + getSources, + isFetchSourcesPending, isUpdateSourcePending +} from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; -import queryString from "query-string"; +import Button from "@scm-manager/ui-components/src/buttons/Button"; -type Props = WithTranslation & { +type Hunk = { + tree: File; loading: boolean; error: Error; - tree: File; + updateSources: (hunk: number) => void; +}; + +type Props = WithTranslation & { repository: Repository; revision: string; path: string; baseUrl: string; location: any; + hunks: Hunk[]; - updateSources: () => void; + // dispatch props + fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => void; // context props match: any; }; type State = { - stoppableUpdateHandler?: number; + stoppableUpdateHandler: number[]; }; const FixedWidthTh = styled.th` @@ -50,44 +62,57 @@ export function findParent(path: string) { class FileTree extends React.Component { constructor(props: Props) { super(props); - this.state = {}; + this.state = { stoppableUpdateHandler: [] }; } componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { - const { tree, updateSources } = this.props; - if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) { - const stoppableUpdateHandler = setTimeout(updateSources, 3000); - this.setState({ stoppableUpdateHandler: stoppableUpdateHandler }); - } + const { hunks } = this.props; + hunks?.forEach((hunk, index) => { + if (hunk.tree?._embedded?.children && hunk.tree._embedded.children.find(c => c.partialResult)) { + const stoppableUpdateHandler = setTimeout(hunk.updateSources, 3000); + this.setState(prevState => { + return { + stoppableUpdateHandler: [...prevState.stoppableUpdateHandler, stoppableUpdateHandler] + }; + }); + } + }); } } componentWillUnmount(): void { - if (this.state.stoppableUpdateHandler) { - clearTimeout(this.state.stoppableUpdateHandler); - } + this.state.stoppableUpdateHandler.forEach(handler => clearTimeout(handler)); } + loadMore = () => { + // console.log("smth"); + }; + render() { - const { error, loading, tree } = this.props; + const { hunks, t } = this.props; - if (error) { - return ; - } - - if (loading) { - return ; - } - if (!tree) { + if (!hunks || hunks.length === 0) { return null; } - return
{this.renderSourcesTable()}
; + if (hunks.some(hunk => hunk.error)) { + return hunk.error)[0]} />; + } + + const lastHunk = hunks[hunks.length - 1]; + + return ( +
+ {this.renderSourcesTable()} + {lastHunk.loading && } + {lastHunk.tree?.truncated &&
+ ); } renderSourcesTable() { - const { tree, revision, path, baseUrl, t, location } = this.props; + const { hunks, revision, path, baseUrl, t } = this.props; const files = []; @@ -115,46 +140,41 @@ class FileTree extends React.Component { } }; - if (tree._embedded && tree._embedded.children) { - const children = [...tree._embedded.children].sort(compareFiles); - files.push(...children); - } + hunks + .filter(hunk => !hunk.loading) + .forEach(hunk => { + if (hunk.tree?._embedded && hunk.tree._embedded.children) { + const children = [...hunk.tree._embedded.children]; + files.push(...children); + } + }); if (files && files.length > 0) { - let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); } else { - baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); - } - - const offset = queryString.parse(location.search).offset; - if (offset) { - baseUrlWithRevision += "?offset=" + offset; + baseUrlWithRevision += "/" + encodeURIComponent(hunks[0].tree.revision); } return ( - <> - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map((file: any) => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
- {tree.truncated &&

TRUNCATED

} - + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
); } return {t("sources.noSources")}; @@ -164,24 +184,37 @@ class FileTree extends React.Component { const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const updateSources = () => dispatch(fetchSources(repository, revision, path, false)); - - return { updateSources }; + return { + updateSources: (hunk: number) => dispatch(fetchSources(repository, revision, path, false, hunk)), + fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => { + dispatch(fetchSources(repository, revision, path, true, hunk)); + } + }; }; const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path); - const error = getFetchSourcesFailure(state, repository, revision, path); - const tree = getSources(state, repository, revision, path); + const loading = isFetchSourcesPending(state, repository, revision, path, 0); + const error = getFetchSourcesFailure(state, repository, revision, path, 0); + const hunkCount = getHunkCount(state, repository, revision, path); + const hunks = []; + for (let i = 0; i < hunkCount; ++i) { + console.log(`getting data for hunk ${i}`); + const tree = getSources(state, repository, revision, path, i); + const loading = isFetchSourcesPending(state, repository, revision, path, i); + hunks.push({ + tree, + loading + }); + } return { revision, path, loading, error, - tree + hunks }; }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 2c7413f7ea..51c5eeb0e1 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -10,34 +10,36 @@ export const FETCH_UPDATES_PENDING = `${FETCH_SOURCES}_UPDATE_PENDING`; export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`; export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`; -export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true) { +export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true, hunk = 0) { return function(dispatch: any, getState: () => any) { const state = getState(); if ( - isFetchSourcesPending(state, repository, revision, path) || - isUpdateSourcePending(state, repository, revision, path) + isFetchSourcesPending(state, repository, revision, path, hunk) || + isUpdateSourcePending(state, repository, revision, path, hunk) ) { return; } if (initialLoad) { - dispatch(fetchSourcesPending(repository, revision, path)); + dispatch(fetchSourcesPending(repository, revision, path, hunk)); } else { - dispatch(updateSourcesPending(repository, revision, path, getSources(state, repository, revision, path))); + dispatch( + updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) + ); } return apiClient - .get(createUrl(repository, revision, path)) + .get(createUrl(repository, revision, path, hunk)) .then(response => response.json()) .then((sources: File) => { - dispatch(fetchSourcesSuccess(repository, revision, path, sources)); + dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); }) .catch(err => { - dispatch(fetchSourcesFailure(repository, revision, path, err)); + dispatch(fetchSourcesFailure(repository, revision, path, hunk, err)); }); }; } -function createUrl(repository: Repository, revision: string, path: string) { +function createUrl(repository: Repository, revision: string, path: string, hunk: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -45,13 +47,14 @@ function createUrl(repository: Repository, revision: string, path: string) { // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; } -export function fetchSourcesPending(repository: Repository, revision: string, path: string): Action { +export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path), + payload: { hunk, pending: true, sources: {} } }; } @@ -59,24 +62,37 @@ export function updateSourcesPending( repository: Repository, revision: string, path: string, + hunk: number, currentSources: any ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { updatePending: true, sources: currentSources }, + payload: { hunk, updatePending: true, sources: currentSources }, itemId: createItemId(repository, revision, path) }; } -export function fetchSourcesSuccess(repository: Repository, revision: string, path: string, sources: File) { +export function fetchSourcesSuccess( + repository: Repository, + revision: string, + path: string, + hunk: number, + sources: File +) { return { type: FETCH_SOURCES_SUCCESS, - payload: { updatePending: false, sources }, + payload: { hunk, pending: false, updatePending: false, sources }, itemId: createItemId(repository, revision, path) }; } -export function fetchSourcesFailure(repository: Repository, revision: string, path: string, error: Error): Action { +export function fetchSourcesFailure( + repository: Repository, + revision: string, + path: string, + hunk: number, + error: Error +): Action { return { type: FETCH_SOURCES_FAILURE, payload: error, @@ -99,9 +115,14 @@ export default function reducer( } ): any { if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { + console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); return { ...state, - [action.itemId]: action.payload + [action.itemId + "/hunkCount"]: action.payload.hunk + 1, + [action.itemId + "/" + action.payload.hunk]: { + sources: action.payload.sources, + loading: false + } }; } return state; @@ -110,7 +131,7 @@ export default function reducer( // selectors export function isDirectory(state: any, repository: Repository, revision: string, path: string): boolean { - const currentFile = getSources(state, repository, revision, path); + const currentFile = getSources(state, repository, revision, path, 0); if (currentFile && !currentFile.directory) { return false; } else { @@ -118,31 +139,53 @@ export function isDirectory(state: any, repository: Repository, revision: string } } +export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { + if (state.sources) { + const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + return count ? count : 0; + } + return 0; +} + export function getSources( state: any, repository: Repository, revision: string | undefined, - path: string + path: string, + hunk: number ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path)]?.sources; + return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; } return null; } -export function isFetchSourcesPending(state: any, repository: Repository, revision: string, path: string): boolean { +export function isFetchSourcesPending( + state: any, + repository: Repository, + revision: string, + path: string, + hunk: number +): boolean { return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); } -function isUpdateSourcePending(state: any, repository: Repository, revision: string, path: string): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path)]?.updatePending; +export function isUpdateSourcePending( + state: any, + repository: Repository, + revision: string, + path: string, + hunk: number +): boolean { + return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; } export function getFetchSourcesFailure( state: any, repository: Repository, revision: string, - path: string + path: string, + hunk: number ): Error | null | undefined { return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); } From c4a801a7be919c80bdd900cf9a68c6a057a7625b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 20 Feb 2020 14:36:13 +0100 Subject: [PATCH 013/111] WIP --- .../repository/spi/BrowseCommandRequest.java | 10 ++- scm-ui/ui-types/src/Sources.ts | 6 +- .../src/repos/sources/components/FileTree.tsx | 26 ++---- .../src/repos/sources/modules/sources.test.ts | 84 ++++++++++++----- .../src/repos/sources/modules/sources.ts | 89 +++++++++++++------ 5 files changed, 137 insertions(+), 78 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 70dec0980a..4477a26e31 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -112,10 +112,11 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest final BrowseCommandRequest other = (BrowseCommandRequest) obj; - return super.equals(obj) && Objects.equal(recursive, other.recursive) + return super.equals(obj) + && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) - && Objects.equal(disableSubRepositoryDetection, - other.disableSubRepositoryDetection); + && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) + && Objects.equal(offset, other.offset); } /** @@ -128,7 +129,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection); + disableSubRepositoryDetection, offset); } /** @@ -147,6 +148,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest .add("recursive", recursive) .add("disableLastCommit", disableLastCommit) .add("disableSubRepositoryDetection", disableSubRepositoryDetection) + .add("offset", offset) .toString(); //J+ } diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 99e3bde7ac..b1cca3c59f 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -16,9 +16,9 @@ export type File = { length?: number; commitDate?: string; subRepository?: SubRepository; // TODO - partialResult: boolean; - computationAborted: boolean; - truncated: boolean; + partialResult?: boolean; + computationAborted?: boolean; + truncated?: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 55a9abdf18..00cf1de981 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -12,7 +12,7 @@ import { getFetchSourcesFailure, getHunkCount, getSources, - isFetchSourcesPending, isUpdateSourcePending + isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; import Button from "@scm-manager/ui-components/src/buttons/Button"; @@ -86,7 +86,7 @@ class FileTree extends React.Component { } loadMore = () => { - // console.log("smth"); + this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; render() { @@ -124,22 +124,6 @@ class FileTree extends React.Component { }); } - const compareFiles = function(f1: File, f2: File): number { - if (f1.directory) { - if (f2.directory) { - return f1.name.localeCompare(f2.name); - } else { - return -1; - } - } else { - if (f2.directory) { - return 1; - } else { - return f1.name.localeCompare(f2.name); - } - } - }; - hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -149,7 +133,9 @@ class FileTree extends React.Component { } }); - if (files && files.length > 0) { + const loading = hunks.filter(hunk => hunk.loading).length > 0; + + if (loading || (files && files.length > 0)) { let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); @@ -195,7 +181,6 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path, 0); const error = getFetchSourcesFailure(state, repository, revision, path, 0); const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; @@ -212,7 +197,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { return { revision, path, - loading, error, hunks }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fa6ff5c1f6..fdd7a9fc78 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -37,6 +37,9 @@ const collection = { length: 176, revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4", subRepository: undefined, + truncated: true, + partialResult: false, + computationAborted: false, _links: { self: { href: @@ -120,12 +123,23 @@ describe("sources fetch", () => { const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/_/" + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/_/", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -136,17 +150,28 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository with the given revision and path", () => { - fetchMock.getOnce(sourcesUrl + "abc/src", collection); + fetchMock.getOnce(sourcesUrl + "abc/src?offset=0", collection); const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/abc/src" + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/abc/src", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -166,7 +191,7 @@ describe("sources fetch", () => { const actions = store.getActions(); expect(actions[0].type).toBe(FETCH_SOURCES_PENDING); expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE); - expect(actions[1].itemId).toBe("scm/core/_/"); + expect(actions[1].itemId).toBe("scm/core/_//"); expect(actions[1].payload).toBeDefined(); }); }); @@ -180,16 +205,18 @@ describe("reducer tests", () => { it("should store the collection, without revision and path", () => { const expectedState = { - "scm/core/_/": { updatePending: false, sources: collection } + "scm/core/_//0": { pending: false, updatePending: false, sources: collection }, + "scm/core/_//hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "", "", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "", "", 0, collection))).toEqual(expectedState); }); it("should store the collection, with revision and path", () => { const expectedState = { - "scm/core/abc/src/main": { updatePending: false, sources: collection } + "scm/core/abc/src/main/0": { pending: false, updatePending: false, sources: collection }, + "scm/core/abc/src/main/hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", 0, collection))).toEqual(expectedState); }); }); @@ -197,7 +224,7 @@ describe("selector tests", () => { it("should return false if it is no directory", () => { const state = { sources: { - "scm/core/abc/src/main/package.json": { + "scm/core/abc/src/main/package.json/0": { sources: { noDirectory } } } @@ -208,7 +235,7 @@ describe("selector tests", () => { it("should return true if it is directory", () => { const state = { sources: { - "scm/core/abc/src": noDirectory + "scm/core/abc/src/0": noDirectory } }; expect(isDirectory(state, repository, "abc", "src")).toBe(true); @@ -221,7 +248,7 @@ describe("selector tests", () => { it("should return the source collection without revision and path", () => { const state = { sources: { - "scm/core/_/": { + "scm/core/_//0": { sources: collection } } @@ -232,7 +259,7 @@ describe("selector tests", () => { it("should return the source collection with revision and path", () => { const state = { sources: { - "scm/core/abc/src/main": { + "scm/core/abc/src/main/0": { sources: collection } } @@ -242,15 +269,26 @@ describe("selector tests", () => { it("should return true, when fetch sources is pending", () => { const state = { - pending: { - [FETCH_SOURCES + "/scm/core/_/"]: true + sources: { + "scm/core/_//0": { + pending: true, + sources: {} + } } }; - expect(isFetchSourcesPending(state, repository, "", "")).toEqual(true); + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(true); }); it("should return false, when fetch sources is not pending", () => { - expect(isFetchSourcesPending({}, repository, "", "")).toEqual(false); + const state = { + sources: { + "scm/core/_//0": { + pending: false, + sources: {} + } + } + }; + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(false); }); const error = new Error("incredible error from hell"); @@ -258,13 +296,13 @@ describe("selector tests", () => { it("should return error when fetch sources did fail", () => { const state = { failure: { - [FETCH_SOURCES + "/scm/core/_/"]: error + [FETCH_SOURCES + "/scm/core/_//"]: error } }; - expect(getFetchSourcesFailure(state, repository, "", "")).toEqual(error); + expect(getFetchSourcesFailure(state, repository, "", "", 0)).toEqual(error); }); it("should return undefined when fetch sources did not fail", () => { - expect(getFetchSourcesFailure({}, repository, "", "")).toBe(undefined); + expect(getFetchSourcesFailure({}, repository, "", "", 0)).toBe(undefined); }); }); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 51c5eeb0e1..a12bf3ba0d 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -1,7 +1,6 @@ import * as types from "../../../modules/types"; import { Action, File, Link, Repository } from "@scm-manager/ui-types"; import { apiClient } from "@scm-manager/ui-components"; -import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES"; @@ -27,8 +26,18 @@ export function fetchSources(repository: Repository, revision: string, path: str updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) ); } + + let offset = 0; + const hunkCount = getHunkCount(state, repository, revision, path); + for (let i = 0; i < hunkCount; ++i) { + const sources = getSources(state, repository, revision, path, i); + if (sources?._embedded.children) { + offset += sources._embedded.children.length; + } + } + return apiClient - .get(createUrl(repository, revision, path, hunk)) + .get(createUrl(repository, revision, path, offset)) .then(response => response.json()) .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); @@ -39,7 +48,7 @@ export function fetchSources(repository: Repository, revision: string, path: str }; } -function createUrl(repository: Repository, revision: string, path: string, hunk: number) { +function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -47,14 +56,14 @@ function createUrl(repository: Repository, revision: string, path: string, hunk: // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?offset=${offset}`; } export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path), - payload: { hunk, pending: true, sources: {} } + itemId: createItemId(repository, revision, path, ""), + payload: { hunk, pending: true, updatePending: false, sources: {} } }; } @@ -63,12 +72,12 @@ export function updateSourcesPending( revision: string, path: string, hunk: number, - currentSources: any + currentSources: File ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { hunk, updatePending: true, sources: currentSources }, - itemId: createItemId(repository, revision, path) + payload: { hunk, pending: false, updatePending: true, sources: currentSources }, + itemId: createItemId(repository, revision, path, "") }; } @@ -82,7 +91,7 @@ export function fetchSourcesSuccess( return { type: FETCH_SOURCES_SUCCESS, payload: { hunk, pending: false, updatePending: false, sources }, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } @@ -96,14 +105,14 @@ export function fetchSourcesFailure( return { type: FETCH_SOURCES_FAILURE, payload: error, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } -function createItemId(repository: Repository, revision: string | undefined, path: string) { +function createItemId(repository: Repository, revision: string | undefined, path: string, hunk: number | string) { const revPart = revision ? revision : "_"; const pathPart = path ? path : ""; - return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}`; + return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}/${hunk}`; } // reducer @@ -114,18 +123,38 @@ export default function reducer( type: "UNKNOWN" } ): any { - if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { - console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); + if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) { return { ...state, - [action.itemId + "/hunkCount"]: action.payload.hunk + 1, - [action.itemId + "/" + action.payload.hunk]: { + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { sources: action.payload.sources, - loading: false + updatePending: false, + pending: false } }; + } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + sources: action.payload.sources, + updatePending: true, + pending: false + } + }; + } else if (action.itemId && action.type === FETCH_SOURCES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + updatePending: false, + pending: true + } + }; + } else { + return state; } - return state; } // selectors @@ -141,7 +170,7 @@ export function isDirectory(state: any, repository: Repository, revision: string export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { if (state.sources) { - const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + const count = state.sources[createItemId(repository, revision, path, "hunkCount")]; return count ? count : 0; } return 0; @@ -152,10 +181,10 @@ export function getSources( repository: Repository, revision: string | undefined, path: string, - hunk: number + hunk = 0 ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; + return state.sources[createItemId(repository, revision, path, hunk)]?.sources; } return null; } @@ -165,9 +194,12 @@ export function isFetchSourcesPending( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): boolean { - return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.pending; + } + return false; } export function isUpdateSourcePending( @@ -177,7 +209,10 @@ export function isUpdateSourcePending( path: string, hunk: number ): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.updatePending; + } + return false; } export function getFetchSourcesFailure( @@ -185,7 +220,7 @@ export function getFetchSourcesFailure( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): Error | null | undefined { - return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); + return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path, "")); } From 264f6efd049ab8e25a7c3566cf398576441efb1b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 07:37:55 +0100 Subject: [PATCH 014/111] Fix loading detection --- .../src/repos/sources/components/FileTree.tsx | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 00cf1de981..f3ecba8982 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -105,7 +105,6 @@ class FileTree extends React.Component { return (
{this.renderSourcesTable()} - {lastHunk.loading && } {lastHunk.tree?.truncated &&
); @@ -124,6 +123,10 @@ class FileTree extends React.Component { }); } + if (hunks.every(hunk => hunk.loading)) { + return ; + } + hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -144,23 +147,26 @@ class FileTree extends React.Component { } return ( - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map((file: any) => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
+ <> + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
+ {hunks[hunks.length - 1].loading && } + ); } return {t("sources.noSources")}; @@ -185,7 +191,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; for (let i = 0; i < hunkCount; ++i) { - console.log(`getting data for hunk ${i}`); const tree = getSources(state, repository, revision, path, i); const loading = isFetchSourcesPending(state, repository, revision, path, i); hunks.push({ From 722e38788b0fbd39d63525d030bdbb24a6f05770 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 09:45:43 +0100 Subject: [PATCH 015/111] Fix update --- .../ui-webapp/src/repos/sources/components/FileTree.tsx | 8 ++++---- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index f3ecba8982..881a841a57 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -21,7 +21,6 @@ type Hunk = { tree: File; loading: boolean; error: Error; - updateSources: (hunk: number) => void; }; type Props = WithTranslation & { @@ -34,6 +33,7 @@ type Props = WithTranslation & { // dispatch props fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => void; + updateSources: (hunk: number) => () => void; // context props match: any; @@ -67,10 +67,10 @@ class FileTree extends React.Component { componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { - const { hunks } = this.props; + const { hunks, updateSources } = this.props; hunks?.forEach((hunk, index) => { if (hunk.tree?._embedded?.children && hunk.tree._embedded.children.find(c => c.partialResult)) { - const stoppableUpdateHandler = setTimeout(hunk.updateSources, 3000); + const stoppableUpdateHandler = setTimeout(updateSources(index), 3000); this.setState(prevState => { return { stoppableUpdateHandler: [...prevState.stoppableUpdateHandler, stoppableUpdateHandler] @@ -177,7 +177,7 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const { repository, revision, path } = ownProps; return { - updateSources: (hunk: number) => dispatch(fetchSources(repository, revision, path, false, hunk)), + updateSources: (hunk: number) => () => dispatch(fetchSources(repository, revision, path, false, hunk)), fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => { dispatch(fetchSources(repository, revision, path, true, hunk)); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index a12bf3ba0d..d37833c1c3 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -28,8 +28,7 @@ export function fetchSources(repository: Repository, revision: string, path: str } let offset = 0; - const hunkCount = getHunkCount(state, repository, revision, path); - for (let i = 0; i < hunkCount; ++i) { + for (let i = 0; i < hunk; ++i) { const sources = getSources(state, repository, revision, path, i); if (sources?._embedded.children) { offset += sources._embedded.children.length; @@ -136,7 +135,6 @@ export default function reducer( } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { return { ...state, - [action.itemId + "hunkCount"]: action.payload.hunk + 1, [action.itemId + action.payload.hunk]: { sources: action.payload.sources, updatePending: true, From a05c371910a531a78380a63658379c22458daf0a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 11:06:21 +0100 Subject: [PATCH 016/111] Render truncated info --- scm-ui/ui-webapp/public/locales/de/repos.json | 4 ++- scm-ui/ui-webapp/public/locales/en/repos.json | 4 ++- .../src/repos/sources/components/FileTree.tsx | 25 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 27909f338c..2b7b0f2ca4 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -128,7 +128,9 @@ "noSources": "Keine Sources in diesem Branch gefunden.", "extension": { "notBound": "Keine Erweiterung angebunden." - } + }, + "loadMore": "Laden", + "moreEntriesAvailable": "Es werden nur die ersten {{count}} Einträge angezeigt. Es sind weitere Einträge vorhanden." }, "permission": { "title": "Berechtigungen bearbeiten", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 793c01388d..d5a5a7fff6 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -128,7 +128,9 @@ "noSources": "No sources found for this branch.", "extension": { "notBound": "No extension bound." - } + }, + "loadMore": "Load", + "moreEntriesAvailable": "These are just the first {{count}} entries. There are more entries available." }, "permission": { "title": "Edit Permissions", diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 881a841a57..df70b44500 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -89,8 +89,27 @@ class FileTree extends React.Component { this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; - render() { + renderTruncatedInfo = () => { const { hunks, t } = this.props; + const lastHunk = hunks[hunks.length - 1]; + const entryCount = hunks + .filter(hunk => hunk?.tree?._embedded?.children) + .map(hunk => hunk.tree._embedded.children.length) + .reduce((a, b) => a + b, 0); + if (lastHunk.tree?.truncated) { + return ( + +
+
{t("sources.moreEntriesAvailable", { count: entryCount })}
+
+
+ ); + } + }; + + render() { + const { hunks } = this.props; if (!hunks || hunks.length === 0) { return null; @@ -100,12 +119,10 @@ class FileTree extends React.Component { return hunk.error)[0]} />; } - const lastHunk = hunks[hunks.length - 1]; - return (
{this.renderSourcesTable()} - {lastHunk.tree?.truncated &&
); } From a8694eb668348738f659a8fbc090bdb07b051e70 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 11:50:04 +0100 Subject: [PATCH 017/111] Fix offset for svn repositories --- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index d37833c1c3..857229f8c2 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -50,7 +50,7 @@ export function fetchSources(repository: Repository, revision: string, path: str function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { - return base; + return `${base}?offset=${offset}`; } // TODO handle trailing slash From 4a82c541b293e198c602e598297f9d21efab19a9 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 12:49:43 +0100 Subject: [PATCH 018/111] Sort svn files --- .../scm/repository/spi/SvnBrowseCommand.java | 6 ++++- .../repository/spi/SvnBrowseCommandTest.java | 27 +++++++------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 1daaf5af61..a6e131fa4a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -51,9 +51,12 @@ import sonia.scm.repository.SubRepository; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; +import static java.util.Comparator.comparing; import static org.tmatesoft.svn.core.SVNErrorCode.FS_NO_SUCH_REVISION; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -130,7 +133,8 @@ public class SvnBrowseCommand extends AbstractSvnCommand FileObject parent, String basePath) throws SVNException { - Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); + List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); + entries.sort(comparing(SVNDirEntry::getName)); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 589f0d4107..0fc39c8174 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -39,6 +39,7 @@ import sonia.scm.repository.FileObject; import java.io.IOException; import java.util.Collection; +import java.util.Iterator; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -77,8 +78,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = foList1; - FileObject a = getFileObject(foList, "a.txt"); - FileObject c = getFileObject(foList, "c"); + Iterator iterator = foList.iterator(); + FileObject a = iterator.next(); + FileObject c = iterator.next(); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); @@ -113,20 +115,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertFalse(foList.isEmpty()); assertEquals(2, foList.size()); - FileObject d = null; - FileObject e = null; - - for (FileObject f : foList) - { - if ("d.txt".equals(f.getName())) - { - d = f; - } - else if ("e.txt".equals(f.getName())) - { - e = f; - } - } + Iterator iterator = foList.iterator(); + FileObject d = iterator.next(); + FileObject e = iterator.next(); assertNotNull(d); assertFalse(d.isDirectory()); @@ -198,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + assertThat(foList).extracting("name").containsExactly("a.txt"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -212,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c"); + assertThat(foList).extracting("name").containsExactly("c"); } /** From 736ea3d93f2b0097d57e431231bf184216be991e Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 14:29:09 +0100 Subject: [PATCH 019/111] Sort git files --- .../scm/repository/spi/BrowseCommand.java | 15 +++++ .../java/sonia/scm/repository/GitUtil.java | 6 +- .../scm/repository/spi/GitBrowseCommand.java | 66 +++++++++++++++---- .../scm/repository/spi/SvnBrowseCommand.java | 2 +- .../repository/spi/SvnBrowseCommandTest.java | 6 +- 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index ee37d6243e..7859c1845b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -38,6 +38,9 @@ package sonia.scm.repository.spi; import sonia.scm.repository.BrowserResult; import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; //~--- JDK imports ------------------------------------------------------------ @@ -60,4 +63,16 @@ public interface BrowseCommand * @throws IOException */ BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException; + + default void sort(List entries, Function isDirectory, Function nameOf) { + entries.sort((e1, e2) -> { + if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) { + return nameOf.apply(e1).toLowerCase(Locale.ENGLISH).compareTo(nameOf.apply(e2).toLowerCase(Locale.ENGLISH)); + } else if (isDirectory.apply(e1)) { + return -1; + } else { + return 1; + } + }); + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index a93c1b5d81..e816aaf76d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -749,9 +749,13 @@ public final class GitUtil } public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + return getLfsPointer(repo, blobId, attributes); + } + + public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, ObjectId blobId, Attributes attributes) throws IOException { Attribute filter = attributes.get("filter"); if (filter != null && "lfs".equals(filter.getValue())) { - ObjectId blobId = treeWalk.getObjectId(0); try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) { return of(LfsPointer.parseLfsPointer(is)); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index f53b7ec0a4..fe75e6d1b1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -69,7 +69,9 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -158,14 +160,14 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) + BrowseCommandRequest request, ObjectId revId, TreeEntry treeEntry) throws IOException { FileObject file = new FileObject(); - String path = treeWalk.getPathString(); + String path = treeEntry.getPathString(); - file.setName(treeWalk.getNameString()); + file.setName(treeEntry.getNameString()); file.setPath(path); SubRepository sub = null; @@ -183,7 +185,7 @@ public class GitBrowseCommand extends AbstractGitCommand } else { - ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); + ObjectLoader loader = repo.open(treeEntry.getObjectId()); file.setDirectory(loader.getType() == Constants.OBJ_TREE); @@ -195,7 +197,7 @@ public class GitBrowseCommand extends AbstractGitCommand try (RevWalk walk = new RevWalk(repo)) { commit = walk.parseCommit(revId); } - Optional lfsPointer = getLfsPointer(repo, path, commit, treeWalk); + Optional lfsPointer = getLfsPointer(repo, path, commit, treeEntry); if (lfsPointer.isPresent()) { setFileLengthFromLfsBlob(lfsPointer.get(), file); @@ -253,11 +255,18 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { - List files = Lists.newArrayList(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) - { + List entries = new ArrayList<>(); + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { + entries.add(new TreeEntry(repo, treeWalk)); + } + sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); - FileObject fileObject = createFileObject(repo, request, revId, treeWalk); + List files = Lists.newArrayList(); + Iterator entryIterator = entries.iterator(); + while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) + { + TreeEntry entry = entryIterator.next(); + FileObject fileObject = createFileObject(repo, request, revId, entry); if (!fileObject.getPath().startsWith(parent.getPath())) { parent.setChildren(files); return fileObject; @@ -298,7 +307,7 @@ public class GitBrowseCommand extends AbstractGitCommand currentDepth++; if (currentDepth >= limit) { - return createFileObject(repo, request, revId, treeWalk); + return createFileObject(repo, request, revId, new TreeEntry(repo, treeWalk)); } else { treeWalk.enterSubtree(); } @@ -338,11 +347,11 @@ public class GitBrowseCommand extends AbstractGitCommand return null; } - private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) { + private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeEntry treeWalk) { try { Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); - return GitUtil.getLfsPointer(repo, treeWalk, attributes); + return GitUtil.getLfsPointer(repo, treeWalk.getObjectId(), attributes); } catch (IOException e) { throw new InternalRepositoryException(repository, "could not read lfs pointer", e); } @@ -448,4 +457,37 @@ public class GitBrowseCommand extends AbstractGitCommand return changed; } } + + private static class TreeEntry { + + private final String pathString; + private final String nameString; + private final ObjectId objectId; + private final boolean directory; + + public TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { + this.pathString = treeWalk.getPathString(); + this.nameString = treeWalk.getNameString(); + this.objectId = treeWalk.getObjectId(0); + ObjectLoader loader = repo.open(objectId); + + this.directory = loader.getType() == Constants.OBJ_TREE; + } + + public String getPathString() { + return pathString; + } + + public String getNameString() { + return nameString; + } + + public ObjectId getObjectId() { + return objectId; + } + + public boolean isDirectory() { + return directory; + } + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index a6e131fa4a..423b82004a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -134,7 +134,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); - entries.sort(comparing(SVNDirEntry::getName)); + sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 0fc39c8174..1c6e228a6d 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -79,8 +79,8 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = foList1; Iterator iterator = foList.iterator(); - FileObject a = iterator.next(); FileObject c = iterator.next(); + FileObject a = iterator.next(); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); @@ -189,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("a.txt"); + assertThat(foList).extracting("name").containsExactly("c"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -203,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("c"); + assertThat(foList).extracting("name").containsExactly("a.txt"); } /** From f0da22ad296a6b3d67c354e4a6acab0ab0db0ce8 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 14:50:07 +0100 Subject: [PATCH 020/111] Fix git sort --- .../main/java/sonia/scm/repository/spi/GitBrowseCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index fe75e6d1b1..8d25504b86 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -256,7 +256,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List entries = new ArrayList<>(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { + while (treeWalk.next()) { entries.add(new TreeEntry(repo, treeWalk)); } sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); From 58625ba606dfd51ac957a34bd1cb57edf3ebaac6 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 15:00:44 +0100 Subject: [PATCH 021/111] Sort files with an upper case letter first --- .../scm/repository/spi/BrowseCommand.java | 2 +- .../scm/repository/spi/BrowseCommandTest.java | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index 7859c1845b..899bda077b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -67,7 +67,7 @@ public interface BrowseCommand default void sort(List entries, Function isDirectory, Function nameOf) { entries.sort((e1, e2) -> { if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) { - return nameOf.apply(e1).toLowerCase(Locale.ENGLISH).compareTo(nameOf.apply(e2).toLowerCase(Locale.ENGLISH)); + return nameOf.apply(e1).compareTo(nameOf.apply(e2)); } else if (isDirectory.apply(e1)) { return -1; } else { diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java new file mode 100644 index 0000000000..1ed39c8366 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java @@ -0,0 +1,70 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.BrowserResult; + +import java.io.IOException; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.d; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.f; + +class BrowseCommandTest implements BrowseCommand { + + @Test + void shouldSort() { + List entries = asList( + f("b.txt"), + f("a.txt"), + f("Dockerfile"), + f(".gitignore"), + d("src"), + f("README") + ); + + sort(entries, Entry::isDirecotry, Entry::getName); + + assertThat(entries).extracting("name") + .containsExactly( + "src", + ".gitignore", + "Dockerfile", + "README", + "a.txt", + "b.txt" + ); + } + + @Override + public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { + return null; + } + + static class Entry { + private final String name; + private final boolean direcotry; + + static Entry f(String name) { + return new Entry(name, false); + } + + static Entry d(String name) { + return new Entry(name, true); + } + + public Entry(String name, boolean direcotry) { + this.name = name; + this.direcotry = direcotry; + } + + public String getName() { + return name; + } + + public boolean isDirecotry() { + return direcotry; + } + } +} From 76665b4dbe5b9105edb39cd6a8855e365210d7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 25 Feb 2020 08:25:25 +0100 Subject: [PATCH 022/111] Sort hg files --- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 3 ++- .../java/sonia/scm/repository/spi/HgBrowseCommandTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 86e6175d55..51e33349cf 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -153,7 +153,8 @@ class File_Walker: return path def walk(self, structure, parent = ""): - for key, value in structure.iteritems(): + sortedItems = sorted(structure.iteritems(), key = lambda item: item[1]) + for key, value in sortedItems: if key == FILE_MARKER: if value: for v in value: diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index f74d036843..bf34a0ad92 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -190,7 +190,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "a.txt"); assertThat(root.isTruncated()).isTrue(); } @@ -205,7 +205,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + assertThat(foList).extracting("name").containsExactlyInAnyOrder("b.txt", "f.txt"); assertThat(root.isTruncated()).isFalse(); } From 7fe8b58e7d2aa147c6479efa26d2f4e6a98a8843 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 25 Feb 2020 09:49:23 +0100 Subject: [PATCH 023/111] make secondary navigation collapsable // save collapse status in local storage --- .../ui-components/src/navigation/NavLink.tsx | 10 ++- .../ui-components/src/navigation/Section.tsx | 46 ++++++++--- .../src/navigation/SubNavigation.tsx | 11 ++- .../src/repos/containers/RepositoryRoot.tsx | 78 ++++++++++++++----- scm-ui/ui-webapp/src/repos/modules/repos.ts | 16 +++- 5 files changed, 122 insertions(+), 39 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/NavLink.tsx b/scm-ui/ui-components/src/navigation/NavLink.tsx index f87351d496..9a858dc5af 100644 --- a/scm-ui/ui-components/src/navigation/NavLink.tsx +++ b/scm-ui/ui-components/src/navigation/NavLink.tsx @@ -10,6 +10,7 @@ type Props = { label: string; activeOnlyWhenExact?: boolean; activeWhenMatch?: (route: any) => boolean; + collapsed: boolean; }; class NavLink extends React.Component { @@ -23,7 +24,7 @@ class NavLink extends React.Component { } renderLink = (route: any) => { - const { to, icon, label } = this.props; + const { to, icon, label, collapsed } = this.props; let showIcon = null; if (icon) { @@ -36,9 +37,12 @@ class NavLink extends React.Component { return (
  • - + {showIcon} - {label} + {collapsed ? null : label}
  • ); diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index b6f0542506..7890afe442 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -1,20 +1,44 @@ -import React, { ReactNode } from "react"; +import React, { FC, ReactNode } from "react"; +import Icon from "../Icon"; +import { Button } from "../buttons"; +import styled from "styled-components"; type Props = { label: string; children?: ReactNode; + collapsed?: boolean; + onCollapse?: (newStatus: boolean) => void; }; -class Section extends React.Component { - render() { - const { label, children } = this.props; - return ( -
    -

    {label}

    -
      {children}
    -
    - ); +const SmallButton = styled(Button)` + height: 1.5rem; + width: 1rem; + position: absolute; + right: 1.5rem; + > { + outline: none; } -} +`; + +const MenuLabel = styled.p` + min-height: 2.5rem; +`; + +const Section: FC = ({ label, children, collapsed, onCollapse }) => { + const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { collapsed: collapsed })); + return ( +
    + + {collapsed ? "" : label} + {onCollapse && ( + + + + )} + +
      {childrenWithProps}
    +
    + ); +}; export default Section; diff --git a/scm-ui/ui-components/src/navigation/SubNavigation.tsx b/scm-ui/ui-components/src/navigation/SubNavigation.tsx index 258658561a..4cb9d2b306 100644 --- a/scm-ui/ui-components/src/navigation/SubNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/SubNavigation.tsx @@ -9,6 +9,8 @@ type Props = { activeOnlyWhenExact?: boolean; activeWhenMatch?: (route: any) => boolean; children?: ReactNode; + collapsed?: boolean; + onCollapsed?: (newStatus: boolean) => void; }; class SubNavigation extends React.Component { @@ -22,7 +24,7 @@ class SubNavigation extends React.Component { } renderLink = (route: any) => { - const { to, icon, label } = this.props; + const { to, icon, label, collapsed } = this.props; let defaultIcon = "fas fa-cog"; if (icon) { @@ -36,8 +38,11 @@ class SubNavigation extends React.Component { return (
  • - - {label} + + {collapsed ? "" : label} {children}
  • diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index e0452416f7..62b482cbaa 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -1,12 +1,18 @@ import React from "react"; import { connect } from "react-redux"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; -import { History } from "history"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { Repository } from "@scm-manager/ui-types"; import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; -import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; +import { + fetchRepoByName, + getFetchRepoFailure, + getRepository, + isFetchRepoPending, + isRepositoryMenuCollapsed, + switchRepositoryMenuCollapsed +} from "../modules/repos"; import RepositoryDetails from "../components/RepositoryDetails"; import EditRepo from "./EditRepo"; import BranchesOverview from "../branches/containers/BranchesOverview"; @@ -21,29 +27,46 @@ import CodeOverview from "../codeSection/containers/CodeOverview"; import ChangesetView from "./ChangesetView"; import SourceExtensions from "../sources/containers/SourceExtensions"; -type Props = WithTranslation & { - namespace: string; - name: string; - repository: Repository; - loading: boolean; - error: Error; - repoLink: string; - indexLinks: object; +type Props = RouteComponentProps & + WithTranslation & { + namespace: string; + name: string; + repository: Repository; + loading: boolean; + error: Error; + repoLink: string; + indexLinks: object; - // dispatch functions - fetchRepoByName: (link: string, namespace: string, name: string) => void; + // dispatch functions + fetchRepoByName: (link: string, namespace: string, name: string) => void; + }; - // context props - history: History; - match: any; +type State = { + collapsedMenu: boolean; }; -class RepositoryRoot extends React.Component { +class RepositoryRoot extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + collapsedMenu: isRepositoryMenuCollapsed() + }; + } componentDidMount() { const { fetchRepoByName, namespace, name, repoLink } = this.props; fetchRepoByName(repoLink, namespace, name); } + componentDidUpdate() { + if (this.state.collapsedMenu && this.isCollapseForbidden()) { + this.onCollapse(false); + } + } + + isCollapseForbidden= () => { + return this.props.location.pathname.includes("/settings/"); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 1); @@ -87,8 +110,13 @@ class RepositoryRoot extends React.Component { return `${url}/changesets`; }; + onCollapse = (newStatus: boolean) => { + this.setState({ collapsedMenu: newStatus }, () => switchRepositoryMenuCollapsed(newStatus)); + }; + render() { const { loading, error, indexLinks, repository, t } = this.props; + const { collapsedMenu } = this.state; if (error) { return ( @@ -119,7 +147,7 @@ class RepositoryRoot extends React.Component { return (
    -
    +
    @@ -169,9 +197,13 @@ class RepositoryRoot extends React.Component {
    -
    +
    -
    +
    this.onCollapse(!collapsedMenu) : undefined} + collapsed={collapsedMenu} + > { activeOnlyWhenExact={false} /> - + this.onCollapse(false)} + > diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index dbea9c24fd..ac9e8fd81b 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -155,7 +155,12 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error): // create repo -export function createRepo(link: string, repository: Repository, initRepository: boolean, callback?: (repo: Repository) => void) { +export function createRepo( + link: string, + repository: Repository, + initRepository: boolean, + callback?: (repo: Repository) => void +) { return function(dispatch: any) { dispatch(createRepoPending()); const repoLink = initRepository ? link + "?initialize=true" : link; @@ -436,3 +441,12 @@ export function getPermissionsLink(state: object, namespace: string, name: strin const repo = getRepository(state, namespace, name); return repo && repo._links ? repo._links.permissions.href : undefined; } + +const REPOSITORY_NAVIGATION_COLLAPSED = "repository-menu-collapsed"; + +export function isRepositoryMenuCollapsed() { + return localStorage.getItem(REPOSITORY_NAVIGATION_COLLAPSED) === "true"; +} +export function switchRepositoryMenuCollapsed(newStatus: boolean) { + localStorage.setItem(REPOSITORY_NAVIGATION_COLLAPSED, String(newStatus)); +} From eee6cad1d350f3d5e760bf0fda9fb1973429503c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 25 Feb 2020 17:15:23 +0100 Subject: [PATCH 024/111] make repository navigation fixed // add title for collapsed navigation items --- .../ui-components/src/navigation/NavLink.tsx | 5 ++-- .../ui-components/src/navigation/Section.tsx | 30 ++++++++++++++----- .../src/navigation/SubNavigation.tsx | 5 ++-- .../repos/components/RepositoryNavLink.tsx | 1 + .../src/repos/containers/RepositoryRoot.tsx | 4 +++ scm-ui/ui-webapp/src/repos/modules/repos.ts | 8 ++--- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/NavLink.tsx b/scm-ui/ui-components/src/navigation/NavLink.tsx index 9a858dc5af..bff7e71e1c 100644 --- a/scm-ui/ui-components/src/navigation/NavLink.tsx +++ b/scm-ui/ui-components/src/navigation/NavLink.tsx @@ -11,6 +11,7 @@ type Props = { activeOnlyWhenExact?: boolean; activeWhenMatch?: (route: any) => boolean; collapsed: boolean; + title?: string; }; class NavLink extends React.Component { @@ -24,7 +25,7 @@ class NavLink extends React.Component { } renderLink = (route: any) => { - const { to, icon, label, collapsed } = this.props; + const { to, icon, label, collapsed, title } = this.props; let showIcon = null; if (icon) { @@ -36,7 +37,7 @@ class NavLink extends React.Component { } return ( -
  • +
  • void; }; +const SectionContainer = styled.div` + position: ${props => (props.scrollPositionY > 210 ? "fixed" : "absolute")}; + top: ${props => props.scrollPositionY > 210 && "4.5rem"}; + width: ${props => (props.collapsed ? "5.5rem" : "20.5rem")}; +`; + const SmallButton = styled(Button)` height: 1.5rem; width: 1rem; position: absolute; right: 1.5rem; - > { - outline: none; - } `; const MenuLabel = styled.p` @@ -25,19 +27,31 @@ const MenuLabel = styled.p` `; const Section: FC = ({ label, children, collapsed, onCollapse }) => { + const [scrollPositionY, setScrollPositionY] = useState(0); + + useEffect(() => { + window.addEventListener("scroll", () => setScrollPositionY(window.pageYOffset)); + + return () => { + window.removeEventListener("scroll", () => setScrollPositionY(window.pageYOffset)); + }; + }, []); + const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { collapsed: collapsed })); + const arrowIcon = collapsed ? : ; + return ( -
    + {collapsed ? "" : label} {onCollapse && ( - + {arrowIcon} )}
      {childrenWithProps}
    -
    + ); }; diff --git a/scm-ui/ui-components/src/navigation/SubNavigation.tsx b/scm-ui/ui-components/src/navigation/SubNavigation.tsx index 4cb9d2b306..e254975e0c 100644 --- a/scm-ui/ui-components/src/navigation/SubNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/SubNavigation.tsx @@ -11,6 +11,7 @@ type Props = { children?: ReactNode; collapsed?: boolean; onCollapsed?: (newStatus: boolean) => void; + title?: string }; class SubNavigation extends React.Component { @@ -24,7 +25,7 @@ class SubNavigation extends React.Component { } renderLink = (route: any) => { - const { to, icon, label, collapsed } = this.props; + const { to, icon, label, collapsed, title } = this.props; let defaultIcon = "fas fa-cog"; if (icon) { @@ -37,7 +38,7 @@ class SubNavigation extends React.Component { } return ( -
  • +
  • boolean; activeOnlyWhenExact: boolean; icon?: string; + title?: string; }; /** diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 62b482cbaa..96f941c21a 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -209,6 +209,7 @@ class RepositoryRoot extends React.Component { to={`${url}/info`} icon="fas fa-info-circle" label={t("repositoryRoot.menu.informationNavLink")} + title={t("repositoryRoot.menu.informationNavLink")} /> { label={t("repositoryRoot.menu.branchesNavLink")} activeWhenMatch={this.matchesBranches} activeOnlyWhenExact={false} + title={t("repositoryRoot.menu.branchesNavLink")} /> { label={t("repositoryRoot.menu.sourcesNavLink")} activeWhenMatch={this.matchesCode} activeOnlyWhenExact={false} + title={t("repositoryRoot.menu.sourcesNavLink")} /> this.onCollapse(false)} + title={t("repositoryRoot.menu.settingsNavLink")} > diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index ac9e8fd81b..5dde4e4104 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -442,11 +442,11 @@ export function getPermissionsLink(state: object, namespace: string, name: strin return repo && repo._links ? repo._links.permissions.href : undefined; } -const REPOSITORY_NAVIGATION_COLLAPSED = "repository-menu-collapsed"; +const REPOSITORY_MENU_COLLAPSED = "repository-menu-collapsed"; export function isRepositoryMenuCollapsed() { - return localStorage.getItem(REPOSITORY_NAVIGATION_COLLAPSED) === "true"; + return localStorage.getItem(REPOSITORY_MENU_COLLAPSED) === "true"; } -export function switchRepositoryMenuCollapsed(newStatus: boolean) { - localStorage.setItem(REPOSITORY_NAVIGATION_COLLAPSED, String(newStatus)); +export function switchRepositoryMenuCollapsed(status: boolean) { + localStorage.setItem(REPOSITORY_MENU_COLLAPSED, String(status)); } From c0e0ed3d17eab175bbf658072f3c89428883f312 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 25 Feb 2020 17:34:41 +0100 Subject: [PATCH 025/111] Fix tests --- .../sonia/scm/repository/spi/GitBrowseCommandTest.java | 8 ++++---- .../ui-webapp/src/repos/sources/modules/sources.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index ce8214e2fa..23d3bfabca 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -87,7 +87,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList) .extracting("name") - .containsExactly("a.txt", "b.txt", "c", "f.txt"); + .containsExactly("c", "a.txt", "b.txt", "f.txt"); } @Test @@ -100,7 +100,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList) .extracting("name") - .containsExactly("a.txt", "c"); + .containsExactly("c", "a.txt"); } @Test @@ -207,7 +207,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList) .extracting("name") - .containsExactly("a.txt", "b.txt", "c", "f.txt"); + .containsExactly("c", "a.txt", "b.txt", "f.txt"); FileObject c = findFile(foList, "c"); @@ -262,7 +262,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").contains("c", "f.txt"); + assertThat(foList).extracting("name").contains("b.txt", "f.txt"); assertFalse(root.isTruncated()); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fdd7a9fc78..f49488a7df 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -118,7 +118,7 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository", () => { - fetchMock.getOnce(sourcesUrl, collection); + fetchMock.getOnce(sourcesUrl + "?offset=0", collection); const expectedActions = [ { @@ -182,7 +182,7 @@ describe("sources fetch", () => { }); it("should dispatch FETCH_SOURCES_FAILURE on server error", () => { - fetchMock.getOnce(sourcesUrl, { + fetchMock.getOnce(sourcesUrl + "?offset=0", { status: 500 }); From 72328159000d4cee7d91aaec7da68bce5294a381 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 26 Feb 2020 10:41:39 +0100 Subject: [PATCH 026/111] use react context to toggle collapsable repository menu --- .../src/__snapshots__/storyshots.test.ts.snap | 48 ++-- .../ui-components/src/navigation/NavLink.tsx | 2 +- .../ui-components/src/navigation/Section.tsx | 16 +- .../src/navigation/SubNavigation.tsx | 3 +- .../src/repos/containers/RepositoryRoot.tsx | 222 +++++++++--------- scm-ui/ui-webapp/src/repos/modules/repos.ts | 6 + 6 files changed, 160 insertions(+), 137 deletions(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 0ec9bb0c76..4fefa33757 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Diff Binaries 1`] = `
  • + +
    -
    - + + ); } } diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index 5dde4e4104..b2c1150fc3 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -3,6 +3,7 @@ import * as types from "../../modules/types"; import { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; +import React from "react"; export const FETCH_REPOS = "scm/repos/FETCH_REPOS"; export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`; @@ -450,3 +451,8 @@ export function isRepositoryMenuCollapsed() { export function switchRepositoryMenuCollapsed(status: boolean) { localStorage.setItem(REPOSITORY_MENU_COLLAPSED, String(status)); } + +export const RepositoryContext = React.createContext({ + menuCollapsed: isRepositoryMenuCollapsed(), + toggleMenuCollapsed: () => {} +}); From 67192a203e7500854690f874a424e7188ea75430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 10:54:16 +0100 Subject: [PATCH 027/111] Read and sort tree first before applying limit --- .../scm/repository/spi/GitBrowseCommand.java | 73 +++++++++++++------ .../repository/spi/GitBrowseCommandTest.java | 4 +- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 8d25504b86..5bd25e549b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -77,6 +77,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -254,34 +255,26 @@ public class GitBrowseCommand extends AbstractGitCommand return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath()); } - private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { - List entries = new ArrayList<>(); - while (treeWalk.next()) { - entries.add(new TreeEntry(repo, treeWalk)); - } - sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); + private void findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { + TreeEntry entry = new TreeEntry(); + createTree(parent.getPath(), entry, repo, request, treeWalk); + convertToFileObject(parent, repo, request, revId, entry.getChildren()); + } + private void convertToFileObject(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, List entries) throws IOException { List files = Lists.newArrayList(); Iterator entryIterator = entries.iterator(); while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) { TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); - if (!fileObject.getPath().startsWith(parent.getPath())) { - parent.setChildren(files); - return fileObject; - } if (resultCount > request.getOffset()) { files.add(fileObject); } if (request.isRecursive() && fileObject.isDirectory()) { - treeWalk.enterSubtree(); - FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); - if (rc != null) { - files.add(rc); - } + convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } } @@ -290,8 +283,27 @@ public class GitBrowseCommand extends AbstractGitCommand if (resultCount > request.getLimit() + request.getOffset()) { parent.setTruncated(true); } + } - return null; + private Optional createTree(String path, TreeEntry parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, TreeWalk treeWalk) throws IOException { + List entries = new ArrayList<>(); + while (treeWalk.next()) { + TreeEntry treeEntry = new TreeEntry(repo, treeWalk); + if (!treeEntry.getPathString().startsWith(path)) { + parent.setChildren(entries); + return of(treeEntry); + } + + entries.add(treeEntry); + + if (request.isRecursive() && treeEntry.isDirectory()) { + treeWalk.enterSubtree(); + Optional surplus = createTree(treeEntry.getNameString(), treeEntry, repo, request, treeWalk); + surplus.ifPresent(entries::add); + } + } + parent.setChildren(entries); + return empty(); } private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, @@ -458,14 +470,22 @@ public class GitBrowseCommand extends AbstractGitCommand } } - private static class TreeEntry { + private class TreeEntry { private final String pathString; private final String nameString; private final ObjectId objectId; private final boolean directory; + private List children = emptyList(); - public TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { + TreeEntry() { + pathString = ""; + nameString = ""; + objectId = null; + directory = true; + } + + TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { this.pathString = treeWalk.getPathString(); this.nameString = treeWalk.getNameString(); this.objectId = treeWalk.getObjectId(0); @@ -474,20 +494,29 @@ public class GitBrowseCommand extends AbstractGitCommand this.directory = loader.getType() == Constants.OBJ_TREE; } - public String getPathString() { + String getPathString() { return pathString; } - public String getNameString() { + String getNameString() { return nameString; } - public ObjectId getObjectId() { + ObjectId getObjectId() { return objectId; } - public boolean isDirectory() { + boolean isDirectory() { return directory; } + + List getChildren() { + return children; + } + + void setChildren(List children) { + sort(children, TreeEntry::isDirectory, TreeEntry::getNameString); + this.children = children; + } } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 23d3bfabca..53a5bb0296 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -175,7 +175,9 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).hasSize(2); + assertThat(foList) + .extracting("name") + .containsExactly("d.txt", "e.txt"); FileObject d = findFile(foList, "d.txt"); FileObject e = findFile(foList, "e.txt"); From 4e7381b98f0cc3d352731329088f591764172d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 11:10:01 +0100 Subject: [PATCH 028/111] Fix offset in recursion --- .../scm/repository/spi/GitBrowseCommand.java | 8 +-- .../repository/spi/GitBrowseCommandTest.java | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 5bd25e549b..f4b6f15eba 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -269,13 +269,13 @@ public class GitBrowseCommand extends AbstractGitCommand TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); - if (resultCount > request.getOffset()) { - files.add(fileObject); - } - if (request.isRecursive() && fileObject.isDirectory()) { convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } + + if (resultCount > request.getOffset()) { + files.add(fileObject); + } } parent.setChildren(files); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 53a5bb0296..8684837d37 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -268,6 +268,75 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertFalse(root.isTruncated()); } + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 77100888655635760c8b9f9bd38cb77c62e88d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 13:24:31 +0100 Subject: [PATCH 029/111] Fix offset in recursion --- .../scm/repository/spi/SvnBrowseCommand.java | 11 +-- .../repository/spi/SvnBrowseCommandTest.java | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 423b82004a..274155fd55 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -135,17 +135,18 @@ public class SvnBrowseCommand extends AbstractSvnCommand { List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); - for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ) { + ++resultCount; SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - if (resultCount >= request.getOffset()) { - parent.addChild(child); - } - if (child.isDirectory() && request.isRecursive()) { traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } + + if (resultCount > request.getOffset()) { + parent.addChild(child); + } } if (resultCount >= request.getLimit() + request.getOffset()) { parent.setTruncated(true); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 1c6e228a6d..f0bdb38c4a 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -206,6 +206,75 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertThat(foList).extracting("name").containsExactly("a.txt"); } + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + /** * Method description * From 119236a2276e48eeca737455fa79c74968aac931 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 26 Feb 2020 15:10:56 +0100 Subject: [PATCH 030/111] move MenuContext to ui-components --- scm-ui/ui-components/src/contexts/MenuContext.ts | 15 +++++++++++++++ scm-ui/ui-components/src/contexts/index.ts | 3 +++ scm-ui/ui-components/src/index.ts | 1 + scm-ui/ui-webapp/src/repos/modules/repos.ts | 14 -------------- 4 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 scm-ui/ui-components/src/contexts/MenuContext.ts create mode 100644 scm-ui/ui-components/src/contexts/index.ts diff --git a/scm-ui/ui-components/src/contexts/MenuContext.ts b/scm-ui/ui-components/src/contexts/MenuContext.ts new file mode 100644 index 0000000000..fcd83ce801 --- /dev/null +++ b/scm-ui/ui-components/src/contexts/MenuContext.ts @@ -0,0 +1,15 @@ +import React from "react"; + +const MENU_COLLAPSED = "secondary-menu-collapsed"; + +export const MenuContext = React.createContext({ + menuCollapsed: localStorage.getItem(MENU_COLLAPSED) === "true", + setMenuCollapsed: (collapsed: boolean) => {} +}); + +export function isMenuCollapsed() { + return localStorage.getItem(MENU_COLLAPSED) === "true"; +} +export function storeMenuCollapsed(status: boolean) { + localStorage.setItem(MENU_COLLAPSED, String(status)); +} diff --git a/scm-ui/ui-components/src/contexts/index.ts b/scm-ui/ui-components/src/contexts/index.ts new file mode 100644 index 0000000000..56f9c0126a --- /dev/null +++ b/scm-ui/ui-components/src/contexts/index.ts @@ -0,0 +1,3 @@ +// @create-index + +export { MenuContext, storeMenuCollapsed, isMenuCollapsed } from "./MenuContext"; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 46ccbb48ec..566bcf38aa 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -67,6 +67,7 @@ export * from "./navigation"; export * from "./repos"; export * from "./table"; export * from "./toast"; +export * from "./contexts"; export { File, diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index b2c1150fc3..38da52611f 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -442,17 +442,3 @@ export function getPermissionsLink(state: object, namespace: string, name: strin const repo = getRepository(state, namespace, name); return repo && repo._links ? repo._links.permissions.href : undefined; } - -const REPOSITORY_MENU_COLLAPSED = "repository-menu-collapsed"; - -export function isRepositoryMenuCollapsed() { - return localStorage.getItem(REPOSITORY_MENU_COLLAPSED) === "true"; -} -export function switchRepositoryMenuCollapsed(status: boolean) { - localStorage.setItem(REPOSITORY_MENU_COLLAPSED, String(status)); -} - -export const RepositoryContext = React.createContext({ - menuCollapsed: isRepositoryMenuCollapsed(), - toggleMenuCollapsed: () => {} -}); From ed53745d9f083868b80c3a348b583708a6aa035a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 26 Feb 2020 15:45:24 +0100 Subject: [PATCH 031/111] make secondary navigation also for user, group and administration collapsable --- .../ui-webapp/src/admin/containers/Admin.tsx | 221 +++++++++++------- .../src/groups/containers/SingleGroup.tsx | 133 +++++++---- .../src/repos/containers/RepositoryRoot.tsx | 160 ++++++------- .../src/users/containers/SingleUser.tsx | 132 +++++++---- 4 files changed, 400 insertions(+), 246 deletions(-) diff --git a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx index 23b1a58c65..82b564a607 100644 --- a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx @@ -2,11 +2,18 @@ import React from "react"; import { connect } from "react-redux"; import { compose } from "redux"; import { WithTranslation, withTranslation } from "react-i18next"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { History } from "history"; +import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Links } from "@scm-manager/ui-types"; -import { Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + Navigation, + NavLink, + Page, + Section, + SubNavigation, + isMenuCollapsed, + MenuContext +} from "@scm-manager/ui-components"; import { getAvailablePluginsLink, getInstalledPluginsLink, getLinks } from "../../modules/indexResource"; import AdminDetails from "./AdminDetails"; import PluginsOverview from "../plugins/containers/PluginsOverview"; @@ -14,18 +21,44 @@ import GlobalConfig from "./GlobalConfig"; import RepositoryRoles from "../roles/containers/RepositoryRoles"; import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole"; import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - links: Links; - availablePluginsLink: string; - installedPluginsLink: string; +type Props = RouteComponentProps & + WithTranslation & { + links: Links; + availablePluginsLink: string; + installedPluginsLink: string; + }; - // context objects - match: any; - history: History; +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -class Admin extends React.Component { +class Admin extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/") || this.props.location.pathname.includes("/plugins/"); + }; + + onCollapseAdminMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { if (url.includes("role")) { @@ -48,6 +81,7 @@ class Admin extends React.Component { render() { const { links, availablePluginsLink, installedPluginsLink, t } = this.props; + const { menuCollapsed } = this.state; const url = this.matchedUrl(); const extensionProps = { @@ -56,82 +90,99 @@ class Admin extends React.Component { }; return ( - -
    -
    - - - - - - } - /> - } - /> - } - /> - } - /> - } - /> - } /> - } - /> - } /> - - -
    -
    - -
    - - {(availablePluginsLink || installedPluginsLink) && ( - - {installedPluginsLink && ( - - )} - {availablePluginsLink && ( - - )} - - )} - + +
    +
    + + + + + + } /> - - - - - -
    -
    + } + /> + } + /> + } + /> + } + /> + } /> + } + /> + } /> + + +
    +
    + +
    this.onCollapseAdminMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + {(availablePluginsLink || installedPluginsLink) && ( + + {installedPluginsLink && ( + + )} + {availablePluginsLink && ( + + )} + + )} + + + + + + +
    +
    +
    -
    -
    + + ); } } diff --git a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx index 90e15b5e43..c80a3225c9 100644 --- a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx @@ -1,38 +1,73 @@ import React from "react"; import { connect } from "react-redux"; -import { Route } from "react-router-dom"; +import { Route, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; -import { History } from "history"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Group } from "@scm-manager/ui-types"; -import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + ErrorPage, + Loading, + Navigation, + NavLink, + Page, + Section, + SubNavigation, + isMenuCollapsed, + MenuContext +} from "@scm-manager/ui-components"; import { getGroupsLink } from "../../modules/indexResource"; import { fetchGroupByName, getFetchGroupFailure, getGroupByName, isFetchGroupPending } from "../modules/groups"; import { Details } from "./../components/table"; import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import EditGroup from "./EditGroup"; import SetPermissions from "../../permissions/components/SetPermissions"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - name: string; - group: Group; - loading: boolean; - error: Error; - groupLink: string; +type Props = RouteComponentProps & + WithTranslation & { + name: string; + group: Group; + loading: boolean; + error: Error; + groupLink: string; - // dispatcher functions - fetchGroupByName: (p1: string, p2: string) => void; + // dispatcher functions + fetchGroupByName: (p1: string, p2: string) => void; + }; - // context objects - match: any; - history: History; +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -class SingleGroup extends React.Component { +class SingleGroup extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + componentDidMount() { this.props.fetchGroupByName(this.props.groupLink, this.props.name); } + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/"); + }; + + onCollapseGroupMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -46,6 +81,7 @@ class SingleGroup extends React.Component { render() { const { t, loading, error, group } = this.props; + const { menuCollapsed } = this.state; if (error) { return ; @@ -63,33 +99,48 @@ class SingleGroup extends React.Component { }; return ( - -
    -
    -
    } /> - } /> - } - /> - + + +
    +
    +
    } /> + } /> + } + /> + +
    +
    + +
    this.onCollapseGroupMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + + + + + + +
    +
    +
    -
    - -
    - - - - - - - -
    -
    -
    -
    - + + ); } } diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 06184842ff..c436219e8b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -4,16 +4,19 @@ import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { Repository } from "@scm-manager/ui-types"; -import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; import { - fetchRepoByName, - getFetchRepoFailure, - getRepository, - isFetchRepoPending, - isRepositoryMenuCollapsed, - switchRepositoryMenuCollapsed, - RepositoryContext -} from "../modules/repos"; + ErrorPage, + Loading, + Navigation, + NavLink, + Page, + Section, + SubNavigation, + MenuContext, + storeMenuCollapsed, + isMenuCollapsed +} from "@scm-manager/ui-components"; +import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; import RepositoryDetails from "../components/RepositoryDetails"; import EditRepo from "./EditRepo"; import BranchesOverview from "../branches/containers/BranchesOverview"; @@ -43,24 +46,28 @@ type Props = RouteComponentProps & }; type State = { - collapsedRepositoryMenu: boolean; + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; class RepositoryRoot extends React.Component { constructor(props: Props) { super(props); + this.state = { - collapsedRepositoryMenu: isRepositoryMenuCollapsed() + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }; } + componentDidMount() { const { fetchRepoByName, namespace, name, repoLink } = this.props; fetchRepoByName(repoLink, namespace, name); } componentDidUpdate() { - if (this.state.collapsedRepositoryMenu && this.isCollapseForbidden()) { - this.onCollapseRepositoryMenu(false); + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); } } @@ -111,13 +118,13 @@ class RepositoryRoot extends React.Component { return `${url}/changesets`; }; - onCollapseRepositoryMenu = (status: boolean) => { - this.setState({ collapsedRepositoryMenu: status }, () => switchRepositoryMenuCollapsed(status)); + onCollapseRepositoryMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); }; render() { const { loading, error, indexLinks, repository, t } = this.props; - const { collapsedRepositoryMenu } = this.state; + const { menuCollapsed } = this.state; if (error) { return ( @@ -135,7 +142,7 @@ class RepositoryRoot extends React.Component { repository, url, indexLinks, - collapsedRepositoryMenu + collapsedRepositoryMenu: menuCollapsed }; const redirectUrlFactory = binder.getExtension("repository.redirect", this.props); @@ -147,15 +154,10 @@ class RepositoryRoot extends React.Component { } return ( - this.setState({ collapsedRepositoryMenu: !this.state.collapsedRepositoryMenu }) - }} - > - -
    -
    + +
    +
    + @@ -204,61 +206,59 @@ class RepositoryRoot extends React.Component { } /> -
    -
    - -
    this.onCollapseRepositoryMenu(!collapsedRepositoryMenu) - } - collapsed={collapsedRepositoryMenu} - > - - - - - - - - - - -
    -
    -
    +
    -
    - +
    + +
    this.onCollapseRepositoryMenu(!menuCollapsed) + } + collapsed={menuCollapsed} + > + + + + + + + + + + +
    +
    +
    +
    + ); } } diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx index 8b0826ee6b..9d0dd2b9c5 100644 --- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx @@ -1,10 +1,20 @@ import React from "react"; import { connect } from "react-redux"; -import { Route } from "react-router-dom"; +import { Route, RouteComponentProps } from "react-router-dom"; import { History } from "history"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { User } from "@scm-manager/ui-types"; -import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + ErrorPage, + Loading, + Navigation, + NavLink, + Page, + Section, + SubNavigation, + MenuContext, + isMenuCollapsed +} from "@scm-manager/ui-components"; import { Details } from "./../components/table"; import EditUser from "./EditUser"; import { fetchUserByName, getFetchUserFailure, getUserByName, isFetchUserPending } from "../modules/users"; @@ -13,27 +23,45 @@ import { WithTranslation, withTranslation } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; import SetPermissions from "../../permissions/components/SetPermissions"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - name: string; - user: User; - loading: boolean; - error: Error; - usersLink: string; +type Props = RouteComponentProps & + WithTranslation & { + name: string; + user: User; + loading: boolean; + error: Error; + usersLink: string; - // dispatcher function - fetchUserByName: (p1: string, p2: string) => void; + // dispatcher function + fetchUserByName: (p1: string, p2: string) => void; + }; - // context objects - match: any; - history: History; +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -class SingleUser extends React.Component { +class SingleUser extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + componentDidMount() { this.props.fetchUserByName(this.props.usersLink, this.props.name); } + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -41,12 +69,21 @@ class SingleUser extends React.Component { return url; }; + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/"); + }; + + onCollapseUserMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + matchedUrl = () => { return this.stripEndingSlash(this.props.match.url); }; render() { const { t, loading, error, user } = this.props; + const { menuCollapsed } = this.state; if (error) { return ; @@ -64,33 +101,48 @@ class SingleUser extends React.Component { }; return ( - -
    -
    -
    } /> - } /> - } /> - } - /> - + + +
    +
    +
    } /> + } /> + } /> + } + /> + +
    +
    + +
    this.onCollapseUserMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + + + + + + +
    +
    +
    -
    - -
    - - - - - - - -
    -
    -
    -
    - + + ); } } From ffdd80df6168e2ae9cadef6942f04f9c74caaece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 15:59:03 +0100 Subject: [PATCH 032/111] Correct hash and equals for cache --- .../java/sonia/scm/repository/spi/BrowseCommandRequest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 4477a26e31..c50ab06cee 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -116,7 +116,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) - && Objects.equal(offset, other.offset); + && Objects.equal(offset, other.offset) + && Objects.equal(limit, other.limit); } /** @@ -129,7 +130,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection, offset); + disableSubRepositoryDetection, offset, limit); } /** From 8ff870ccb1411f4456c6d5ec20b2dd5fdccdb7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 16:05:49 +0100 Subject: [PATCH 033/111] Remove unused property --- .../scm/repository/spi/FileBaseCommandRequest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 0a2192897a..9f563345fd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,10 +147,6 @@ public abstract class FileBaseCommandRequest this.revision = revision; } - public void setLimit(int limit) { - this.limit = limit; - } - //~--- get methods ---------------------------------------------------------- /** @@ -175,10 +171,6 @@ public abstract class FileBaseCommandRequest return revision; } - public int getLimit() { - return limit; - } - //~--- methods -------------------------------------------------------------- /** @@ -216,6 +208,4 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; - - private int limit; } From 1567cd87653a33479da7c2500910b191b2d7088c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 17:36:41 +0100 Subject: [PATCH 034/111] Add tests for recursive request --- .../repository/spi/HgBrowseCommandTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index bf34a0ad92..c017d0632c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -209,6 +209,76 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertThat(root.isTruncated()).isFalse(); } + + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + //~--- get methods ---------------------------------------------------------- /** From f8720087a14e6f81cff9c207d0af33d0831c32ef Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 09:27:02 +0100 Subject: [PATCH 035/111] refactor --- scm-ui/ui-webapp/src/containers/Profile.tsx | 106 +++++++++++++----- .../src/groups/containers/SingleGroup.tsx | 6 +- .../src/users/containers/SingleUser.tsx | 7 +- 3 files changed, 86 insertions(+), 33 deletions(-) diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index 4852993afd..68350deca5 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -1,24 +1,62 @@ import React from "react"; -import { Route, withRouter } from "react-router-dom"; +import { Route, RouteComponentProps, withRouter } from "react-router-dom"; import { getMe } from "../modules/auth"; import { compose } from "redux"; import { connect } from "react-redux"; import { WithTranslation, withTranslation } from "react-i18next"; import { Me } from "@scm-manager/ui-types"; -import { ErrorPage, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + ErrorPage, + isMenuCollapsed, + MenuContext, + Navigation, + NavLink, + Page, + Section, + SubNavigation +} from "@scm-manager/ui-components"; import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - me: Me; +type Props = RouteComponentProps & + WithTranslation & { + me: Me; - // Context props - match: any; + // Context props + match: any; + }; + +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -type State = {}; class Profile extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/"); + }; + + onCollapseProfileMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -34,6 +72,7 @@ class Profile extends React.Component { const url = this.matchedUrl(); const { me, t } = this.props; + const { menuCollapsed } = this.state; if (!me) { return ( @@ -54,26 +93,41 @@ class Profile extends React.Component { }; return ( - -
    -
    - } /> - } /> - + + +
    +
    + } /> + } /> + +
    +
    + +
    this.onCollapseProfileMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + + + + +
    +
    +
    -
    - -
    - - - - - -
    -
    -
    -
    - + + ); } } diff --git a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx index c80a3225c9..ddcb387ca7 100644 --- a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx @@ -6,14 +6,14 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Group } from "@scm-manager/ui-types"; import { ErrorPage, + isMenuCollapsed, Loading, + MenuContext, Navigation, NavLink, Page, Section, - SubNavigation, - isMenuCollapsed, - MenuContext + SubNavigation } from "@scm-manager/ui-components"; import { getGroupsLink } from "../../modules/indexResource"; import { fetchGroupByName, getFetchGroupFailure, getGroupByName, isFetchGroupPending } from "../modules/groups"; diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx index 9d0dd2b9c5..d2718650bd 100644 --- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx @@ -1,19 +1,18 @@ import React from "react"; import { connect } from "react-redux"; import { Route, RouteComponentProps } from "react-router-dom"; -import { History } from "history"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { User } from "@scm-manager/ui-types"; import { ErrorPage, + isMenuCollapsed, Loading, + MenuContext, Navigation, NavLink, Page, Section, - SubNavigation, - MenuContext, - isMenuCollapsed + SubNavigation } from "@scm-manager/ui-components"; import { Details } from "./../components/table"; import EditUser from "./EditUser"; From f9b680f548ece6959ca7fe1fad4f12067d5a919a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 10:00:29 +0100 Subject: [PATCH 036/111] fix responsiveness for section --- .../ui-components/src/navigation/Section.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index 2214b974c5..c4af45e203 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -1,33 +1,33 @@ -import React, {FC, ReactChild, useEffect, useState} from "react"; +import React, { FC, ReactElement, useEffect, useState } from "react"; import { Button } from "../buttons"; import styled from "styled-components"; type Props = { label: string; - children?: ReactChild; + children: ReactElement[]; collapsed?: boolean; onCollapse?: (newStatus: boolean) => void; }; +type StylingProps = { + scrollPositionY: number; + collapsed: boolean; +}; const SectionContainer = styled.div` -// @ts-ignore - position: ${props => (props.scrollPositionY > 210 ? "fixed" : "absolute")}; - // @ts-ignore - top: ${props => props.scrollPositionY > 210 && "4.5rem"}; - // @ts-ignore - width: ${props => (props.collapsed ? "5.5rem" : "20.5rem")}; + position: ${(props: StylingProps) => (props.scrollPositionY > 210 && window.innerWidth > 770 ? "fixed" : "inherit")}; + top: ${(props: StylingProps) => props.scrollPositionY > 210 && window.innerWidth > 770 && "4.5rem"}; + width: ${(props: StylingProps) => (props.collapsed ? "5.5rem" : "20.5rem")}; `; const SmallButton = styled(Button)` height: 1.5rem; - width: 1rem; - position: absolute; - right: 1.5rem; `; const MenuLabel = styled.p` min-height: 2.5rem; + display: flex; + justify-content: ${(props: StylingProps) => (props.collapsed ? "center" : "space-between")}; `; const Section: FC = ({ label, children, collapsed, onCollapse }) => { @@ -41,14 +41,14 @@ const Section: FC = ({ label, children, collapsed, onCollapse }) => { }; }, []); - // @ts-ignore - const childrenWithProps = React.Children.map(children, (child: ReactChild) => React.cloneElement(child, { collapsed: collapsed })); + const childrenWithProps = React.Children.map(children, (child: ReactElement) => + React.cloneElement(child, { collapsed: collapsed }) + ); const arrowIcon = collapsed ? : ; return ( - // @ts-ignore - - + + {collapsed ? "" : label} {onCollapse && ( onCollapse(!collapsed)}> From d59778c3bd24e2bd41f715ef878a876643f007b7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 16:41:19 +0100 Subject: [PATCH 037/111] fix secondary navigation responsiveness --- scm-ui/ui-components/src/navigation/Section.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index c4af45e203..951ac71a3a 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -27,7 +27,7 @@ const SmallButton = styled(Button)` const MenuLabel = styled.p` min-height: 2.5rem; display: flex; - justify-content: ${(props: StylingProps) => (props.collapsed ? "center" : "space-between")}; + justify-content: ${(props: { collapsed: boolean }) => (props.collapsed ? "center" : "space-between")}; `; const Section: FC = ({ label, children, collapsed, onCollapse }) => { @@ -48,10 +48,10 @@ const Section: FC = ({ label, children, collapsed, onCollapse }) => { return ( - + {collapsed ? "" : label} {onCollapse && ( - onCollapse(!collapsed)}> + onCollapse(!collapsed)}> {arrowIcon} )} From 2d038327d0ceb7ba899c18c8c92419619b921b75 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 13:15:27 +0100 Subject: [PATCH 038/111] fix re-render bug for changesets --- scm-ui/ui-components/src/repos/DiffFile.tsx | 26 +++++++++----- .../src/repos/containers/Changesets.tsx | 34 ++++++++++--------- .../src/repos/containers/RepositoryRoot.tsx | 14 +++++--- .../src/repos/modules/changesets.test.ts | 34 +++++++++++++++++++ .../ui-webapp/src/repos/modules/changesets.ts | 14 ++++++-- 5 files changed, 89 insertions(+), 33 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index edf1d8c79a..90a4239971 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -10,6 +10,7 @@ import Icon from "../Icon"; import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes"; import TokenizedDiffView from "./TokenizedDiffView"; import DiffButton from "./DiffButton"; +import { MenuContext } from "@scm-manager/ui-components"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -100,10 +101,13 @@ class DiffFile extends React.Component { } }; - toggleSideBySide = () => { - this.setState(state => ({ - sideBySide: !state.sideBySide - })); + toggleSideBySide = (callback: (collapsed: boolean) => void) => { + this.setState( + state => ({ + sideBySide: !state.sideBySide + }), + () => callback(this.state.sideBySide ? this.state.sideBySide : false) + ); }; setCollapse = (collapsed: boolean) => { @@ -259,11 +263,15 @@ class DiffFile extends React.Component { file.hunks && file.hunks.length > 0 ? ( - + + {({ setMenuCollapsed }) => ( + this.toggleSideBySide(() => setMenuCollapsed(sideBySide ? true : false))} + /> + )} + {fileControls} diff --git a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx index a5ca949cee..9b58662805 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import { compose } from "redux"; -import { withRouter } from "react-router-dom"; +import { withRouter, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; import { Branch, Changeset, PagedCollection, Repository } from "@scm-manager/ui-types"; import { @@ -20,23 +20,21 @@ import { selectListAsCollection } from "../modules/changesets"; -type Props = WithTranslation & { - repository: Repository; - branch: Branch; - page: number; +type Props = RouteComponentProps & + WithTranslation & { + repository: Repository; + branch: Branch; + page: number; - // State props - changesets: Changeset[]; - list: PagedCollection; - loading: boolean; - error: Error; + // State props + changesets: Changeset[]; + list: PagedCollection; + loading: boolean; + error: Error; - // Dispatch props - fetchChangesets: (p1: Repository, p2: Branch, p3: number) => void; - - // context props - match: any; -}; + // Dispatch props + fetchChangesets: (p1: Repository, p2: Branch, p3: number) => void; + }; class Changesets extends React.Component { componentDidMount() { @@ -44,6 +42,10 @@ class Changesets extends React.Component { fetchChangesets(repository, branch, page); } + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly<{}>, nextContext: any): boolean { + return this.props.changesets !== nextProps.changesets; + } + render() { const { changesets, loading, error, t } = this.props; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index c436219e8b..3d65d16cb2 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -138,10 +138,14 @@ class RepositoryRoot extends React.Component { const url = this.matchedUrl(); - const extensionProps = { + const extensionProps: any = { repository, url, - indexLinks, + indexLinks + }; + + const navExtensionProps = { + ...extensionProps, collapsedRepositoryMenu: menuCollapsed }; @@ -217,7 +221,7 @@ class RepositoryRoot extends React.Component { } collapsed={menuCollapsed} > - + { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.sourcesNavLink")} /> - + { > - + diff --git a/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts b/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts index b0d9675c40..45191b1e69 100644 --- a/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts +++ b/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts @@ -581,6 +581,31 @@ describe("changesets", () => { ]); }); + it("should return always the same changeset array for the given parameters", () => { + const state = { + changesets: { + "foo/bar": { + byId: { + id2: { + id: "id2" + }, + id1: { + id: "id1" + } + }, + byBranch: { + "": { + entries: ["id1", "id2"] + } + } + } + } + }; + const one = getChangesets(state, repository); + const two = getChangesets(state, repository); + expect(one).toBe(two); + }); + it("should return true, when fetching changesets is pending", () => { const state = { pending: { @@ -639,5 +664,14 @@ describe("changesets", () => { expect(collection.page).toBe(1); expect(collection.pageTotal).toBe(10); }); + + it("should return always the same empty object", () => { + const state = { + changesets: {} + }; + const one = selectListAsCollection(state, repository); + const two = selectListAsCollection(state, repository); + expect(one).toBe(two); + }); }); }); diff --git a/scm-ui/ui-webapp/src/repos/modules/changesets.ts b/scm-ui/ui-webapp/src/repos/modules/changesets.ts index 5be98cf4df..3e75a1668d 100644 --- a/scm-ui/ui-webapp/src/repos/modules/changesets.ts +++ b/scm-ui/ui-webapp/src/repos/modules/changesets.ts @@ -3,6 +3,7 @@ import { apiClient, urls } from "@scm-manager/ui-components"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; import { Action, Branch, PagedCollection, Repository } from "@scm-manager/ui-types"; +import memoizeOne from "memoize-one"; export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS"; export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`; @@ -254,10 +255,15 @@ export function getChangesets(state: object, repository: Repository, branch?: Br return null; } + return collectChangesets(stateRoot, changesets); +} +const mapChangesets = (stateRoot, changesets) => { return changesets.entries.map((id: string) => { return stateRoot.byId[id]; }); -} +}; + +const collectChangesets = memoizeOne(mapChangesets); export function getChangeset(state: object, repository: Repository, id: string) { const key = createItemId(repository); @@ -291,6 +297,8 @@ export function getFetchChangesetsFailure(state: object, repository: Repository, return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch)); } +const EMPTY = {}; + const selectList = (state: object, repository: Repository, branch?: Branch) => { const repoId = createItemId(repository); @@ -302,7 +310,7 @@ const selectList = (state: object, repository: Repository, branch?: Branch) => { return repoState.byBranch[branchName]; } } - return {}; + return EMPTY; }; const selectListEntry = (state: object, repository: Repository, branch?: Branch): object => { @@ -310,7 +318,7 @@ const selectListEntry = (state: object, repository: Repository, branch?: Branch) if (list.entry) { return list.entry; } - return {}; + return EMPTY; }; export const selectListAsCollection = (state: object, repository: Repository, branch?: Branch): PagedCollection => { From 4df33fcc7396a924ce890c259670280daae27792 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 14:58:56 +0100 Subject: [PATCH 039/111] fix styling --- scm-ui/ui-components/src/navigation/Section.tsx | 2 ++ scm-ui/ui-components/src/repos/DiffFile.tsx | 2 +- .../src/repos/containers/RepositoryRoot.tsx | 12 ++++-------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index 951ac71a3a..5b1ba58aa3 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -21,6 +21,8 @@ const SectionContainer = styled.div` `; const SmallButton = styled(Button)` + padding-left: 1rem; + padding-right: 1rem; height: 1.5rem; `; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 90a4239971..09abcc6e5a 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -268,7 +268,7 @@ class DiffFile extends React.Component { this.toggleSideBySide(() => setMenuCollapsed(sideBySide ? true : false))} + onClick={() => this.toggleSideBySide(() => setMenuCollapsed(true))} /> )} diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 3d65d16cb2..58eb18391d 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -141,11 +141,7 @@ class RepositoryRoot extends React.Component { const extensionProps: any = { repository, url, - indexLinks - }; - - const navExtensionProps = { - ...extensionProps, + indexLinks, collapsedRepositoryMenu: menuCollapsed }; @@ -221,7 +217,7 @@ class RepositoryRoot extends React.Component { } collapsed={menuCollapsed} > - + { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.sourcesNavLink")} /> - + { > - + From 9705347c1fa97f97d023630e2cd70c94c3ae2c64 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 15:14:13 +0100 Subject: [PATCH 040/111] update storyshots after merge --- .../src/__snapshots__/storyshots.test.ts.snap | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 790c2cdc94..cb24897d56 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Diff Binaries 1`] = `