diff --git a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java index 54607ae..7bead18 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java @@ -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 otherParams) { - System.out.println(otherParams); + @RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap otherParams, + HttpServletRequest request, + HttpServletResponse response) { if (page == null) page = 1; if (pageSize == null) pageSize = 50; + Set queryFilters = Utils.computeFilters(otherParams); + if (otherParams.containsKey("remove_field")) { + List 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 parameterMap = Utils.computeParams(queryFilters); + + MultiValueMap 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) { diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java index 1da34db..185e8bd 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java @@ -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 filteringParams) { + public long count(String q, Set queryFilters) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Long.class); Root root = query.from(schema.getJavaClass()); - List finalPredicates = buildPredicates(q, filteringParams, cb, root); + List 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 search(String q, int page, int pageSize, String sortKey, String sortOrder, MultiValueMap filteringParams) { + public List search(String q, int page, int pageSize, String sortKey, String sortOrder, Set filters) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(schema.getJavaClass()); Root root = query.from(schema.getJavaClass()); - List finalPredicates = buildPredicates(q, filteringParams, cb, root); + List 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 buildPredicates(String q, MultiValueMap filteringParams, + private List buildPredicates(String q, Set queryFilters, CriteriaBuilder cb, Path root) { List finalPredicates = new ArrayList<>(); @@ -89,33 +89,29 @@ public class AdvancedJpaRepository extends SimpleJpaRepository { finalPredicates.add(queryPredicate); } - /* - * Compute filtering predicates - */ - if (filteringParams != null) { - List ops = filteringParams.get("filter_op[]"); - List fields = filteringParams.get("filter_field[]"); - List 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); - - if (op.equalsIgnoreCase("equals")) { - finalPredicates.add(cb.equal(cb.toString(root.get(field)), value)); - } else if (op.equalsIgnoreCase("contains")) { - System.out.println("CONTAINS"); - finalPredicates.add(cb.like(cb.toString(root.get(field)), "%" + value + "%")); - } - } + 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.lower(cb.toString(root.get(field))), value.toLowerCase())); + } else if (op.equalsIgnoreCase("contains")) { + 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; diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java index 11196c5..aa01777 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java @@ -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 filteringParams) { - return schema.getJpaRepository().count(query, filteringParams); + public long count(DbObjectSchema schema, String query, Set 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 filteringParams) { + String sortOrder, Set 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() ); diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java index 871862e..ececf23 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java @@ -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 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 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 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 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 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 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 getCompareOperators() { + return List.of("Equals", "Contains"); + } }, BOOLEAN { @Override @@ -141,6 +177,11 @@ public enum DbFieldType { public Class getJavaClass() { return Boolean.class; } + + @Override + public List getCompareOperators() { + return List.of("Equals"); + } }, BIG_DECIMAL { @Override @@ -157,6 +198,11 @@ public enum DbFieldType { public Class getJavaClass() { return BigDecimal.class; } + + @Override + public List getCompareOperators() { + return List.of("gt", "eq", "lt"); + } }, BYTE_ARRAY { @Override @@ -178,6 +224,10 @@ public enum DbFieldType { return byte[].class; } + @Override + public List 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 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 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 getCompareOperators() { + throw new DbAdminException(); + } }, COMPUTED { @Override @@ -272,6 +337,11 @@ public enum DbFieldType { public Class getJavaClass() { throw new UnsupportedOperationException(); } + + @Override + public List getCompareOperators() { + throw new DbAdminException(); + } }; public abstract String getHTMLName(); @@ -280,6 +350,8 @@ public enum DbFieldType { public abstract Class getJavaClass(); + public abstract List getCompareOperators(); + public boolean isRelationship() { return false; } diff --git a/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java b/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java index e41d8d0..58db91f 100644 --- a/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java +++ b/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java @@ -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. @@ -30,12 +36,18 @@ public class PaginationInfo { private int pageSize; private long maxElement; + + private Set queryFilters; + + private String query; - public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement) { + public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, Set queryFilters) { this.currentPage = currentPage; this.maxPage = maxPage; this.pageSize = pageSize; + this.query = query; this.maxElement = maxElement; + this.queryFilters = queryFilters; } public int getCurrentPage() { @@ -65,6 +77,20 @@ public class PaginationInfo { public long getMaxElement() { return maxElement; } + + public String getLink(int page) { + MultiValueMap 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 getBeforePages() { return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList()); diff --git a/src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java b/src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java new file mode 100644 index 0000000..166feed --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java @@ -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); + } + + +} diff --git a/src/main/java/tech/ailef/dbadmin/misc/Utils.java b/src/main/java/tech/ailef/dbadmin/misc/Utils.java index 1667199..b6230ef 100644 --- a/src/main/java/tech/ailef/dbadmin/misc/Utils.java +++ b/src/main/java/tech/ailef/dbadmin/misc/Utils.java @@ -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 computeParams(Set filters) { + MultiValueMap 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 computeFilters(MultiValueMap params) { + if (params == null) + return new HashSet<>(); + + List ops = params.get("filter_op"); + List fields = params.get("filter_field"); + List 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 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 params) { + Set currentParams = params.keySet(); + List 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(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 85abd79..2267faf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,3 +6,4 @@ spring.datasource.password=password spring.jpa.show-sql=true +server.tomcat.relaxed-path-chars=[,] \ No newline at end of file diff --git a/src/main/resources/static/js/filters.js b/src/main/resources/static/js/filters.js index 4f6754f..befd667 100644 --- a/src/main/resources/static/js/filters.js +++ b/src/main/resources/static/js/filters.js @@ -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'); - } 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'); - } + 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 { + 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'); + } + }); + }); }); + + }); \ No newline at end of file diff --git a/src/main/resources/templates/fragments/forms.html b/src/main/resources/templates/fragments/forms.html index 5cf1a9e..ba89fb3 100644 --- a/src/main/resources/templates/fragments/forms.html +++ b/src/main/resources/templates/fragments/forms.html @@ -48,6 +48,10 @@
+ + + @@ -59,14 +63,13 @@
- +
- +
+ + +
@@ -50,13 +53,12 @@ [[ ${schema.getTableName()} ]] - - - -

-

+ +

+ +

@@ -64,10 +66,34 @@
-
+

Filters

- +
+
+ + + +
+ + + + + + +
+
+ + + + + + + +