mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-09 05:48:20 +00:00
WIP
This commit is contained in:
parent
06bd4bf5b1
commit
d34676f7a7
@ -0,0 +1,11 @@
|
|||||||
|
package tech.ailef.dbadmin.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface DisplayImage {
|
||||||
|
}
|
@ -39,14 +39,6 @@ import tech.ailef.dbadmin.misc.Utils;
|
|||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/dbadmin")
|
@RequestMapping("/dbadmin")
|
||||||
/**
|
/**
|
||||||
* - Sort controls DONE
|
|
||||||
* - @DisplayFormat for fields DONE
|
|
||||||
* - Fix pagination in product where total count = page size = 50 (it shows 'next' button and then empty page) DONE
|
|
||||||
* - Show number of entries in home DONE
|
|
||||||
* - @ComputedColumn name parameter DONE
|
|
||||||
* - Basic search
|
|
||||||
* - Improve create/edit UX WIP
|
|
||||||
* - blob edit doesn't show if it's present WIP
|
|
||||||
* - double data source for internal database and settings
|
* - double data source for internal database and settings
|
||||||
* - role based authorization (PRO)
|
* - role based authorization (PRO)
|
||||||
* - Pagination in one to many results?
|
* - Pagination in one to many results?
|
||||||
@ -54,13 +46,12 @@ import tech.ailef.dbadmin.misc.Utils;
|
|||||||
* - AI console (PRO)
|
* - AI console (PRO)
|
||||||
* - Action logs
|
* - Action logs
|
||||||
* - Boolean icons
|
* - Boolean icons
|
||||||
* - @Filterable
|
|
||||||
* - Boolean in create/edit is checkbox
|
* - Boolean in create/edit is checkbox
|
||||||
* - SQL console (PRO)
|
* - SQL console (PRO)
|
||||||
* - JPA Validation (PRO)
|
* - JPA Validation (PRO)
|
||||||
* - Logging
|
* - Logging
|
||||||
* - TODO FIX: list model page crash
|
* - Selenium tests
|
||||||
* EDIT error on table product
|
* - @DisplayImage
|
||||||
* - Logs in web ui
|
* - Logs in web ui
|
||||||
* - Tests: AutocompleteController, REST API, create/edit
|
* - Tests: AutocompleteController, REST API, create/edit
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +8,7 @@ import org.apache.tika.mime.MimeTypes;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@ -31,6 +32,28 @@ public class DownloadController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DbAdmin dbAdmin;
|
private DbAdmin dbAdmin;
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping(value="/{className}/{fieldName}/{id}/image", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||||
|
@ResponseBody
|
||||||
|
public ResponseEntity<byte[]> serveImage(@PathVariable String className,
|
||||||
|
@PathVariable String fieldName, @PathVariable String id) {
|
||||||
|
|
||||||
|
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||||
|
|
||||||
|
Optional<DbObject> object = repository.findById(schema, id);
|
||||||
|
|
||||||
|
if (object.isPresent()) {
|
||||||
|
DbObject dbObject = object.get();
|
||||||
|
DbFieldValue dbFieldValue = dbObject.get(fieldName);
|
||||||
|
byte[] file = (byte[])dbFieldValue.getValue();
|
||||||
|
return ResponseEntity.ok(file);
|
||||||
|
} else {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Object with id " + id + " not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{className}/{fieldName}/{id}")
|
@GetMapping("/{className}/{fieldName}/{id}")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public ResponseEntity<byte[]> serveFile(@PathVariable String className,
|
public ResponseEntity<byte[]> serveFile(@PathVariable String className,
|
||||||
|
@ -80,6 +80,7 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
|||||||
.setFirstResult((page - 1) * pageSize).getResultList();
|
.setFirstResult((page - 1) * pageSize).getResultList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private List<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
|
private List<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
|
||||||
CriteriaBuilder cb, Path root) {
|
CriteriaBuilder cb, Path root) {
|
||||||
List<Predicate> finalPredicates = new ArrayList<>();
|
List<Predicate> finalPredicates = new ArrayList<>();
|
||||||
@ -152,7 +153,8 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
|||||||
return finalPredicates;
|
return finalPredicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
@SuppressWarnings("unchecked")
|
||||||
|
public int update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
||||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||||
|
|
||||||
CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass());
|
CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass());
|
||||||
@ -162,8 +164,9 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
|||||||
for (DbField field : schema.getSortedFields()) {
|
for (DbField field : schema.getSortedFields()) {
|
||||||
if (field.isPrimaryKey()) continue;
|
if (field.isPrimaryKey()) continue;
|
||||||
|
|
||||||
if (params.getOrDefault("__keep_" + field.getJavaName(), "off").equals("on")) {
|
boolean keepValue = params.getOrDefault("__keep_" + field.getJavaName(), "off").equals("on");
|
||||||
System.out.println("SKIPPING: " + field);
|
|
||||||
|
if (keepValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,8 +178,10 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
MultipartFile file = files.get(field.getName());
|
MultipartFile file = files.get(field.getName());
|
||||||
if (file != null)
|
if (file != null) {
|
||||||
value = file.getBytes();
|
if (file.isEmpty()) value = null;
|
||||||
|
else value = file.getBytes();
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new DbAdminException(e);
|
throw new DbAdminException(e);
|
||||||
}
|
}
|
||||||
@ -184,11 +189,11 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
|||||||
|
|
||||||
update.set(employee.get(field.getJavaName()), value);
|
update.set(employee.get(field.getJavaName()), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
String pkName = schema.getPrimaryKey().getJavaName();
|
String pkName = schema.getPrimaryKey().getJavaName();
|
||||||
update.where(cb.equal(employee.get(pkName), params.get(schema.getPrimaryKey().getName())));
|
update.where(cb.equal(employee.get(pkName), params.get(schema.getPrimaryKey().getName())));
|
||||||
|
|
||||||
Query query = entityManager.createQuery(update);
|
Query query = entityManager.createQuery(update);
|
||||||
int rowCount = query.executeUpdate();
|
return query.executeUpdate();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import java.lang.reflect.Field;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
|
import tech.ailef.dbadmin.annotations.DisplayImage;
|
||||||
|
|
||||||
public class DbField {
|
public class DbField {
|
||||||
protected String dbName;
|
protected String dbName;
|
||||||
|
|
||||||
@ -114,6 +116,10 @@ public class DbField {
|
|||||||
return type == DbFieldType.BYTE_ARRAY;
|
return type == DbFieldType.BYTE_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isImage() {
|
||||||
|
return field.getAnnotation(DisplayImage.class) != null;
|
||||||
|
}
|
||||||
|
|
||||||
public String getFormat() {
|
public String getFormat() {
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
.separator-light {
|
||||||
|
opacity: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
form.delete-form {
|
form.delete-form {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@ -174,3 +178,14 @@ AUTOCOMPLETE
|
|||||||
.filterable-field .card-header:hover {
|
.filterable-field .card-header:hover {
|
||||||
background-color: #F0F0F0;
|
background-color: #F0F0F0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Images
|
||||||
|
*/
|
||||||
|
.thumb-image {
|
||||||
|
max-width: 128px;
|
||||||
|
}
|
||||||
|
.img-muted {
|
||||||
|
filter: brightness(50%);
|
||||||
|
|
||||||
|
}
|
@ -1,3 +1,24 @@
|
|||||||
|
function showFileInput(inputElement) {
|
||||||
|
inputElement.classList.add('d-block');
|
||||||
|
inputElement.classList.remove('d-none');
|
||||||
|
inputElement.value = '';
|
||||||
|
|
||||||
|
let img = document.getElementById(`__thumb_${inputElement.name}`);
|
||||||
|
if (img != null) {
|
||||||
|
img.classList.add('img-muted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFileInput(inputElement) {
|
||||||
|
inputElement.classList.add('d-none');
|
||||||
|
inputElement.classList.remove('d-block');
|
||||||
|
|
||||||
|
let img = document.getElementById(`__thumb_${inputElement.name}`);
|
||||||
|
if (img != null) {
|
||||||
|
img.classList.remove('img-muted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
let checkboxes = document.querySelectorAll(".binary-field-checkbox");
|
let checkboxes = document.querySelectorAll(".binary-field-checkbox");
|
||||||
|
|
||||||
@ -5,22 +26,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
let fieldName = checkbox.dataset.fieldname;
|
let fieldName = checkbox.dataset.fieldname;
|
||||||
|
|
||||||
if (!checkbox.checked) {
|
if (!checkbox.checked) {
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.add('d-block');
|
showFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.remove('d-none');
|
|
||||||
document.querySelector(`input[name="${fieldName}"]`).value = '';
|
|
||||||
} else {
|
} else {
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.add('d-none');
|
hideFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.remove('d-block');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox.addEventListener('change', function(e) {
|
checkbox.addEventListener('change', function(e) {
|
||||||
if (!e.target.checked) {
|
if (!e.target.checked) {
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.add('d-block');
|
showFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.remove('d-none');
|
|
||||||
document.querySelector(`input[name="${fieldName}"]`).value = '';
|
|
||||||
} else {
|
} else {
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.add('d-none');
|
hideFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
document.querySelector(`input[name="${fieldName}"]`).classList.remove('d-block');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
<input type="checkbox" class="form-check-input" name="ids"
|
<input type="checkbox" class="form-check-input" name="ids"
|
||||||
th:value="${row.getPrimaryKeyValue()}" form="delete-form">
|
th:value="${row.getPrimaryKeyValue()}" form="delete-form">
|
||||||
</td>
|
</td>
|
||||||
<td th:each="field : ${schema.getSortedFields()}">
|
<td th:each="field : ${schema.getSortedFields()}"
|
||||||
|
th:classAppend="${field.isBinary() ? 'text-center' : ''}">
|
||||||
<th:block th:if="${!row.has(field)}">
|
<th:block th:if="${!row.has(field)}">
|
||||||
<span class="font-monospace null-label">NULL</span>
|
<span class="font-monospace null-label">NULL</span>
|
||||||
</th:block>
|
</th:block>
|
||||||
@ -61,12 +62,19 @@
|
|||||||
</th:block>
|
</th:block>
|
||||||
<span th:unless="${!field.isBinary()}">
|
<span th:unless="${!field.isBinary()}">
|
||||||
<th:block th:if="${object.get(field).getValue()}">
|
<th:block th:if="${object.get(field).getValue()}">
|
||||||
|
<div th:if="${field.isImage()}" class="mb-2">
|
||||||
|
<img class="thumb-image"
|
||||||
|
th:src="|/dbadmin/download/${schema.getClassName()}/${field.getJavaName()}/${object.getPrimaryKeyValue()}/image|">
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="text-decoration-none null-label"
|
<a class="text-decoration-none null-label"
|
||||||
th:href="|/dbadmin/download/${schema.getJavaClass().getName()}/${field.getJavaName()}/${object.get(schema.getPrimaryKey()).getValue()}|">
|
th:href="|/dbadmin/download/${schema.getClassName()}/${field.getJavaName()}/${object.getPrimaryKeyValue()}|">
|
||||||
<i class="align-middle bi bi-box-arrow-down"></i><span class="align-middle"> Download
|
<i class="align-middle bi bi-box-arrow-down"></i><span class="align-middle"> Download
|
||||||
<!--/*--> <span class="text-muted">([[ ${object.get(field).getValue().length} ]] bytes)</span> <!--*/-->
|
<!--/*--> <span class="text-muted">([[ ${object.get(field).getValue().length} ]] bytes)</span> <!--*/-->
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:unless="${object.get(field).getValue()}">
|
<th:block th:unless="${object.get(field).getValue()}">
|
||||||
<span class="font-monospace null-label">NULL</span>
|
<span class="font-monospace null-label">NULL</span>
|
||||||
|
@ -13,9 +13,10 @@
|
|||||||
|
|
||||||
<h1 class="fw-bold mb-4">
|
<h1 class="fw-bold mb-4">
|
||||||
<i class="align-middle bi bi-database"></i>
|
<i class="align-middle bi bi-database"></i>
|
||||||
<span class="align-middle">Entities</span>
|
<span class="align-middle"><a href="/dbadmin">Entities</a></span>
|
||||||
<i class="align-middle bi bi-chevron-double-right"></i>
|
<i class="align-middle bi bi-chevron-double-right"></i>
|
||||||
<span class="align-middle"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
|
<a class="align-middle" th:href="|/dbadmin/model/${schema.getJavaClass().getName()}|">
|
||||||
|
[[ ${schema.getJavaClass().getSimpleName()} ]] </a>
|
||||||
<i class="align-middle bi bi-chevron-double-right"></i>
|
<i class="align-middle bi bi-chevron-double-right"></i>
|
||||||
<span class="align-middle" th:text="${create ? 'Create' : 'Edit'}"></span>
|
<span class="align-middle" th:text="${create ? 'Create' : 'Edit'}"></span>
|
||||||
<th:block th:if="${!create}">
|
<th:block th:if="${!create}">
|
||||||
@ -30,8 +31,9 @@
|
|||||||
<form class="form" enctype="multipart/form-data" method="post" th:action="|/dbadmin/model/${className}/create|">
|
<form class="form" enctype="multipart/form-data" method="post" th:action="|/dbadmin/model/${className}/create|">
|
||||||
<input type="hidden" name="__dbadmin_create" th:value="${create}">
|
<input type="hidden" name="__dbadmin_create" th:value="${create}">
|
||||||
<div th:each="field : ${schema.getSortedFields()}" class="mt-2">
|
<div th:each="field : ${schema.getSortedFields()}" class="mt-2">
|
||||||
<label th:for="|__id_${field.getName()}|" class="mb-1">[[ ${field.getName()} ]]</label>
|
<label th:for="|__id_${field.getName()}|" class="mb-1 fw-bold">
|
||||||
|
[[ ${field.getName()} ]]
|
||||||
|
</label>
|
||||||
|
|
||||||
<th:block th:if="${field.isForeignKey()}">
|
<th:block th:if="${field.isForeignKey()}">
|
||||||
<div th:replace="~{fragments/forms :: input_autocomplete(field=${field}, value=${
|
<div th:replace="~{fragments/forms :: input_autocomplete(field=${field}, value=${
|
||||||
@ -54,9 +56,11 @@
|
|||||||
step="any"
|
step="any"
|
||||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||||
oninput="this.setCustomValidity('')">
|
oninput="this.setCustomValidity('')">
|
||||||
<!--/*--> Binary field flag <!--*/-->
|
|
||||||
|
<!--/*--> Binary field <!--*/-->
|
||||||
<th:block th:if="${field.isBinary()}">
|
<th:block th:if="${field.isBinary()}">
|
||||||
<div th:if="${object.get(field).getValue() != null}">
|
<!--/*--> Edit options <!--*/-->
|
||||||
|
<div th:if="${!create && object.get(field).getValue() != null}">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
class="binary-field-checkbox"
|
class="binary-field-checkbox"
|
||||||
th:data-fieldname="${field.getName()}"
|
th:data-fieldname="${field.getName()}"
|
||||||
@ -64,16 +68,21 @@
|
|||||||
checked
|
checked
|
||||||
th:name="|__keep_${field.getName()}|">
|
th:name="|__keep_${field.getName()}|">
|
||||||
<span>Keep current data</span>
|
<span>Keep current data</span>
|
||||||
|
<div th:if="${field.isImage()}" class="mb-2">
|
||||||
|
<img class="thumb-image" th:id="|__thumb_${field.getName()}|"
|
||||||
|
th:src="|/dbadmin/download/${schema.getClassName()}/${field.getJavaName()}/${object.getPrimaryKeyValue()}/image|">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!--/*--> File input <!--*/-->
|
||||||
<input th:if="${field.isBinary()}" placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
<input th:if="${field.isBinary()}" placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||||
th:name="${field.getName()}"
|
th:name="${field.getName()}"
|
||||||
class="form-control mt-2" th:id="|__id_${field.getName()}|"
|
class="form-control mt-2" th:id="|__id_${field.getName()}|"
|
||||||
th:classAppend="${object.get(field).getValue() == null ? '' : ''}"
|
|
||||||
th:required="${!field.isNullable()}"
|
th:required="${!field.isNullable()}"
|
||||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||||
oninput="this.setCustomValidity('')">
|
oninput="this.setCustomValidity('')">
|
||||||
</th:block>
|
</th:block>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
<div class="separator mt-3 mb-2 separator-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div th:each="field : ${schema.getManyToManyOwnedFields()}" class="mt-3">
|
<div th:each="field : ${schema.getManyToManyOwnedFields()}" class="mt-3">
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<h1 class="fw-bold mb-4"><i class="align-middle bi bi-database"></i>
|
<h1 class="fw-bold mb-4"><i class="align-middle bi bi-database"></i>
|
||||||
<a class="align-middle" href="/dbadmin">Entities</a>
|
<a class="align-middle" href="/dbadmin">Entities</a>
|
||||||
<i class="align-middle bi bi-chevron-double-right"></i>
|
<i class="align-middle bi bi-chevron-double-right"></i>
|
||||||
<a class="align-middle "th:href="|/dbadmin/model/${schema.getJavaClass().getName()}|">
|
<a class="align-middle" th:href="|/dbadmin/model/${schema.getJavaClass().getName()}|">
|
||||||
[[ ${schema.getJavaClass().getSimpleName()} ]]</a>
|
[[ ${schema.getJavaClass().getSimpleName()} ]]</a>
|
||||||
<i class="align-middle bi bi-chevron-double-right"></i>
|
<i class="align-middle bi bi-chevron-double-right"></i>
|
||||||
<span class="align-middle"> [[ ${object.getDisplayName()} ]]</span>
|
<span class="align-middle"> [[ ${object.getDisplayName()} ]]</span>
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<i title="Foreign Key" class="bi bi-link"></i>
|
<i title="Foreign Key" class="bi bi-link"></i>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="fw-bold">
|
||||||
<span class="m-0 p-0" th:text="${field.getName()}"></span>
|
<span class="m-0 p-0" th:text="${field.getName()}"></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user