Paing Update

This commit is contained in:
lowCost 2025-10-31 00:33:42 +09:00
parent 6feeae0f13
commit 3eef14eef4
12 changed files with 280 additions and 73 deletions

View file

@ -15,9 +15,11 @@
*/
package org.springframework.samples.petclinic.owner;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@ -51,6 +53,12 @@ class OwnerController {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";
private static final String DEFAULT_PAGE_SIZE_VALUE = "10";
private static final int DEFAULT_PAGE_SIZE = Integer.parseInt(DEFAULT_PAGE_SIZE_VALUE);
private static final List<Integer> PAGE_SIZE_OPTIONS = List.of(DEFAULT_PAGE_SIZE, 20, 30, 40, 50);
private final OwnerRepository owners;
public OwnerController(OwnerRepository owners) {
@ -94,15 +102,20 @@ class OwnerController {
@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
Model model) {
Model model, @RequestParam(name = "size", defaultValue = DEFAULT_PAGE_SIZE_VALUE) int size) {
// allow parameterless GET request for /owners to return all records
String lastName = owner.getLastName();
if (lastName == null) {
lastName = ""; // empty string signifies broadest possible search
}
owner.setLastName(lastName);
int resolvedPage = Math.max(page, 1);
int pageSize = resolvePageSize(size);
// find owners by last name
Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, lastName);
Page<Owner> ownersResults = findPaginatedForOwnersLastName(resolvedPage, pageSize, lastName);
if (ownersResults.isEmpty()) {
// no owners found
result.rejectValue("lastName", "notFound", "not found");
@ -116,24 +129,56 @@ class OwnerController {
}
// multiple owners found
return addPaginationModel(page, model, ownersResults);
return addPaginationModel(resolvedPage, pageSize, owner, model, ownersResults);
}
private String addPaginationModel(int page, Model model, Page<Owner> paginated) {
private String addPaginationModel(int page, int pageSize, Owner owner, Model model, Page<Owner> paginated) {
List<Owner> listOwners = paginated.getContent();
long totalItems = paginated.getTotalElements();
int currentItemCount = paginated.getNumberOfElements();
long startItem = totalItems == 0 ? 0L : (long) ((page - 1L) * pageSize) + 1L;
long endItem = currentItemCount == 0 ? startItem : Math.min(startItem + currentItemCount - 1L, totalItems);
int totalPages = paginated.getTotalPages();
int windowSize = 10;
int windowStart = 1;
int windowEnd = 0;
List<Integer> pageNumbers = Collections.emptyList();
boolean showLeadingGap = false;
boolean showTrailingGap = false;
if (totalPages > 0) {
int maxWindowStart = Math.max(1, totalPages - windowSize + 1);
windowStart = Math.max(1, Math.min(page, maxWindowStart));
windowEnd = Math.min(windowStart + windowSize - 1, totalPages);
pageNumbers = IntStream.rangeClosed(windowStart, windowEnd).boxed().toList();
int leadingHiddenCount = windowStart - 1;
int trailingHiddenCount = totalPages - windowEnd;
showLeadingGap = leadingHiddenCount > 0;
showTrailingGap = trailingHiddenCount > 0;
}
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", paginated.getTotalPages());
model.addAttribute("totalItems", paginated.getTotalElements());
model.addAttribute("pageSize", pageSize);
model.addAttribute("pageSizeOptions", PAGE_SIZE_OPTIONS);
model.addAttribute("totalPages", totalPages);
model.addAttribute("totalItems", totalItems);
model.addAttribute("startItem", startItem);
model.addAttribute("endItem", endItem);
model.addAttribute("listOwners", listOwners);
model.addAttribute("owner", owner);
model.addAttribute("pageNumbers", pageNumbers);
model.addAttribute("showLeadingGap", showLeadingGap);
model.addAttribute("showTrailingGap", showTrailingGap);
return "owners/ownersList";
}
private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
int pageSize = 5;
private Page<Owner> findPaginatedForOwnersLastName(int page, int pageSize, String lastname) {
Pageable pageable = PageRequest.of(page - 1, pageSize);
return owners.findByLastNameStartingWith(lastname, pageable);
}
private int resolvePageSize(int requestedSize) {
return PAGE_SIZE_OPTIONS.contains(requestedSize) ? requestedSize : DEFAULT_PAGE_SIZE;
}
@GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm() {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;

View file

@ -15,7 +15,9 @@
*/
package org.springframework.samples.petclinic.vet;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@ -35,6 +37,14 @@ import org.springframework.web.bind.annotation.ResponseBody;
@Controller
class VetController {
private static final String DEFAULT_PAGE_SIZE_VALUE = "10";
private static final int DEFAULT_PAGE_SIZE = Integer.parseInt(DEFAULT_PAGE_SIZE_VALUE);
private static final List<Integer> PAGE_SIZE_OPTIONS = List.of(DEFAULT_PAGE_SIZE, 20, 30, 40, 50);
private static final int PAGINATION_WINDOW_SIZE = 10;
private final VetRepository vetRepository;
public VetController(VetRepository vetRepository) {
@ -42,30 +52,63 @@ class VetController {
}
@GetMapping("/vets.html")
public String showVetList(@RequestParam(defaultValue = "1") int page, Model model) {
public String showVetList(@RequestParam(defaultValue = "1") int page, Model model,
@RequestParam(name = "size", defaultValue = DEFAULT_PAGE_SIZE_VALUE) int size) {
// Here we are returning an object of type 'Vets' rather than a collection of Vet
// objects so it is simpler for Object-Xml mapping
Vets vets = new Vets();
Page<Vet> paginated = findPaginated(page);
int resolvedPage = Math.max(page, 1);
int pageSize = resolvePageSize(size);
Page<Vet> paginated = findPaginated(resolvedPage, pageSize);
vets.getVetList().addAll(paginated.toList());
return addPaginationModel(page, paginated, model);
return addPaginationModel(resolvedPage, pageSize, paginated, model);
}
private String addPaginationModel(int page, Page<Vet> paginated, Model model) {
private String addPaginationModel(int page, int pageSize, Page<Vet> paginated, Model model) {
List<Vet> listVets = paginated.getContent();
long totalItems = paginated.getTotalElements();
int currentItemCount = paginated.getNumberOfElements();
long startItem = totalItems == 0 ? 0L : (long) ((page - 1L) * pageSize) + 1L;
long endItem = currentItemCount == 0 ? startItem : Math.min(startItem + currentItemCount - 1L, totalItems);
int totalPages = paginated.getTotalPages();
int windowStart = 1;
int windowEnd = 0;
List<Integer> pageNumbers = Collections.emptyList();
boolean showLeadingGap = false;
boolean showTrailingGap = false;
if (totalPages > 0) {
int maxWindowStart = Math.max(1, totalPages - PAGINATION_WINDOW_SIZE + 1);
windowStart = Math.max(1, Math.min(page, maxWindowStart));
windowEnd = Math.min(windowStart + PAGINATION_WINDOW_SIZE - 1, totalPages);
pageNumbers = IntStream.rangeClosed(windowStart, windowEnd).boxed().toList();
int leadingHiddenCount = windowStart - 1;
int trailingHiddenCount = totalPages - windowEnd;
showLeadingGap = leadingHiddenCount > 0;
showTrailingGap = trailingHiddenCount > 0;
}
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", paginated.getTotalPages());
model.addAttribute("totalItems", paginated.getTotalElements());
model.addAttribute("pageSize", pageSize);
model.addAttribute("pageSizeOptions", PAGE_SIZE_OPTIONS);
model.addAttribute("totalPages", totalPages);
model.addAttribute("totalItems", totalItems);
model.addAttribute("startItem", startItem);
model.addAttribute("endItem", endItem);
model.addAttribute("listVets", listVets);
model.addAttribute("pageNumbers", pageNumbers);
model.addAttribute("showLeadingGap", showLeadingGap);
model.addAttribute("showTrailingGap", showTrailingGap);
return "vets/vetList";
}
private Page<Vet> findPaginated(int page) {
int pageSize = 5;
private Page<Vet> findPaginated(int page, int pageSize) {
Pageable pageable = PageRequest.of(page - 1, pageSize);
return vetRepository.findAll(pageable);
}
private int resolvePageSize(int requestedSize) {
return PAGE_SIZE_OPTIONS.contains(requestedSize) ? requestedSize : DEFAULT_PAGE_SIZE;
}
@GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() {
// Here we are returning an object of type 'Vets' rather than a collection of Vet

View file

@ -13,6 +13,10 @@ address=Address
city=City
telephone=Telephone
owners=Owners
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...
addOwner=Add Owner
findOwner=Find Owner
findOwners=Find Owners

View file

@ -49,3 +49,9 @@ petsAndVisits=Haustiere und Besuche
error.404=Die angeforderte Seite wurde nicht gefunden.
error.500=Ein interner Serverfehler ist aufgetreten.
error.general=Ein unerwarteter Fehler ist aufgetreten.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -49,3 +49,9 @@ petsAndVisits=Mascotas y visitas
error.404=La página solicitada no fue encontrada.
error.500=Ocurrió un error interno del servidor.
error.general=Ocurrió un error inesperado.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -49,3 +49,9 @@ petsAndVisits=حیوانات و ویزیت‌ها
error.404=صفحه درخواستی پیدا نشد.
error.500=خطای داخلی سرور رخ داد.
error.general=خطای غیرمنتظره‌ای رخ داد.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -49,3 +49,9 @@ petsAndVisits=반려동물 및 방문
error.404=요청하신 페이지를 찾을 수 없습니다.
error.500=서버 내부 오류가 발생했습니다.
error.general=알 수 없는 오류가 발생했습니다.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -49,3 +49,9 @@ petsAndVisits=Animais e visitas
error.404=A página solicitada não foi encontrada.
error.500=Ocorreu um erro interno no servidor.
error.general=Ocorreu um erro inesperado.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -49,3 +49,9 @@ petsAndVisits=Питомцы и визиты
error.404=Запрашиваемая страница не найдена.
error.500=Произошла внутренняя ошибка сервера.
error.general=Произошла непредвиденная ошибка.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -49,3 +49,9 @@ petsAndVisits=Evcil Hayvanlar ve Ziyaretler
error.404=İstenen sayfa bulunamadı.
error.500=Sunucuda dahili bir hata oluştu.
error.general=Beklenmeyen bir hata oluştu.
owners.pageSize.label=Page Row
owners.pageSize.submit=Apply
owners.pagination.summary=Showing {0}-{1} of {2}
owners.pagination.ellipsis=...

View file

@ -6,6 +6,23 @@
<h2 th:text="#{owners}">Owners</h2>
<div class="owner-list-toolbar d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-3">
<div class="text-muted small" th:if="${totalItems > 0}"
th:text="#{owners.pagination.summary(${startItem}, ${endItem}, ${totalItems})}">Showing 1-10 of 42</div>
<form class="d-flex align-items-center gap-2" th:action="@{/owners}" method="get">
<input type="hidden" name="page" value="1" />
<input type="hidden" name="lastName" th:value="${owner.lastName}" />
<label class="form-label mb-0 small" for="pageSize" th:text="#{owners.pageSize.label}">Page Row</label>
<select id="pageSize" name="size" class="form-select form-select-sm" onchange="this.form.submit()">
<option th:each="option : ${pageSizeOptions}" th:value="${option}" th:selected="${option == pageSize}"
th:text="${option}">10</option>
</select>
<noscript>
<button type="submit" class="btn btn-primary btn-sm" th:text="#{owners.pageSize.submit}">Apply</button>
</noscript>
</form>
</div>
<table id="owners" class="table table-striped">
<thead>
<tr>
@ -28,34 +45,56 @@
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<span th:text="#{pages}">Pages:</span>
<span>[</span>
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/owners?page=' + ${i}}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" th:title="#{first}" class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=__${currentPage - 1}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${currentPage + 1}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${totalPages}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" th:title="#{last}" class="fa fa-fast-forward"></span>
</span>
<div class="owner-list-pagination d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between gap-3 mt-4"
th:if="${totalPages > 1}">
<div class="text-muted small" th:text="#{owners.pagination.summary(${startItem}, ${endItem}, ${totalItems})}">
Showing 1-10 of 42
</div>
<nav class="flex-grow-1" aria-label="Owners pagination">
<ul class="pagination justify-content-lg-end justify-content-center mb-0">
<li class="page-item" th:classappend="${currentPage == 1} ? ' disabled'">
<a class="page-link" th:href="@{/owners(page=1, size=${pageSize}, lastName=${owner.lastName})}"
th:title="#{first}" aria-label="First">
<span class="fa fa-fast-backward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{first}">First</span>
</a>
</li>
<li class="page-item" th:classappend="${currentPage == 1} ? ' disabled'">
<a class="page-link"
th:href="@{/owners(page=${currentPage - 1}, size=${pageSize}, lastName=${owner.lastName})}"
th:title="#{previous}" aria-label="Previous">
<span class="fa fa-step-backward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{previous}">Previous</span>
</a>
</li>
<li class="page-item disabled" th:if="${showLeadingGap}">
<span class="page-link" aria-hidden="true" th:text="#{owners.pagination.ellipsis}"></span>
</li>
<li class="page-item" th:each="i : ${pageNumbers}" th:classappend="${currentPage == i} ? ' active'">
<a class="page-link" th:href="@{/owners(page=${i}, size=${pageSize}, lastName=${owner.lastName})}"
th:text="${i}" th:aria-current="${currentPage == i} ? 'page' : null"></a>
</li>
<li class="page-item disabled" th:if="${showTrailingGap}">
<span class="page-link" aria-hidden="true" th:text="#{owners.pagination.ellipsis}"></span>
</li>
<li class="page-item" th:classappend="${currentPage == totalPages} ? ' disabled'">
<a class="page-link"
th:href="@{/owners(page=${currentPage + 1}, size=${pageSize}, lastName=${owner.lastName})}" th:title="#{next}"
aria-label="Next">
<span class="fa fa-step-forward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{next}">Next</span>
</a>
</li>
<li class="page-item" th:classappend="${currentPage == totalPages} ? ' disabled'">
<a class="page-link" th:href="@{/owners(page=${totalPages}, size=${pageSize}, lastName=${owner.lastName})}"
th:title="#{last}" aria-label="Last">
<span class="fa fa-fast-forward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{last}">Last</span>
</a>
</li>
</ul>
</nav>
</div>
</body>
</html>
</html>

View file

@ -6,6 +6,22 @@
<h2 th:text="#{vets}">Veterinarians</h2>
<div class="vet-list-toolbar d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-3">
<div class="text-muted small" th:if="${totalItems > 0}"
th:text="#{owners.pagination.summary(${startItem}, ${endItem}, ${totalItems})}">Showing 1-10 of 42</div>
<form class="d-flex align-items-center gap-2" th:action="@{/vets.html}" method="get">
<input type="hidden" name="page" value="1" />
<label class="form-label mb-0 small" for="vetsPageSize" th:text="#{owners.pageSize.label}">Page Row</label>
<select id="vetsPageSize" name="size" class="form-select form-select-sm" onchange="this.form.submit()">
<option th:each="option : ${pageSizeOptions}" th:value="${option}" th:selected="${option == pageSize}"
th:text="${option}">10</option>
</select>
<noscript>
<button type="submit" class="btn btn-primary btn-sm" th:text="#{owners.pageSize.submit}">Apply</button>
</noscript>
</form>
</div>
<table id="vets" class="table table-striped">
<thead>
<tr>
@ -23,35 +39,53 @@
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<span th:text="#{pages}">Pages:</span>
<span>[</span>
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/vets.html?page=__${i}__'}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=1'}" th:title="#{first}"
class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=__${currentPage - 1}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${currentPage + 1}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${totalPages}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" th:title="#{last}" class="fa fa-fast-forward"></span>
</span>
<div class="vet-list-pagination d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between gap-3 mt-4"
th:if="${totalPages > 1}">
<div class="text-muted small" th:text="#{owners.pagination.summary(${startItem}, ${endItem}, ${totalItems})}">
Showing 1-10 of 42
</div>
<nav class="flex-grow-1" aria-label="Veterinarians pagination">
<ul class="pagination justify-content-lg-end justify-content-center mb-0">
<li class="page-item" th:classappend="${currentPage == 1} ? ' disabled'">
<a class="page-link" th:href="@{/vets.html(page=1, size=${pageSize})}" th:title="#{first}" aria-label="First">
<span class="fa fa-fast-backward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{first}">First</span>
</a>
</li>
<li class="page-item" th:classappend="${currentPage == 1} ? ' disabled'">
<a class="page-link" th:href="@{/vets.html(page=${currentPage - 1}, size=${pageSize})}"
th:title="#{previous}" aria-label="Previous">
<span class="fa fa-step-backward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{previous}">Previous</span>
</a>
</li>
<li class="page-item disabled" th:if="${showLeadingGap}">
<span class="page-link" aria-hidden="true" th:text="#{owners.pagination.ellipsis}"></span>
</li>
<li class="page-item" th:each="i : ${pageNumbers}" th:classappend="${currentPage == i} ? ' active'">
<a class="page-link" th:href="@{/vets.html(page=${i}, size=${pageSize})}" th:text="${i}"
th:aria-current="${currentPage == i} ? 'page' : null"></a>
</li>
<li class="page-item disabled" th:if="${showTrailingGap}">
<span class="page-link" aria-hidden="true" th:text="#{owners.pagination.ellipsis}"></span>
</li>
<li class="page-item" th:classappend="${currentPage == totalPages} ? ' disabled'">
<a class="page-link" th:href="@{/vets.html(page=${currentPage + 1}, size=${pageSize})}" th:title="#{next}"
aria-label="Next">
<span class="fa fa-step-forward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{next}">Next</span>
</a>
</li>
<li class="page-item" th:classappend="${currentPage == totalPages} ? ' disabled'">
<a class="page-link" th:href="@{/vets.html(page=${totalPages}, size=${pageSize})}" th:title="#{last}"
aria-label="Last">
<span class="fa fa-fast-forward" aria-hidden="true"></span>
<span class="visually-hidden" th:text="#{last}">Last</span>
</a>
</li>
</ul>
</nav>
</div>
</body>
</html>
</html>