diff options
Diffstat (limited to 'src/main')
4 files changed, 200 insertions, 39 deletions
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java index 1c6653a..dc45f34 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java @@ -29,9 +29,11 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.CheckForNull; @@ -41,9 +43,11 @@ import javax.json.JsonObject; import javax.json.spi.JsonProvider; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.From; import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Predicate; import javax.transaction.Transactional; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @@ -76,6 +80,9 @@ public class BookmarkResource { private static JsonProvider jsonProvider = JsonProvider.provider(); + @ConfigProperty(name = "mulkcms.bookmarks.default-max-results") + int defaultMaxResults; + @ResourcePath("benki/bookmarks/bookmarkList.html") @Inject Template bookmarkList; @@ -96,24 +103,47 @@ public class BookmarkResource { @GET @Produces(TEXT_HTML) - public TemplateInstance getIndex() { - var bookmarkQuery = selectBookmarks(null); + public TemplateInstance getIndex( + @QueryParam("i") @CheckForNull Integer cursor, + @QueryParam("n") @CheckForNull Integer maxResults) { + + maxResults = maxResults == null ? defaultMaxResults : maxResults; + + var q = selectBookmarks(null, cursor, maxResults); + return bookmarkList - .data("bookmarks", bookmarkQuery) + .data("bookmarks", q.bookmarks) .data("feedUri", "/bookmarks/feed") - .data("authenticated", !identity.isAnonymous()); + .data("authenticated", !identity.isAnonymous()) + .data("hasPreviousPage", q.prevCursor != null) + .data("hasNextPage", q.nextCursor != null) + .data("previousCursor", q.prevCursor) + .data("nextCursor", q.nextCursor) + .data("pageSize", maxResults); } @GET @Path("~{ownerName}") @Produces(TEXT_HTML) - public TemplateInstance getUserIndex(@PathParam("ownerName") String ownerName) { + public TemplateInstance getUserIndex( + @PathParam("ownerName") String ownerName, + @QueryParam("i") @CheckForNull Integer cursor, + @QueryParam("n") @CheckForNull Integer maxResults) { + + maxResults = maxResults == null ? defaultMaxResults : maxResults; + var owner = User.findByNickname(ownerName); - var bookmarkQuery = selectBookmarks(owner); + var q = selectBookmarks(owner, cursor, maxResults); + return bookmarkList - .data("bookmarks", bookmarkQuery) + .data("bookmarks", q.bookmarks) .data("feedUri", String.format("/bookmarks/~%s/feed", ownerName)) - .data("authenticated", !identity.isAnonymous()); + .data("authenticated", !identity.isAnonymous()) + .data("hasPreviousPage", q.prevCursor != null) + .data("hasNextPage", q.nextCursor != null) + .data("previousCursor", q.prevCursor) + .data("nextCursor", q.nextCursor) + .data("pageSize", maxResults); } @GET @@ -267,35 +297,114 @@ public class BookmarkResource { return htmlDateFormatter.format(x); } - private List<Bookmark> selectBookmarks(@Nullable User owner) { + private static class BookmarkPage { + @CheckForNull Integer prevCursor; + @CheckForNull Integer cursor; + @CheckForNull Integer nextCursor; + List<Bookmark> bookmarks; + + public BookmarkPage( + @CheckForNull Integer c0, + @CheckForNull Integer c1, + @CheckForNull Integer c2, + List<Bookmark> resultList) { + this.prevCursor = c0; + this.cursor = c1; + this.nextCursor = c2; + this.bookmarks = resultList; + } + } + + private List<Bookmark> selectBookmarks(@CheckForNull User owner) { + return selectBookmarks(owner, null, null).bookmarks; + } + + private BookmarkPage selectBookmarks( + @CheckForNull User owner, @CheckForNull Integer cursor, @CheckForNull Integer count) { + + if (cursor != null) { + Objects.requireNonNull(count); + } + var cb = entityManager.unwrap(Session.class).getCriteriaBuilder(); + var forwardCriteria = generateBookmarkCriteriaQuery(owner, cursor, cb, true); + var forwardQuery = entityManager.createQuery(forwardCriteria); + + if (count != null) { + forwardQuery.setMaxResults(count + 1); + } + + log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString()); + + @CheckForNull Integer prevCursor = null; + @CheckForNull Integer nextCursor = null; + + if (cursor != null) { + // Look backwards as well so we can find the prevCursor. + var backwardCriteria = generateBookmarkCriteriaQuery(owner, cursor, cb, false); + var backwardQuery = entityManager.createQuery(backwardCriteria); + backwardQuery.setMaxResults(count); + var backwardResults = backwardQuery.getResultList(); + if (!backwardResults.isEmpty()) { + prevCursor = backwardResults.get(backwardResults.size() - 1).id; + } + } + + var forwardResults = forwardQuery.getResultList(); + if (count != null) { + if (forwardResults.size() == count + 1) { + nextCursor = forwardResults.get(count).id; + forwardResults.remove((int) count); + } + } + + return new BookmarkPage(prevCursor, cursor, nextCursor, forwardResults); + } + + private CriteriaQuery<Bookmark> generateBookmarkCriteriaQuery( + @CheckForNull User owner, @CheckForNull Integer cursor, CriteriaBuilder cb, boolean forward) { CriteriaQuery<Bookmark> query = cb.createQuery(Bookmark.class); + var conditions = new ArrayList<Predicate>(); + From<?, Bookmark> bm; if (identity.isAnonymous()) { bm = query.from(Bookmark.class); var target = bm.join(Bookmark_.targets); - query.where(cb.equal(target, Role.getWorld())); + conditions.add(cb.equal(target, Role.getWorld())); } else { var userName = identity.getPrincipal().getName(); var user = User.findByNickname(userName); var root = query.from(User.class); - query.where(cb.equal(root, user)); + conditions.add(cb.equal(root, user)); bm = root.join(User_.visibleBookmarks); } query.select(bm); bm.fetch(Bookmark_.owner, JoinType.LEFT); - query.orderBy(cb.desc(bm.get(Bookmark_.date))); if (owner != null) { - query.where(cb.equal(bm.get(Bookmark_.owner), owner)); + conditions.add(cb.equal(bm.get(Bookmark_.owner), owner)); + } + + if (forward) { + query.orderBy(cb.desc(bm.get(Bookmark_.id))); + } else { + query.orderBy(cb.asc(bm.get(Bookmark_.id))); + } + + if (cursor != null) { + if (forward) { + conditions.add(cb.le(bm.get(Bookmark_.id), cursor)); + } else { + conditions.add(cb.gt(bm.get(Bookmark_.id), cursor)); + } } - var q = entityManager.createQuery(query); - log.debug(q.unwrap(org.hibernate.query.Query.class).getQueryString()); - return q.getResultList(); + query.where(conditions.toArray(new Predicate[0])); + + return query; } } diff --git a/src/main/resources/META-INF/resources/cms2/base.css b/src/main/resources/META-INF/resources/cms2/base.css index ec84ca9..61f447c 100644 --- a/src/main/resources/META-INF/resources/cms2/base.css +++ b/src/main/resources/META-INF/resources/cms2/base.css @@ -124,7 +124,18 @@ body > main { background-color: var(--main-bg-color); padding: 10px; border-left: 1px solid lightgray; - overflow: scroll; + overflow: auto; + + display: flex; + flex-direction: column; +} + +main > * { + margin-top: 0.5rem; +} + +main > *:first-child { + margin-top: 0; } body > footer { @@ -175,3 +186,24 @@ article.lazychat-message { #bookmark-submission textarea { min-width: calc(100% - 12em); } + +.paging { + display: flex; + flex-direction: row; + flex-wrap: wrap-reverse; +} + +.paging > .filler { + flex: 1; +} + +.paging > a { + flex-grow: 0; + flex-shrink: 1; + flex-basis: content; +} + +elix-expandable-section .expandable-section-title { + margin-top: 0; + margin-bottom: 0; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 50423c0..b90cc9e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,6 +8,7 @@ quarkus.log.level = INFO #quarkus.log.category."io.vertx.ext.jwt".level = FINEST mulkcms.tag-base = hub.benkard.de +mulkcms.bookmarks.default-max-results = 25 quarkus.datasource.driver = org.postgresql.Driver quarkus.datasource.max-size = 8 diff --git a/src/main/resources/templates/benki/bookmarks/bookmarkList.html b/src/main/resources/templates/benki/bookmarks/bookmarkList.html index 9b5025c..290cb26 100644 --- a/src/main/resources/templates/benki/bookmarks/bookmarkList.html +++ b/src/main/resources/templates/benki/bookmarks/bookmarkList.html @@ -1,5 +1,10 @@ {@java.util.List<eu.mulk.mulkcms2.benki.bookmarks.Bookmark> bookmarks} {@java.lang.Boolean authenticated} +{@java.lang.Boolean hasPreviousPage} +{@java.lang.Boolean hasNextPage} +{@java.lang.Integer previousCursor} +{@java.lang.Integer nextCursor} +{@java.lang.Integer pageSize} {#include base.html} @@ -9,40 +14,54 @@ {#head} <link href="{feedUri}" rel="alternate" type="application/atom+xml" /> -{/head} - -{#body} -{! #if authenticated !} <script type="module" src="/web_modules/elix/define/ExpandableSection.js"></script> <script type="module" src="/bookmarks/MlkBookmarkSubmissionForm.js"></script> <script type="module" src="/bookmarks/bookmarkList.js" defer></script> +{/head} +{#body} + +{! #if authenticated !} <elix-expandable-section id="bookmark-submission-pane"> - <h2 slot="header" class="small-title"><button class="pure-button">Create New Bookmark</button></h2> + <h2 slot="header" class="small-title expandable-section-title"><button class="pure-button">Create New Bookmark</button></h2> <section id="bookmark-submission"> <mlk-bookmark-submission-form id="bookmark-submission-form"></mlk-bookmark-submission-form> </section> </elix-expandable-section> {! /if !} -{#for bookmark in bookmarks} - {#with bookmark} - <article class="bookmark"> - <header> - <a href="{uri}"><h1 class="bookmark-title">{title}</h1></a> - <div class="bookmark-info"> - <time datetime="{date.htmlDateTime}">{date.humanDateTime}</time> - <span class="bookmark-owner">{owner.firstName} {owner.lastName}</span> - </div> - </header> - - <section class="bookmark-description"> - {descriptionHtml.raw} - </section> - </article> - {/with} -{/for} +<div class="paging"> + {#if hasPreviousPage}<a href="?i={previousCursor}&n={pageSize}" class="pure-button">⇠ previous page</a>{/if} + <span class="filler"></span> + {#if hasNextPage}<a href="?i={nextCursor}&n={pageSize}" class="pure-button">next page ⇢</a>{/if} +</div> + +<section id="main-content"> + {#for bookmark in bookmarks} + {#with bookmark} + <article class="bookmark"> + <header> + <a href="{uri}"><h1 class="bookmark-title">{title}</h1></a> + <div class="bookmark-info"> + <time datetime="{date.htmlDateTime}">{date.humanDateTime}</time> + <span class="bookmark-owner">{owner.firstName} {owner.lastName}</span> + </div> + </header> + + <section class="bookmark-description"> + {descriptionHtml.raw} + </section> + </article> + {/with} + {/for} +</section> + +<div class="paging"> + {#if hasPreviousPage}<a href="?i={previousCursor}&n={pageSize}" class="pure-button">⇠ previous page</a>{/if} + <span class="filler"></span> + {#if hasNextPage}<a href="?i={nextCursor}&n={pageSize}" class="pure-button">next page ⇢</a>{/if} +</div> {/body} |