This commit is contained in:
Francesco 2023-09-19 18:02:40 +02:00
parent 2db84d9996
commit 234f3d94c8
12 changed files with 392 additions and 77 deletions

View File

@ -2,9 +2,11 @@ package tech.ailef.dbadmin.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
@ -12,6 +14,7 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -22,14 +25,18 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.persistence.criteria.Predicate;
import ch.qos.logback.core.joran.action.ParamAction;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tech.ailef.dbadmin.DbAdmin;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbObject;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.exceptions.InvalidPageException;
import tech.ailef.dbadmin.misc.Utils;
@Controller
@RequestMapping("/dbadmin")
@ -92,18 +99,54 @@ public class DefaultDbAdminController {
public String list(Model model, @PathVariable String className,
@RequestParam(required=false) Integer page, @RequestParam(required=false) String query,
@RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey,
@RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap<String, String> otherParams) {
System.out.println(otherParams);
@RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap<String, String> otherParams,
HttpServletRequest request,
HttpServletResponse response) {
if (page == null) page = 1;
if (pageSize == null) pageSize = 50;
Set<QueryFilter> queryFilters = Utils.computeFilters(otherParams);
if (otherParams.containsKey("remove_field")) {
List<String> fields = otherParams.get("remove_field");
for (int i = 0; i < fields.size(); i++) {
QueryFilter toRemove =
new QueryFilter(fields.get(i), otherParams.get("remove_op").get(i), otherParams.get("remove_value").get(i));
queryFilters.removeIf(f -> f.equals(toRemove));
}
MultiValueMap<String, String> parameterMap = Utils.computeParams(queryFilters);
MultiValueMap<String, String> filteredParams = new LinkedMultiValueMap<>();
request.getParameterMap().entrySet().stream()
.filter(e -> !e.getKey().startsWith("remove_") && !e.getKey().startsWith("filter_"))
.forEach(e -> {
filteredParams.putIfAbsent(e.getKey(), new ArrayList<>());
for (String v : e.getValue()) {
if (filteredParams.get(e.getKey()).isEmpty()) {
filteredParams.get(e.getKey()).add(v);
} else {
filteredParams.get(e.getKey()).set(0, v);
}
}
});
filteredParams.putAll(parameterMap);
String queryString = Utils.getQueryString(filteredParams);
String redirectUrl = request.getServletPath() + queryString;
return "redirect:" + redirectUrl.trim();
}
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
try {
PaginatedResult result = null;
if (query != null || !otherParams.isEmpty()) {
result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, otherParams);
result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, queryFilters);
} else {
result = repository.findAll(schema, page, pageSize, sortKey, sortOrder);
}
@ -115,6 +158,7 @@ public class DefaultDbAdminController {
model.addAttribute("sortKey", sortKey);
model.addAttribute("query", query);
model.addAttribute("sortOrder", sortOrder);
model.addAttribute("activeFilters", queryFilters);
return "model/list";
} catch (InvalidPageException e) {

View File

@ -2,10 +2,10 @@ package tech.ailef.dbadmin.dbmapping;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.util.MultiValueMap;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
@ -13,7 +13,7 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.dto.QueryFilter;
@SuppressWarnings("rawtypes")
public class AdvancedJpaRepository extends SimpleJpaRepository {
@ -30,12 +30,12 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
}
@SuppressWarnings("unchecked")
public long count(String q, MultiValueMap<String, String> filteringParams) {
public long count(String q, Set<QueryFilter> queryFilters) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(Long.class);
Root root = query.from(schema.getJavaClass());
List<Predicate> finalPredicates = buildPredicates(q, filteringParams, cb, root);
List<Predicate> finalPredicates = buildPredicates(q, queryFilters, cb, root);
query.select(cb.count(root.get(schema.getPrimaryKey().getName())))
.where(
@ -49,12 +49,12 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
}
@SuppressWarnings("unchecked")
public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, MultiValueMap<String, String> filteringParams) {
public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, Set<QueryFilter> filters) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(schema.getJavaClass());
Root root = query.from(schema.getJavaClass());
List<Predicate> finalPredicates = buildPredicates(q, filteringParams, cb, root);
List<Predicate> finalPredicates = buildPredicates(q, filters, cb, root);
query.select(root)
.where(
@ -70,7 +70,7 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
.setFirstResult((page - 1) * pageSize).getResultList();
}
private List<Predicate> buildPredicates(String q, MultiValueMap<String, String> filteringParams,
private List<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
CriteriaBuilder cb, Path root) {
List<Predicate> finalPredicates = new ArrayList<>();
@ -89,33 +89,29 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
finalPredicates.add(queryPredicate);
}
/*
* Compute filtering predicates
*/
if (filteringParams != null) {
List<String> ops = filteringParams.get("filter_op[]");
List<String> fields = filteringParams.get("filter_field[]");
List<String> values = filteringParams.get("filter_value[]");
if (ops != null && fields != null && values != null) {
if (ops.size() != fields.size() || fields.size() != values.size()
|| ops.size() != values.size()) {
throw new DbAdminException("Filtering parameters must have the same size");
}
for (int i = 0; i < ops.size(); i++) {
String op = ops.get(i);
String field = fields.get(i);
String value = values.get(i);
for (QueryFilter filter : queryFilters) {
String op = filter.getOp();
String field = filter.getField();
String value = filter.getValue();
if (op.equalsIgnoreCase("equals")) {
finalPredicates.add(cb.equal(cb.toString(root.get(field)), value));
finalPredicates.add(cb.equal(cb.lower(cb.toString(root.get(field))), value.toLowerCase()));
} else if (op.equalsIgnoreCase("contains")) {
System.out.println("CONTAINS");
finalPredicates.add(cb.like(cb.toString(root.get(field)), "%" + value + "%"));
}
}
finalPredicates.add(
cb.like(cb.lower(cb.toString(root.get(field))), "%" + value.toLowerCase() + "%")
);
} else if (op.equalsIgnoreCase("eq")) {
finalPredicates.add(
cb.equal(root.get(field), value)
);
} else if (op.equalsIgnoreCase("gt")) {
finalPredicates.add(
cb.greaterThan(root.get(field), value)
);
} else if (op.equalsIgnoreCase("lt")) {
finalPredicates.add(
cb.lessThan(root.get(field), value)
);
}
}
return finalPredicates;

View File

@ -3,10 +3,12 @@ package tech.ailef.dbadmin.dbmapping;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.data.domain.Page;
@ -16,12 +18,12 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import jakarta.transaction.Transactional;
import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.dto.PaginationInfo;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.exceptions.InvalidPageException;
@ -64,8 +66,8 @@ public class DbAdminRepository {
* @param query
* @return
*/
public long count(DbObjectSchema schema, String query, MultiValueMap<String, String> filteringParams) {
return schema.getJpaRepository().count(query, filteringParams);
public long count(DbObjectSchema schema, String query, Set<QueryFilter> queryFilters) {
return schema.getJpaRepository().count(query, queryFilters);
}
@ -117,7 +119,7 @@ public class DbAdminRepository {
return new PaginatedResult(
new PaginationInfo(page, maxPage, pageSize, maxElement),
new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()),
results
);
}
@ -225,10 +227,10 @@ public class DbAdminRepository {
* @return
*/
public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
String sortOrder, MultiValueMap<String, String> filteringParams) {
String sortOrder, Set<QueryFilter> queryFilters) {
AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
long maxElement = count(schema, query, filteringParams);
long maxElement = count(schema, query, queryFilters);
int maxPage = (int)(Math.ceil ((double)maxElement / pageSize));
if (page <= 0) page = 1;
@ -237,8 +239,8 @@ public class DbAdminRepository {
}
return new PaginatedResult(
new PaginationInfo(page, maxPage, pageSize, maxElement),
jpaRepository.search(query, page, pageSize, sortKey, sortOrder, filteringParams).stream()
new PaginationInfo(page, maxPage, pageSize, maxElement, query, queryFilters),
jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).stream()
.map(o -> new DbObject(o, schema))
.toList()
);

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
@ -29,6 +30,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return Integer.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
},
DOUBLE {
@Override
@ -45,6 +51,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return Double.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
},
LONG {
@Override
@ -61,6 +72,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return Long.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
},
FLOAT {
@Override
@ -77,6 +93,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return Float.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
},
LOCAL_DATE {
@Override
@ -93,6 +114,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return Float.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("After", "Equals", "Before");
}
},
LOCAL_DATE_TIME {
@Override
@ -109,6 +135,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return LocalDateTime.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("After", "Equals", "Before");
}
},
STRING {
@Override
@ -125,6 +156,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return String.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("Equals", "Contains");
}
},
BOOLEAN {
@Override
@ -141,6 +177,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return Boolean.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("Equals");
}
},
BIG_DECIMAL {
@Override
@ -157,6 +198,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
return BigDecimal.class;
}
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
},
BYTE_ARRAY {
@Override
@ -178,6 +224,10 @@ public enum DbFieldType {
return byte[].class;
}
@Override
public List<String> getCompareOperators() {
return List.of("Equals");
}
},
ONE_TO_MANY {
@Override
@ -204,6 +254,11 @@ public enum DbFieldType {
public String toString() {
return "One to Many";
}
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
},
ONE_TO_ONE {
@Override
@ -230,6 +285,11 @@ public enum DbFieldType {
public String toString() {
return "One to One";
}
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
},
MANY_TO_MANY {
@Override
@ -256,6 +316,11 @@ public enum DbFieldType {
public String toString() {
return "Many to Many";
}
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
},
COMPUTED {
@Override
@ -272,6 +337,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() {
throw new UnsupportedOperationException();
}
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
};
public abstract String getHTMLName();
@ -280,6 +350,8 @@ public enum DbFieldType {
public abstract Class<?> getJavaClass();
public abstract List<String> getCompareOperators();
public boolean isRelationship() {
return false;
}

View File

@ -1,9 +1,15 @@
package tech.ailef.dbadmin.dto;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.springframework.util.MultiValueMap;
import tech.ailef.dbadmin.misc.Utils;
/**
* Attached as output to requests that have a paginated response,
* holds information about the current pagination.
@ -31,11 +37,17 @@ public class PaginationInfo {
private long maxElement;
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement) {
private Set<QueryFilter> queryFilters;
private String query;
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, Set<QueryFilter> queryFilters) {
this.currentPage = currentPage;
this.maxPage = maxPage;
this.pageSize = pageSize;
this.query = query;
this.maxElement = maxElement;
this.queryFilters = queryFilters;
}
public int getCurrentPage() {
@ -66,6 +78,20 @@ public class PaginationInfo {
return maxElement;
}
public String getLink(int page) {
MultiValueMap<String, String> params = Utils.computeParams(queryFilters);
if (query != null) {
params.put("query", new ArrayList<>());
params.get("query").add(query);
}
params.add("pageSize", "" + pageSize);
params.add("page", "" + page);
return Utils.getQueryString(params);
}
public List<Integer> getBeforePages() {
return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList());
}

View File

@ -0,0 +1,53 @@
package tech.ailef.dbadmin.dto;
import java.util.Objects;
public class QueryFilter {
private String field;
private String op;
private String value;
public QueryFilter(String field, String op, String value) {
this.field = field;
this.op = op;
this.value = value;
}
public String getField() {
return field;
}
public String getOp() {
return op;
}
public String getValue() {
return value;
}
@Override
public int hashCode() {
return Objects.hash(field, op, value);
}
@Override
public String toString() {
return field + " " + op + " '" + value + "'";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
QueryFilter other = (QueryFilter) obj;
return Objects.equals(field, other.field) && Objects.equals(op, other.op) && Objects.equals(value, other.value);
}
}

View File

@ -1,5 +1,16 @@
package tech.ailef.dbadmin.misc;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
public interface Utils {
public static String camelToSnake(String v) {
if (Character.isUpperCase(v.charAt(0))) {
@ -10,6 +21,66 @@ public interface Utils {
}
public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
MultiValueMap<String, String> r = new LinkedMultiValueMap<>();
r.put("filter_field", new ArrayList<>());
r.put("filter_op", new ArrayList<>());
r.put("filter_value", new ArrayList<>());
for (QueryFilter filter : filters) {
r.get("filter_field").add(filter.getField());
r.get("filter_op").add(filter.getOp());
r.get("filter_value").add(filter.getValue());
}
return r;
}
public static Set<QueryFilter> computeFilters(MultiValueMap<String, String> params) {
if (params == null)
return new HashSet<>();
List<String> ops = params.get("filter_op");
List<String> fields = params.get("filter_field");
List<String> values = params.get("filter_value");
if (ops == null || fields == null || values == null)
return new HashSet<>();
if (ops.size() != fields.size() || fields.size() != values.size()
|| ops.size() != values.size()) {
throw new DbAdminException("Filtering parameters must have the same size");
}
Set<QueryFilter> filters = new HashSet<>();
for (int i = 0; i < ops.size(); i++) {
String op = ops.get(i);
String field = fields.get(i);
String value = values.get(i);
QueryFilter queryFilter = new QueryFilter(field, op, value);
filters.add(queryFilter);
}
return filters;
}
public static String getQueryString(MultiValueMap<String, String> params) {
Set<String> currentParams = params.keySet();
List<String> paramValues = new ArrayList<>();
for (String param : currentParams) {
for (String v : params.get(param)) {
paramValues.add(param + "=" + v.trim());
}
}
if (paramValues.isEmpty()) return "";
return "?" + String.join("&", paramValues);
}
public static String snakeToCamel(String text) {
boolean shouldConvertNextCharToLower = true;
StringBuilder builder = new StringBuilder();

View File

@ -6,3 +6,4 @@ spring.datasource.password=password
spring.jpa.show-sql=true
server.tomcat.relaxed-path-chars=[,]

View File

@ -1,19 +1,34 @@
document.addEventListener("DOMContentLoaded", () => {
let rootElements = document.querySelectorAll('.filterable-field');
let rootElements = document.querySelectorAll('.filterable-fields');
rootElements.forEach(root => {
root.querySelector(".card-header").addEventListener('click', function(e) {
if (root.querySelector(".card-body").classList.contains('d-none')) {
root.querySelector(".card-body").classList.remove('d-none');
root.querySelector(".card-body").classList.add('d-block');
root.querySelector(".card-header i.bi").classList.remove('bi-caret-right');
root.querySelector(".card-header i.bi").classList.add('bi-caret-down');
let fields = root.querySelectorAll('.filterable-field');
let activeFilters = root.querySelectorAll(".active-filter");
activeFilters.forEach(activeFilter => {
activeFilter.addEventListener('click', function(e) {
let formId = e.target.dataset.formid;
document.getElementById(formId).submit()
});
});
fields.forEach(field => {
field.querySelector(".card-header").addEventListener('click', function(e) {
if (field.querySelector(".card-body").classList.contains('d-none')) {
field.querySelector(".card-body").classList.remove('d-none');
field.querySelector(".card-body").classList.add('d-block');
field.querySelector(".card-header i.bi").classList.remove('bi-caret-right');
field.querySelector(".card-header i.bi").classList.add('bi-caret-down');
} else {
root.querySelector(".card-body").classList.remove('d-block');
root.querySelector(".card-body").classList.add('d-none');
root.querySelector(".card-header i.bi").classList.remove('bi-caret-down');
root.querySelector(".card-header i.bi").classList.add('bi-caret-right');
field.querySelector(".card-body").classList.remove('d-block');
field.querySelector(".card-body").classList.add('d-none');
field.querySelector(".card-header i.bi").classList.remove('bi-caret-down');
field.querySelector(".card-header i.bi").classList.add('bi-caret-right');
}
});
});
});
});

View File

@ -48,6 +48,10 @@
<div class="card-body d-none">
<form action="" method="GET">
<!-- Reset page when applying filter to start back at page 1 -->
<!-- <input type="hidden" name="page" value="1">
<input type="hidden" name="pageSize" value="50"> -->
<th:block th:each="p : ${queryParams.keySet()}">
<input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block>
@ -59,14 +63,13 @@
</th:block>
<th:block th:unless="${field.isForeignKey()}">
<div class="container w-25">
<select class="form-select w-auto" name="filter_op[]">
<option value="equals">Equals</option>
<option value="contains">Contains</option>
<select class="form-select w-auto" name="filter_op">
<option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}" th:text="${op}">
</select>
</div>
<input type="hidden" name="filter_field[]" th:value="${field.getName()}">
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
name="filter_value[]"
name="filter_value"
class="form-control" th:id="|__id_${field.getName()}|"
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
th:required="${!field.isNullable() && !field.isPrimaryKey()}"

View File

@ -125,14 +125,17 @@
<div th:if="${page != null && page.getPagination().getMaxPage() != 1}" class="d-flex">
<ul class="pagination me-3">
<li class="page-item" th:if="${page.getPagination().getCurrentPage() != 1}">
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${page.getPagination().getCurrentPage() - 1},pageSize=${page.getPagination().getPageSize()})}" aria-label="Previous">
<a class="page-link"
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() - 1)}|}"
aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
<li class="page-item" th:each="p : ${page.getPagination().getBeforePages()}">
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${p},pageSize=${page.getPagination().getPageSize()})}" th:text="${p}"></a>
<a class="page-link"
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}" th:text="${p}"></a>
</li>
<li class="page-item active">
@ -140,12 +143,15 @@
</li>
<li class="page-item" th:each="p : ${page.getPagination().getAfterPages()}">
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${p},pageSize=${page.getPagination().getPageSize()})}" th:text="${p}"></a>
<a class="page-link"
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}"
th:text="${p}"></a>
</li>
<li class="page-item">
<a class="page-link"
th:if="${!page.getPagination().isLastPage()}"
th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${page.getPagination().getCurrentPage() + 1},pageSize=${page.getPagination().getPageSize()})}" aria-label="Next">
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() + 1)}|}"
aria-label="Next">
<span class="sr-only">Next</span>
<span aria-hidden="true">&raquo;</span>
</a>

View File

@ -39,6 +39,9 @@
class="ui-text-input form-control" name="query" autofocus>
<button class="ui-btn btn btn-primary">Search</button>
</div>
<th:block th:each="p : ${queryParams.keySet()}">
<input th:if="${!p.equals('query')}" th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block>
</form>
<div class="separator mb-4 mt-4"></div>
@ -50,11 +53,10 @@
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
[[ ${schema.getTableName()} ]]
</span>
</h3>
<h3><a title="Create new item"
<h3>
<a title="Create new item"
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a>
</h3>
</div>
@ -64,10 +66,34 @@
</div>
</div>
<div th:if="${!schema.getFilterableFields().isEmpty()}" class="col-3">
<div class="box">
<div class="box filterable-fields">
<h3 class="fw-bold mb-3"><i class="bi bi-funnel"></i> Filters</h3>
<div class="mb-2">
<div th:each="filter : ${activeFilters}">
<span title="Click to remove this filter"
class="active-filter badge bg-primary me-1 mb-2 p-2 font-monospace cursor-pointer noselect"
th:data-formid="${filter.toString()}"
th:text="${filter}">
</span>
<form action="" th:id="${filter.toString()}" method="GET">
<th:block th:each="p : ${queryParams.keySet()}">
<input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block>
<input type="hidden" name="remove_field" th:value="${filter.getField()}">
<input type="hidden" name="remove_op" th:value="${filter.getOp()}">
<input type="hidden" name="remove_value" th:value="${filter.getValue()}">
</form>
</div>
</div>
<th:block th:each="field : ${schema.getFilterableFields()}">
<div th:replace="~{fragments/forms :: filter_field(field=${field})}"></div>