mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-09 05:48:20 +00:00
0.0.3
This commit is contained in:
parent
8039801940
commit
5ba037057f
19
README.md
19
README.md
@ -65,7 +65,7 @@ public String getName() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To show an item in a table its primary key is used by default. If you set a method as `@DisplayName` in your `@Entity` class, this result will be shown in addition to its primary key wherever possible.
|
When displaying a reference to an item, by default we show its primary key. If a class has a `@DisplayName`, this method will be used in addition to the primary key whenever possible, giving the user a more readable option.
|
||||||
|
|
||||||
### @DisplayFormat
|
### @DisplayFormat
|
||||||
```
|
```
|
||||||
@ -73,7 +73,7 @@ To show an item in a table its primary key is used by default. If you set a meth
|
|||||||
private Double price;
|
private Double price;
|
||||||
```
|
```
|
||||||
|
|
||||||
Specify a format to apply when displaying the field.
|
Specify a format string to apply when displaying the field.
|
||||||
|
|
||||||
### @ComputedColumn
|
### @ComputedColumn
|
||||||
```
|
```
|
||||||
@ -87,7 +87,7 @@ public double totalSpent() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Add an extra field that's computed at runtime instead of a database column. It will be displayed everywhere as a normal, read-only column.
|
This annotation can be used to add values computed at runtime that are shown like additional columns.
|
||||||
|
|
||||||
### @Filterable
|
### @Filterable
|
||||||
|
|
||||||
@ -96,11 +96,22 @@ Add an extra field that's computed at runtime instead of a database column. It w
|
|||||||
private LocalDate createdAt;
|
private LocalDate createdAt;
|
||||||
```
|
```
|
||||||
|
|
||||||
Place on one or more fields in a class to activate the faceted search feature. This will allow you to easily combine all these filters when operating on this table.
|
Place on one or more fields in a class to activate the faceted search feature. This will allow you to easily combine all these filters when operating on the table. Can only be placed on fields that correspond to physical columns on the table (e.g. no `@ManyToMany`/`@OneToMany`) and that are not binary (`byte[]`).
|
||||||
|
|
||||||
|
### @DisplayImage
|
||||||
|
|
||||||
|
```
|
||||||
|
@DisplayImage
|
||||||
|
private byte[] image;
|
||||||
|
```
|
||||||
|
|
||||||
|
This annotation can be placed on binary fields to declare they are storing an image and that we want it displayed when possible. The image will be shown as a small thumbnail.
|
||||||
|
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
0.0.3 - @DisplayImage; Selenium tests; Fixed/greatly improved edit page;
|
||||||
|
|
||||||
0.0.2 - Faceted search with `@Filterable` annotation
|
0.0.2 - Faceted search with `@Filterable` annotation
|
||||||
|
|
||||||
0.0.1 - First alpha release (basic CRUD features)
|
0.0.1 - First alpha release (basic CRUD features)
|
||||||
|
@ -4,6 +4,9 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.context.ApplicationContextAware;
|
import org.springframework.context.ApplicationContextAware;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class the get the ApplicationContext
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class ApplicationContextUtils implements ApplicationContextAware {
|
public class ApplicationContextUtils implements ApplicationContextAware {
|
||||||
|
|
||||||
|
@ -35,6 +35,15 @@ import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
|
|||||||
import tech.ailef.dbadmin.exceptions.DbAdminException;
|
import tech.ailef.dbadmin.exceptions.DbAdminException;
|
||||||
import tech.ailef.dbadmin.misc.Utils;
|
import tech.ailef.dbadmin.misc.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main DbAdmin class responsible for the initialization phase. This class scans
|
||||||
|
* the user provided package containing the `@Entity` definitions and tries to map each
|
||||||
|
* entity to a DbObjectSchema instance.
|
||||||
|
*
|
||||||
|
* This process involves determining the correct type for each class field and its
|
||||||
|
* configuration at the database level. An exception will be thrown if it's not possible
|
||||||
|
* to determine the field type.
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class DbAdmin {
|
public class DbAdmin {
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
@ -57,10 +66,6 @@ public class DbAdmin {
|
|||||||
this.modelsPackage = applicationClass.getModelsPackage();
|
this.modelsPackage = applicationClass.getModelsPackage();
|
||||||
this.entityManager = entityManager;
|
this.entityManager = entityManager;
|
||||||
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init() {
|
|
||||||
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
|
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
|
||||||
provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
|
provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
|
||||||
|
|
||||||
@ -70,6 +75,45 @@ public class DbAdmin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the loaded schemas (i.e. entity classes)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<DbObjectSchema> getSchemas() {
|
||||||
|
return Collections.unmodifiableList(schemas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a schema by its full class name
|
||||||
|
* @param className qualified class name
|
||||||
|
* @return
|
||||||
|
* @throws DbAdminException if corresponding schema not found
|
||||||
|
*/
|
||||||
|
public DbObjectSchema findSchemaByClassName(String className) {
|
||||||
|
return schemas.stream().filter(s -> s.getClassName().equals(className)).findFirst().orElseThrow(() -> {
|
||||||
|
return new DbAdminException("Schema " + className + " not found.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a schema by its class
|
||||||
|
* @param klass
|
||||||
|
* @return
|
||||||
|
* @throws DbAdminException if corresponding schema not found
|
||||||
|
*/
|
||||||
|
public DbObjectSchema findSchemaByClass(Class<?> klass) {
|
||||||
|
return findSchemaByClassName(klass.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method processes a BeanDefinition into a DbObjectSchema object,
|
||||||
|
* where all fields have been correctly mapped to DbField objects.
|
||||||
|
*
|
||||||
|
* If any field is not mappable, the method will throw an exception.
|
||||||
|
* @param bd
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
private DbObjectSchema processBeanDefinition(BeanDefinition bd) {
|
private DbObjectSchema processBeanDefinition(BeanDefinition bd) {
|
||||||
String fullClassName = bd.getBeanClassName();
|
String fullClassName = bd.getBeanClassName();
|
||||||
|
|
||||||
@ -88,9 +132,7 @@ public class DbAdmin {
|
|||||||
System.out.println(" - Mapping field " + f);
|
System.out.println(" - Mapping field " + f);
|
||||||
DbField field = mapField(f, schema);
|
DbField field = mapField(f, schema);
|
||||||
if (field == null) {
|
if (field == null) {
|
||||||
// continue;
|
throw new DbAdminException("Impossible to map field: " + f);
|
||||||
// TODO: CHECK THIS EXCEPTION
|
|
||||||
throw new DbAdminException("IMPOSSIBLE TO MAP FIELD: " + f);
|
|
||||||
}
|
}
|
||||||
field.setSchema(schema);
|
field.setSchema(schema);
|
||||||
|
|
||||||
@ -123,6 +165,11 @@ public class DbAdmin {
|
|||||||
return fieldName;
|
return fieldName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a field is nullable from the `@Column` annotation
|
||||||
|
* @param f
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
private boolean determineNullable(Field f) {
|
private boolean determineNullable(Field f) {
|
||||||
Column[] columnAnnotations = f.getAnnotationsByType(Column.class);
|
Column[] columnAnnotations = f.getAnnotationsByType(Column.class);
|
||||||
|
|
||||||
@ -135,6 +182,15 @@ public class DbAdmin {
|
|||||||
return nullable;
|
return nullable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a DbField object from a primitive Java field. This process involves
|
||||||
|
* determining the correct field name on the database, its type and additional
|
||||||
|
* attributes (e.g. nullable).
|
||||||
|
* This method returns null if a field cannot be mapped to a supported type.
|
||||||
|
* @param f primitive Java field to construct a DbField from
|
||||||
|
* @param schema the schema this field belongs to
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
private DbField mapField(Field f, DbObjectSchema schema) {
|
private DbField mapField(Field f, DbObjectSchema schema) {
|
||||||
OneToMany oneToMany = f.getAnnotation(OneToMany.class);
|
OneToMany oneToMany = f.getAnnotation(OneToMany.class);
|
||||||
ManyToMany manyToMany = f.getAnnotation(ManyToMany.class);
|
ManyToMany manyToMany = f.getAnnotation(ManyToMany.class);
|
||||||
@ -208,7 +264,7 @@ public class DbAdmin {
|
|||||||
* Returns the type of a foreign key field, by looking at the type
|
* Returns the type of a foreign key field, by looking at the type
|
||||||
* of the primary key (defined as `@Id`) in the referenced table.
|
* of the primary key (defined as `@Id`) in the referenced table.
|
||||||
*
|
*
|
||||||
* @param f
|
* @param entityClass
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private DbFieldType mapForeignKeyType(Class<?> entityClass) {
|
private DbFieldType mapForeignKeyType(Class<?> entityClass) {
|
||||||
@ -231,26 +287,4 @@ public class DbAdmin {
|
|||||||
throw new DbAdminException(e);
|
throw new DbAdminException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBasePackage() {
|
|
||||||
return modelsPackage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DbObjectSchema> getSchemas() {
|
|
||||||
return Collections.unmodifiableList(schemas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbObjectSchema findSchemaByClassName(String className) {
|
|
||||||
return schemas.stream().filter(s -> s.getClassName().equals(className)).findFirst().orElseThrow(() -> {
|
|
||||||
return new DbAdminException("Schema " + className + " not found.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbObjectSchema findSchemaByClass(Class<?> klass) {
|
|
||||||
return findSchemaByClassName(klass.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityManager getEntityManager() {
|
|
||||||
return entityManager;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,12 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This annotation marks a method as a "virtual" whose value is computed by
|
||||||
|
* using the method itself rather than retrieving it like a physical column
|
||||||
|
* from the database.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
public @interface ComputedColumn {
|
public @interface ComputedColumn {
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package tech.ailef.dbadmin.annotations;
|
package tech.ailef.dbadmin.annotations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface that includes all the configuration methods that
|
||||||
|
* the user has to implement in order to integrate DbAdmin.
|
||||||
|
*
|
||||||
|
*/
|
||||||
public interface DbAdminAppConfiguration {
|
public interface DbAdminAppConfiguration {
|
||||||
public String getModelsPackage();
|
public String getModelsPackage();
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,10 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the class that holds the DbAdmin configuration.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
public @interface DbAdminConfiguration {
|
public @interface DbAdminConfiguration {
|
||||||
|
@ -5,6 +5,11 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies a format string for a field, which will be automatically applied
|
||||||
|
* when displaying its value.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.FIELD)
|
@Target(ElementType.FIELD)
|
||||||
public @interface DisplayFormat {
|
public @interface DisplayFormat {
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a binary field as containing an image, which in turn enables
|
||||||
|
* its display in the interface.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface DisplayImage {
|
||||||
|
}
|
@ -5,6 +5,12 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a method as returning a name that has to be used to display
|
||||||
|
* this item, in addition to its primary key. Use to give users more
|
||||||
|
* readable item names.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
public @interface DisplayName {
|
public @interface DisplayName {
|
||||||
|
@ -5,6 +5,14 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a field as filterable and places it in the faceted search bar.
|
||||||
|
* (This bar only appears in the interface if one or more fields are filterable
|
||||||
|
* in the current schema.)
|
||||||
|
* Can only be placed on fields that correspond to physical columns on the
|
||||||
|
* table (e.g. no `@ManyToMany`/`@OneToMany`) and that are not binary (`byte[]`).
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.FIELD)
|
@Target(ElementType.FIELD)
|
||||||
public @interface Filterable {
|
public @interface Filterable {
|
||||||
|
@ -11,6 +11,7 @@ import java.util.stream.Collectors;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.jdbc.UncategorizedSQLException;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
@ -36,34 +37,11 @@ import tech.ailef.dbadmin.dto.QueryFilter;
|
|||||||
import tech.ailef.dbadmin.exceptions.InvalidPageException;
|
import tech.ailef.dbadmin.exceptions.InvalidPageException;
|
||||||
import tech.ailef.dbadmin.misc.Utils;
|
import tech.ailef.dbadmin.misc.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main DbAdmin controller that register most of the routes of the web interface.
|
||||||
|
*/
|
||||||
@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
|
|
||||||
* - role based authorization (PRO)
|
|
||||||
* - Pagination in one to many results?
|
|
||||||
* - BLOB upload (WIP: check edit not working)
|
|
||||||
* - AI console (PRO)
|
|
||||||
* - Action logs
|
|
||||||
* - Boolean icons
|
|
||||||
* - @Filterable
|
|
||||||
* - Boolean in create/edit is checkbox
|
|
||||||
* - SQL console (PRO)
|
|
||||||
* - JPA Validation (PRO)
|
|
||||||
* - Logging
|
|
||||||
* - TODO FIX: list model page crash
|
|
||||||
* EDIT error on table product
|
|
||||||
* - Logs in web ui
|
|
||||||
* - Tests: AutocompleteController, REST API, create/edit
|
|
||||||
*/
|
|
||||||
public class DefaultDbAdminController {
|
public class DefaultDbAdminController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private DbAdminRepository repository;
|
private DbAdminRepository repository;
|
||||||
@ -71,6 +49,12 @@ public class DefaultDbAdminController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DbAdmin dbAdmin;
|
private DbAdmin dbAdmin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Home page with list of schemas
|
||||||
|
* @param model
|
||||||
|
* @param query
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String index(Model model, @RequestParam(required = false) String query) {
|
public String index(Model model, @RequestParam(required = false) String query) {
|
||||||
List<DbObjectSchema> schemas = dbAdmin.getSchemas();
|
List<DbObjectSchema> schemas = dbAdmin.getSchemas();
|
||||||
@ -90,10 +74,27 @@ public class DefaultDbAdminController {
|
|||||||
model.addAttribute("activePage", "home");
|
model.addAttribute("activePage", "home");
|
||||||
model.addAttribute("title", "Entities | Index");
|
model.addAttribute("title", "Entities | Index");
|
||||||
|
|
||||||
|
|
||||||
return "home";
|
return "home";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the items of a schema by applying a variety of filters:
|
||||||
|
* - query: fuzzy search
|
||||||
|
* - otherParams: filterable fields
|
||||||
|
* Includes pagination and sorting options.
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* @param className
|
||||||
|
* @param page
|
||||||
|
* @param query
|
||||||
|
* @param pageSize
|
||||||
|
* @param sortKey
|
||||||
|
* @param sortOrder
|
||||||
|
* @param otherParams
|
||||||
|
* @param request
|
||||||
|
* @param response
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@GetMapping("/model/{className}")
|
@GetMapping("/model/{className}")
|
||||||
public String list(Model model, @PathVariable String className,
|
public String list(Model model, @PathVariable String className,
|
||||||
@RequestParam(required=false) Integer page, @RequestParam(required=false) String query,
|
@RequestParam(required=false) Integer page, @RequestParam(required=false) String query,
|
||||||
@ -166,6 +167,12 @@ public class DefaultDbAdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays information about the schema
|
||||||
|
* @param model
|
||||||
|
* @param className
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@GetMapping("/model/{className}/schema")
|
@GetMapping("/model/{className}/schema")
|
||||||
public String schema(Model model, @PathVariable String className) {
|
public String schema(Model model, @PathVariable String className) {
|
||||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||||
@ -176,6 +183,13 @@ public class DefaultDbAdminController {
|
|||||||
return "model/schema";
|
return "model/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a single item
|
||||||
|
* @param model
|
||||||
|
* @param className
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@GetMapping("/model/{className}/show/{id}")
|
@GetMapping("/model/{className}/show/{id}")
|
||||||
public String show(Model model, @PathVariable String className, @PathVariable String id) {
|
public String show(Model model, @PathVariable String className, @PathVariable String id) {
|
||||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||||
@ -281,8 +295,6 @@ public class DefaultDbAdminController {
|
|||||||
@RequestParam MultiValueMap<String, String> formParams,
|
@RequestParam MultiValueMap<String, String> formParams,
|
||||||
@RequestParam Map<String, MultipartFile> files,
|
@RequestParam Map<String, MultipartFile> files,
|
||||||
RedirectAttributes attr) {
|
RedirectAttributes attr) {
|
||||||
|
|
||||||
|
|
||||||
// Extract all parameters that have exactly 1 value,
|
// Extract all parameters that have exactly 1 value,
|
||||||
// as these will be the raw values for the object that is being
|
// as these will be the raw values for the object that is being
|
||||||
// created.
|
// created.
|
||||||
@ -341,6 +353,10 @@ public class DefaultDbAdminController {
|
|||||||
attr.addFlashAttribute("errorTitle", "Unable to INSERT row");
|
attr.addFlashAttribute("errorTitle", "Unable to INSERT row");
|
||||||
attr.addFlashAttribute("error", e.getMessage());
|
attr.addFlashAttribute("error", e.getMessage());
|
||||||
attr.addFlashAttribute("params", params);
|
attr.addFlashAttribute("params", params);
|
||||||
|
} catch (UncategorizedSQLException e) {
|
||||||
|
attr.addFlashAttribute("errorTitle", "Unable to INSERT row");
|
||||||
|
attr.addFlashAttribute("error", e.getMessage());
|
||||||
|
attr.addFlashAttribute("params", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -360,6 +376,10 @@ public class DefaultDbAdminController {
|
|||||||
attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)");
|
attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)");
|
||||||
attr.addFlashAttribute("error", e.getMessage());
|
attr.addFlashAttribute("error", e.getMessage());
|
||||||
attr.addFlashAttribute("params", params);
|
attr.addFlashAttribute("params", params);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)");
|
||||||
|
attr.addFlashAttribute("error", e.getMessage());
|
||||||
|
attr.addFlashAttribute("params", params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -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;
|
||||||
@ -21,7 +22,11 @@ import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
|
|||||||
import tech.ailef.dbadmin.dbmapping.DbFieldValue;
|
import tech.ailef.dbadmin.dbmapping.DbFieldValue;
|
||||||
import tech.ailef.dbadmin.dbmapping.DbObject;
|
import tech.ailef.dbadmin.dbmapping.DbObject;
|
||||||
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
|
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
|
||||||
|
import tech.ailef.dbadmin.exceptions.DbAdminException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller to serve file or images (`@DisplayImage`)
|
||||||
|
*/
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/dbadmin/download")
|
@RequestMapping("/dbadmin/download")
|
||||||
public class DownloadController {
|
public class DownloadController {
|
||||||
@ -31,6 +36,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,
|
||||||
@ -42,7 +69,18 @@ public class DownloadController {
|
|||||||
|
|
||||||
if (object.isPresent()) {
|
if (object.isPresent()) {
|
||||||
DbObject dbObject = object.get();
|
DbObject dbObject = object.get();
|
||||||
DbFieldValue dbFieldValue = dbObject.get(fieldName);
|
|
||||||
|
DbFieldValue dbFieldValue;
|
||||||
|
try {
|
||||||
|
dbFieldValue = dbObject.get(fieldName);
|
||||||
|
} catch (DbAdminException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Field not found", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbFieldValue.getValue() == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "There's no file attached to this item");
|
||||||
|
}
|
||||||
|
|
||||||
byte[] file = (byte[])dbFieldValue.getValue();
|
byte[] file = (byte[])dbFieldValue.getValue();
|
||||||
|
|
||||||
String filename = schema.getClassName() + "_" + id + "_" + fieldName;
|
String filename = schema.getClassName() + "_" + id + "_" + fieldName;
|
||||||
|
@ -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,16 +153,20 @@ 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());
|
||||||
|
|
||||||
Root employee = update.from(schema.getJavaClass());
|
Root root = update.from(schema.getJavaClass());
|
||||||
|
|
||||||
for (DbField field : schema.getSortedFields()) {
|
for (DbField field : schema.getSortedFields()) {
|
||||||
if (field.isPrimaryKey()) continue;
|
if (field.isPrimaryKey()) continue;
|
||||||
|
|
||||||
|
boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on");
|
||||||
|
if (keepValue) continue;
|
||||||
|
|
||||||
String stringValue = params.get(field.getName());
|
String stringValue = params.get(field.getName());
|
||||||
Object value = null;
|
Object value = null;
|
||||||
if (stringValue != null && stringValue.isBlank()) stringValue = null;
|
if (stringValue != null && stringValue.isBlank()) stringValue = null;
|
||||||
@ -169,21 +174,26 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
|||||||
value = field.getType().parseValue(stringValue);
|
value = field.getType().parseValue(stringValue);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
MultipartFile file = files.get(field.getJavaName());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update.set(employee.get(field.getJavaName()), value);
|
if (field.getConnectedSchema() != null)
|
||||||
|
value = field.getConnectedSchema().getJpaRepository().findById(value).get();
|
||||||
|
|
||||||
|
update.set(root.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(root.get(pkName), params.get(schema.getPrimaryKey().getName())));
|
||||||
|
|
||||||
Query query = entityManager.createQuery(update);
|
Query query = entityManager.createQuery(update);
|
||||||
int rowCount = query.executeUpdate();
|
return query.executeUpdate();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,16 +131,7 @@ public class DbAdminRepository {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
||||||
// Object[] updateArray = schema.getUpdateArray(params, files);
|
|
||||||
//
|
|
||||||
// String updateFields =
|
|
||||||
// schema.getSortedFields().stream().map(f -> "`" + f.getName() + "` = ?").collect(Collectors.joining(", "));
|
|
||||||
//
|
|
||||||
// String query = "UPDATE `" + schema.getTableName() + "` SET " + updateFields + " WHERE `" + schema.getPrimaryKey().getName() + "` = ?";
|
|
||||||
// jdbcTemplate.update(query, updateArray);
|
|
||||||
|
|
||||||
schema.getJpaRepository().update(schema, params, files);
|
schema.getJpaRepository().update(schema, params, files);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@ -195,6 +186,12 @@ public class DbAdminRepository {
|
|||||||
Map<String, Object> allValues = new HashMap<>();
|
Map<String, Object> allValues = new HashMap<>();
|
||||||
allValues.putAll(values);
|
allValues.putAll(values);
|
||||||
|
|
||||||
|
values.keySet().forEach(fieldName -> {
|
||||||
|
if (values.get(fieldName).isBlank()) {
|
||||||
|
allValues.put(fieldName, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
files.keySet().forEach(f -> {
|
files.keySet().forEach(f -> {
|
||||||
try {
|
try {
|
||||||
allValues.put(f, files.get(f).getBytes());
|
allValues.put(f, files.get(f).getBytes());
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,9 @@ public class DbObject {
|
|||||||
private DbObjectSchema schema;
|
private DbObjectSchema schema;
|
||||||
|
|
||||||
public DbObject(Object instance, DbObjectSchema schema) {
|
public DbObject(Object instance, DbObjectSchema schema) {
|
||||||
|
if (instance == null)
|
||||||
|
throw new DbAdminException("Trying to build object with instance == null");
|
||||||
|
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
}
|
}
|
||||||
@ -54,6 +57,8 @@ public class DbObject {
|
|||||||
OneToOne oneToOne = field.getPrimitiveField().getAnnotation(OneToOne.class);
|
OneToOne oneToOne = field.getPrimitiveField().getAnnotation(OneToOne.class);
|
||||||
if (oneToOne != null || manyToOne != null) {
|
if (oneToOne != null || manyToOne != null) {
|
||||||
Object linkedObject = get(field.getJavaName()).getValue();
|
Object linkedObject = get(field.getJavaName()).getValue();
|
||||||
|
if (linkedObject == null) return null;
|
||||||
|
|
||||||
DbObject linkedDbObject = new DbObject(linkedObject, field.getConnectedSchema());
|
DbObject linkedDbObject = new DbObject(linkedObject, field.getConnectedSchema());
|
||||||
return linkedDbObject;
|
return linkedDbObject;
|
||||||
} else {
|
} else {
|
||||||
@ -120,7 +125,9 @@ public class DbObject {
|
|||||||
|
|
||||||
if (displayNameMethod.isPresent()) {
|
if (displayNameMethod.isPresent()) {
|
||||||
try {
|
try {
|
||||||
return displayNameMethod.get().invoke(instance).toString();
|
Object displayName = displayNameMethod.get().invoke(instance);
|
||||||
|
if (displayName == null) return null;
|
||||||
|
else return displayName.toString();
|
||||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||||
throw new DbAdminException(e);
|
throw new DbAdminException(e);
|
||||||
}
|
}
|
||||||
@ -147,26 +154,6 @@ public class DbObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// public void initializeFromMap(Map<String, String> values) {
|
|
||||||
//// String pkValue = values.get(schema.getPrimaryKey().getName());
|
|
||||||
//
|
|
||||||
// List<String> fields =
|
|
||||||
// values.keySet().stream().filter(f -> !f.startsWith("__dbadmin_")).collect(Collectors.toList());
|
|
||||||
//
|
|
||||||
// for (String field : fields) {
|
|
||||||
// String fieldJavaName = Utils.snakeToCamel(field);
|
|
||||||
// Method setter = findSetter(fieldJavaName);
|
|
||||||
// if (setter == null)
|
|
||||||
// throw new DbAdminException("Unable to find setter for field " + fieldJavaName + " in class " + schema.getClassName());
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// setter.invoke(instance, values.get(field));
|
|
||||||
// } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
|
||||||
// throw new DbAdminException(e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
public void set(String fieldName, Object value) {
|
public void set(String fieldName, Object value) {
|
||||||
Method setter = findSetter(fieldName);
|
Method setter = findSetter(fieldName);
|
||||||
|
|
||||||
@ -199,8 +186,11 @@ public class DbObject {
|
|||||||
String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
||||||
Method[] methods = instance.getClass().getDeclaredMethods();
|
Method[] methods = instance.getClass().getDeclaredMethods();
|
||||||
|
|
||||||
|
DbField dbField = schema.getFieldByJavaName(fieldName);
|
||||||
|
if (dbField == null) return null;
|
||||||
|
|
||||||
String prefix = "get";
|
String prefix = "get";
|
||||||
if (schema.getFieldByJavaName(fieldName).getType() == DbFieldType.BOOLEAN) {
|
if (dbField.getType() == DbFieldType.BOOLEAN) {
|
||||||
prefix = "is";
|
prefix = "is";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||||||
|
|
||||||
import jakarta.persistence.ManyToMany;
|
import jakarta.persistence.ManyToMany;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import tech.ailef.dbadmin.DbAdmin;
|
import tech.ailef.dbadmin.DbAdmin;
|
||||||
import tech.ailef.dbadmin.annotations.ComputedColumn;
|
import tech.ailef.dbadmin.annotations.ComputedColumn;
|
||||||
@ -128,8 +129,13 @@ public class DbObjectSchema {
|
|||||||
public List<DbField> getSortedFields() {
|
public List<DbField> getSortedFields() {
|
||||||
return getFields().stream()
|
return getFields().stream()
|
||||||
.filter(f -> {
|
.filter(f -> {
|
||||||
return f.getPrimitiveField().getAnnotation(OneToMany.class) == null
|
boolean toMany = f.getPrimitiveField().getAnnotation(OneToMany.class) == null
|
||||||
&& f.getPrimitiveField().getAnnotation(ManyToMany.class) == null;
|
&& f.getPrimitiveField().getAnnotation(ManyToMany.class) == null;
|
||||||
|
|
||||||
|
OneToOne oneToOne = f.getPrimitiveField().getAnnotation(OneToOne.class);
|
||||||
|
boolean mappedBy = oneToOne != null && !oneToOne.mappedBy().isBlank();
|
||||||
|
|
||||||
|
return toMany && !mappedBy;
|
||||||
})
|
})
|
||||||
.sorted((a, b) -> {
|
.sorted((a, b) -> {
|
||||||
if (a.isPrimaryKey() && !b.isPrimaryKey())
|
if (a.isPrimaryKey() && !b.isPrimaryKey())
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
.separator-light {
|
||||||
|
opacity: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
form.delete-form {
|
form.delete-form {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@ -52,7 +56,7 @@ tr.table-data-row td:last-child, tr.table-data-row th:last-child {
|
|||||||
|
|
||||||
.row-icons {
|
.row-icons {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
width: 128px;
|
width: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 .bi {
|
h1 .bi {
|
||||||
@ -112,6 +116,8 @@ h1 a:hover {
|
|||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
-webkit-box-shadow: 0px 11px 12px -1px rgba(0,0,0,0.13);
|
||||||
|
box-shadow: 0px 11px 12px -1px rgba(0,0,0,0.13);
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion {
|
.suggestion {
|
||||||
@ -124,7 +130,7 @@ h1 a:hover {
|
|||||||
|
|
||||||
.suggestion:hover {
|
.suggestion:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #FFF;
|
background-color: #EBF7FF;
|
||||||
border-bottom: 2px solid #ADDEFF;
|
border-bottom: 2px solid #ADDEFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,3 +180,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%);
|
||||||
|
|
||||||
|
}
|
43
src/main/resources/static/js/create.js
Normal file
43
src/main/resources/static/js/create.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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", () => {
|
||||||
|
let checkboxes = document.querySelectorAll(".binary-field-checkbox");
|
||||||
|
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
let fieldName = checkbox.dataset.fieldname;
|
||||||
|
|
||||||
|
if (!checkbox.checked) {
|
||||||
|
showFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
|
} else {
|
||||||
|
hideFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox.addEventListener('change', function(e) {
|
||||||
|
if (!e.target.checked) {
|
||||||
|
showFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
|
} else {
|
||||||
|
hideFileInput(document.querySelector(`input[name="${fieldName}"]`));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,15 +1,23 @@
|
|||||||
function updateBulkActions(table, selected) {
|
function updateBulkActions(table, selected) {
|
||||||
let divs = document.querySelectorAll(".bulk-actions");
|
let divs = document.querySelectorAll(".bulk-actions");
|
||||||
divs.forEach(div => {
|
divs.forEach(div => {
|
||||||
div.innerHTML = `${selected} items selected <input type="submit" form="delete-form" class="ui-btn btn btn-secondary" value="Delete">`;
|
div.innerHTML = `${selected} items selected <input type="submit" form="multi-delete-form" class="ui-btn btn btn-secondary" value="Delete">`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
let selected = 0;
|
let selected = 0;
|
||||||
|
|
||||||
if (document.getElementById('delete-form') != null) {
|
document.querySelectorAll(".delete-form").forEach(form => {
|
||||||
document.getElementById('delete-form').addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
|
if (!confirm('Are you sure you want to delete this item?')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.getElementById('multi-delete-form') != null) {
|
||||||
|
document.getElementById('multi-delete-form').addEventListener('submit', function(e) {
|
||||||
if (selected == 0) {
|
if (selected == 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('No items selected');
|
alert('No items selected');
|
||||||
|
@ -5,9 +5,18 @@
|
|||||||
<tr th:fragment="data_row(row, selectable)" class="table-data-row">
|
<tr th:fragment="data_row(row, selectable)" class="table-data-row">
|
||||||
<td th:if=${selectable} class="table-checkbox">
|
<td th:if=${selectable} class="table-checkbox">
|
||||||
<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="multi-delete-form">
|
||||||
</td>
|
</td>
|
||||||
<td th:each="field : ${schema.getSortedFields()}">
|
<td class="text-center row-icons">
|
||||||
|
<a class="ps-1" th:href="|/dbadmin/model/${schema.getJavaClass().getName()}/edit/${row.getPrimaryKeyValue()}|">
|
||||||
|
<i class="bi bi-pencil-square"></i></a>
|
||||||
|
<form class="delete-form" method="POST"
|
||||||
|
th:action="|/dbadmin/model/${schema.getJavaClass().getName()}/delete/${row.getPrimaryKeyValue()}|">
|
||||||
|
<button><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<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>
|
||||||
@ -19,21 +28,12 @@
|
|||||||
<td th:each="colName : ${schema.getComputedColumnNames()}">
|
<td th:each="colName : ${schema.getComputedColumnNames()}">
|
||||||
<span th:text="${row.compute(colName)}"></span>
|
<span th:text="${row.compute(colName)}"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-center row-icons" th:if="${selectable}">
|
|
||||||
<a class="ps-1" th:href="|/dbadmin/model/${schema.getJavaClass().getName()}/edit/${row.getPrimaryKeyValue()}|">
|
|
||||||
<i class="bi bi-pencil-square"></i></a>
|
|
||||||
<form class="delete-form" method="POST"
|
|
||||||
th:action="|/dbadmin/model/${schema.getJavaClass().getName()}/delete/${row.getPrimaryKeyValue()}|">
|
|
||||||
<button><i class="bi bi-trash"></i></button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
<!-- data-row-field fragment -->
|
<!-- data-row-field fragment -->
|
||||||
<th:block th:fragment="data_row_field(field, object)">
|
<th:block th:fragment="data_row_field(field, object)">
|
||||||
<th:block th:if="${field.getConnectedType() != null}">
|
<th:block th:if="${field.getConnectedType() != null && object.traverse(field) != null}">
|
||||||
<a th:href="|/dbadmin/model/${field.getConnectedType().getName()}/show/${object.traverse(field).getPrimaryKeyValue()}|">
|
<a th:href="|/dbadmin/model/${field.getConnectedType().getName()}/show/${object.traverse(field).getPrimaryKeyValue()}|">
|
||||||
<span th:text="${object.has(field) ? object.traverse(field).getPrimaryKeyValue() : 'NULL'}"></span>
|
<span th:text="${object.has(field) ? object.traverse(field).getPrimaryKeyValue() : 'NULL'}"></span>
|
||||||
</a>
|
</a>
|
||||||
@ -61,12 +61,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.getName()}/${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>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<script type="text/javascript" src="/js/autocomplete.js"></script>
|
<script type="text/javascript" src="/js/autocomplete.js"></script>
|
||||||
<script type="text/javascript" src="/js/autocomplete-multi.js"></script>
|
<script type="text/javascript" src="/js/autocomplete-multi.js"></script>
|
||||||
<script type="text/javascript" src="/js/filters.js"></script>
|
<script type="text/javascript" src="/js/filters.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/create.js"></script>
|
||||||
<title th:text="${title != null ? title + ' | Spring Boot DB Admin Panel' : 'Spring Boot DB Admin Panel'}"></title>
|
<title th:text="${title != null ? title + ' | Spring Boot DB Admin Panel' : 'Spring Boot DB Admin Panel'}"></title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<div th:if="${results != null && results.size() > 0}">
|
<div th:if="${results != null && results.size() > 0}">
|
||||||
<table class="table table-striped align-middle mt-3">
|
<table class="table table-striped align-middle mt-3">
|
||||||
<tr class="table-data-row">
|
<tr class="table-data-row">
|
||||||
|
<th class="row-icons"></th>
|
||||||
<th th:each="field : ${schema.getSortedFields()}">
|
<th th:each="field : ${schema.getSortedFields()}">
|
||||||
<div class="m-0 p-0 d-flex justify-content-between">
|
<div class="m-0 p-0 d-flex justify-content-between">
|
||||||
<div class="column-title">
|
<div class="column-title">
|
||||||
|
@ -2,18 +2,19 @@
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
|
||||||
<head></head>
|
<head></head>
|
||||||
<body>
|
<body>
|
||||||
<div class="table-selectable" th:fragment="table(results, schema)">
|
<div class="table-selectable table-responsive" th:fragment="table(results, schema)">
|
||||||
<div th:if="${results.isEmpty()}">
|
<div th:if="${results.isEmpty()}">
|
||||||
<p>This table contains no data.</p>
|
<p>This table contains no data.</p>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${results.size() > 0}">
|
<div th:if="${results.size() > 0}">
|
||||||
<form id="delete-form" th:action="|/dbadmin/model/${schema.getClassName()}/delete|" method="POST">
|
<form id="multi-delete-form" th:action="|/dbadmin/model/${schema.getClassName()}/delete|" method="POST">
|
||||||
</form>
|
</form>
|
||||||
<nav th:replace="~{fragments/resources :: pagination(${page})}">
|
<nav th:replace="~{fragments/resources :: pagination(${page})}">
|
||||||
</nav>
|
</nav>
|
||||||
<table class="table table-striped align-middle mt-3">
|
<table class="table table-striped align-middle mt-3">
|
||||||
<tr class="table-data-row">
|
<tr class="table-data-row">
|
||||||
<th class="table-checkbox"><input type="checkbox" class="form-check-input check-all"></th>
|
<th class="table-checkbox"><input type="checkbox" class="form-check-input check-all"></th>
|
||||||
|
<th></th>
|
||||||
<th class="table-data-row" th:each="field : ${schema.getSortedFields()}">
|
<th class="table-data-row" th:each="field : ${schema.getSortedFields()}">
|
||||||
<div class="m-0 p-0 d-flex justify-content-between">
|
<div class="m-0 p-0 d-flex justify-content-between">
|
||||||
<div class="column-title">
|
<div class="column-title">
|
||||||
@ -56,7 +57,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="m-0 p-0 dbfieldtype"><small>COMPUTED</small></p>
|
<p class="m-0 p-0 dbfieldtype"><small>COMPUTED</small></p>
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<th:block th:each="r : ${results}">
|
<th:block th:each="r : ${results}">
|
||||||
<tr th:replace="~{fragments/data_row :: data_row(row=${r},selectable=${true})}"></tr>
|
<tr th:replace="~{fragments/data_row :: data_row(row=${r},selectable=${true})}"></tr>
|
||||||
|
@ -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=${
|
||||||
@ -39,15 +41,14 @@
|
|||||||
: (object != null ? object.traverse(field).getPrimaryKeyValue() : '' )
|
: (object != null ? object.traverse(field).getPrimaryKeyValue() : '' )
|
||||||
})}">
|
})}">
|
||||||
</div>
|
</div>
|
||||||
<!-- <input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|"> -->
|
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:unless="${field.isForeignKey()}">
|
<th:block th:unless="${field.isForeignKey()}">
|
||||||
<input 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()}"
|
||||||
th:value="
|
th:value="
|
||||||
${create ? (params != null ? params.getOrDefault(field.getName(), '') : '')
|
${create ? (params != null ? params.getOrDefault(field.getName(), '') : '')
|
||||||
: (object != null ? object.get(field).getValue() : '' )}
|
: (object != null ? object.get(field).getValue() : '' )}
|
||||||
|
|
||||||
"
|
"
|
||||||
class="form-control" th:id="|__id_${field.getName()}|"
|
class="form-control" th:id="|__id_${field.getName()}|"
|
||||||
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
||||||
@ -55,8 +56,33 @@
|
|||||||
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('')">
|
||||||
<!-- <input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|"> -->
|
|
||||||
|
<!--/*--> Binary field <!--*/-->
|
||||||
|
<th:block th:if="${field.isBinary()}">
|
||||||
|
<!--/*--> Edit options <!--*/-->
|
||||||
|
<div th:if="${!create && object.get(field).getValue() != null}">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="binary-field-checkbox"
|
||||||
|
th:data-fieldname="${field.getName()}"
|
||||||
|
th:id="|__keep_${field.getName()}|"
|
||||||
|
checked
|
||||||
|
th:name="|__keep_${field.getName()}|">
|
||||||
|
<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>
|
||||||
|
<!--/*--> File input <!--*/-->
|
||||||
|
<input th:if="${field.isBinary()}" placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||||
|
th:name="${field.getName()}"
|
||||||
|
class="form-control mt-2" th:id="|__id_${field.getName()}|"
|
||||||
|
th:required="${!field.isNullable()}"
|
||||||
|
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||||
|
oninput="this.setCustomValidity('')">
|
||||||
</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">
|
||||||
|
@ -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