Can create, edit, view, and delete assets

This commit is contained in:
2025-06-08 14:20:29 +02:00
parent 72f4a1cd28
commit afd36bcb0c
11 changed files with 223 additions and 34 deletions

View File

@@ -2,6 +2,7 @@ package be.seeseepuff.pcinv.controllers;
import be.seeseepuff.pcinv.services.AssetService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.repository.query.Param;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@@ -26,8 +27,14 @@ public class WebController {
private static final String DESCRIPTOR = "descriptor";
/// The name of the model attribute that holds the list of assets.
private static final String ASSETS = "assets";
/// The name of the model attribute that holds the asset being viewed or edited.
private static final String ASSET = "asset";
/// The name of the model attribute that holds a list of all properties of all descriptors.
private static final String PROPERTIES = "properties";
/// The name of the model attribute that holds the action to be performed.
private static final String ACTION = "action";
/// The name of the model attribute that holds the current time in milliseconds.
private static final String TIME = "time";
private final AssetService assetService;
@@ -36,6 +43,7 @@ public class WebController {
*/
@GetMapping("/")
public String index(Model model) {
model.addAttribute(TIME, System.currentTimeMillis());
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
model.addAttribute("asset_count", assetService.countAssets());
return "index";
@@ -46,6 +54,7 @@ public class WebController {
*/
@GetMapping("/browse")
public String browse(Model model) {
model.addAttribute(TIME, System.currentTimeMillis());
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
return "browse";
}
@@ -60,6 +69,7 @@ public class WebController {
*/
@GetMapping("/browse/{type}")
public String browseType(Model model, @PathVariable String type) {
model.addAttribute(TIME, System.currentTimeMillis());
var tree = assetService.getAssetDescriptorTree(type);
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type));
model.addAttribute(DESCRIPTORS, tree);
@@ -76,20 +86,62 @@ public class WebController {
*/
@GetMapping("/view/{qr}")
public String view(Model model, @PathVariable long qr) {
model.addAttribute(TIME, System.currentTimeMillis());
model.addAttribute(ACTION, "view");
return renderView(model, qr);
}
/**
* Show a page asking if the user wants to delete an asset.
*
* @param qr The QR code of the asset to delete.
*/
@GetMapping("/delete/{qr}")
public String delete(Model model, @PathVariable long qr, @Param("confirm") boolean confirm) {
model.addAttribute(TIME, System.currentTimeMillis());
model.addAttribute(ACTION, "delete");
if (confirm) {
var asset = assetService.getAssetByQr(qr);
if (asset == null) {
return "redirect:/";
}
var type = asset.getAsset().getType();
assetService.deleteAsset(qr);
return "redirect:/browse/" + type;
}
return renderView(model, qr);
}
/**
* Renders the view for an asset based on its QR code.
* If the asset does not exist, it redirects to the index page.
*
* @param model The model to add attributes to.
* @param qr The QR code of the asset to view.
* @return The view name for viewing the asset.
*/
private String renderView(Model model, long qr) {
var asset = assetService.getAssetByQr(qr);
if (asset == null) {
return "redirect:/";
}
model.addAttribute("asset", asset);
model.addAttribute(ASSET, asset);
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(asset.getAsset().getType()));
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(asset.getAsset().getType()));
return "view";
}
@GetMapping("/search")
public String search(@Param("qr") long qr) {
return "redirect:/view/" + qr;
}
/**
* Shows a view where the user can create the type of asset to create.
*/
@GetMapping("/create")
public String create(Model model) {
model.addAttribute(TIME, System.currentTimeMillis());
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
return "create_select";
}
@@ -101,11 +153,51 @@ public class WebController {
*/
@GetMapping("/create/{type}")
public String createType(Model model, @PathVariable String type) {
model.addAttribute(TIME, System.currentTimeMillis());
model.addAttribute(ACTION, "create");
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type));
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type));
return "create_asset";
}
/**
* Shows a view where the user can edit an existing asset.
*
* @param qr The QR code of the asset to edit.
*/
@GetMapping("/edit/{qr}")
public String edit(Model model, @PathVariable long qr) {
model.addAttribute(TIME, System.currentTimeMillis());
var asset = assetService.getAssetByQr(qr);
if (asset == null) {
throw new RuntimeException("Asset not found");
}
String assetType = asset.getAsset().getType();
model.addAttribute(ACTION, "edit");
model.addAttribute(ASSET, asset);
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(assetType));
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(assetType));
return "create_asset";
}
/**
* Actually edits an asset based on the form data submitted.
*
* @param qr The QR code of the asset to edit.
*/
@PostMapping("/edit/{qr}")
public String editPost(Model model, @PathVariable long qr, @RequestBody MultiValueMap<String, String> formData) {
model.addAttribute(TIME, System.currentTimeMillis());
var formMap = new HashMap<String, String>();
formData.forEach((key, values) -> {
if (!values.isEmpty()) {
formMap.put(key, values.getFirst());
}
});
var asset = assetService.editAsset(qr, formMap);
return "redirect:/view/" + asset.getQr();
}
/**
* Handles the form submission for creating an asset.
*
@@ -118,6 +210,7 @@ public class WebController {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public String createTypePost(Model model, @PathVariable String type, @RequestBody MultiValueMap<String, String> formData) {
model.addAttribute(TIME, System.currentTimeMillis());
var formMap = new HashMap<String, String>();
formData.forEach((key, values) -> {
if (!values.isEmpty()) {

View File

@@ -41,6 +41,8 @@ public class AssetProperty {
private final BiConsumer<Object, Object> setter;
/// A getter function that can be used to get the value of the property from an asset.
private final Function<Object, Object> getter;
/// Whether the property is an input list.
private final boolean inputList;
/**
* Enum representing the possible types of asset properties.
@@ -84,6 +86,7 @@ public class AssetProperty {
.displayName(annotation.value())
.type(type)
.required(annotation.required())
.inputList(property.isAnnotationPresent(InputList.class))
.setter((obj, value) -> {
try {
property.setAccessible(true);
@@ -167,7 +170,11 @@ public class AssetProperty {
* @param asset The asset to get the property value from.
* @return The value of the property.
*/
public Object getValue(Object asset) {
@Nullable
public Object getValue(@Nullable Object asset) {
if (asset == null) {
return null;
}
return getter.apply(asset);
}
@@ -176,7 +183,11 @@ public class AssetProperty {
*
* @return The rendered value as a string.
*/
public String renderValue(Object asset) {
@Nullable
public String renderValue(@Nullable Object asset) {
if (asset == null) {
return null;
}
var value = getValue(asset);
if (value == null) {
return "Unknown";

View File

@@ -0,0 +1,15 @@
package be.seeseepuff.pcinv.meta;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that any form for the given field should be rendered as a dropdown
* list with optional manual text input.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InputList {
}

View File

@@ -1,11 +1,9 @@
package be.seeseepuff.pcinv.models;
import be.seeseepuff.pcinv.meta.AssetInfo;
import be.seeseepuff.pcinv.meta.InputList;
import be.seeseepuff.pcinv.meta.Property;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@@ -21,7 +19,10 @@ import lombok.Setter;
type = "asset",
isVisible = false
)
@Table(name = "assets")
@Table(
name = "assets",
uniqueConstraints = @UniqueConstraint(columnNames = "qr")
)
public class GenericAsset
{
public static final String TYPE = "asset";
@@ -38,6 +39,7 @@ public class GenericAsset
/// The brand of the asset.
@Property("Brand")
@InputList
private String brand;
/// The model of the asset

View File

@@ -21,9 +21,11 @@ public interface AssetRepository<T extends Asset> {
T findByAsset(GenericAsset asset);
void deleteByAsset(GenericAsset asset);
void flush();
List<T> findAll();
Class<T> getAssetType();
long count();
}

View File

@@ -125,6 +125,31 @@ public class AssetService {
return asset;
}
/**
* Edits an existing asset with the provided form data.
*
* @param qr The QR code of the asset to edit.
* @param formData The form data containing the updated asset properties.
* @return The updated asset.
*/
@Transactional
public Asset editAsset(long qr, Map<String, String> formData) {
var genericAsset = genericRepository.findByQr(qr);
if (genericAsset == null) {
throw new IllegalArgumentException("No asset found with QR code: " + qr);
}
var assetType = genericAsset.getType();
var assetDescriptor = getAssetDescriptor(assetType);
var asset = getRepositoryFor(assetType).findByAsset(genericAsset);
fillIn(genericAsset, getAssetDescriptor(GenericAsset.TYPE), formData);
fillIn(asset, assetDescriptor, formData);
genericRepository.saveAndFlush(genericAsset);
return getRepositoryFor(assetType).saveAndFlushAsset(asset);
}
/**
* Gets the asset repository for the specified type.
*
@@ -148,11 +173,10 @@ public class AssetService {
private void fillIn(Object asset, AssetDescriptor assetDescriptor, Map<String, String> formData) {
for (var property : assetDescriptor.getProperties()) {
var value = parseValue(assetDescriptor, property, formData);
if (value != null) {
property.setValue(asset, value);
} else if (property.isRequired()) {
if (value == null && property.isRequired()) {
throw new IllegalArgumentException("Property '" + property.getName() + "' is required but not provided.");
}
property.setValue(asset, value);
}
}
@@ -199,4 +223,18 @@ public class AssetService {
throw new IllegalArgumentException("Unsupported property type: " + property.getType());
}
}
@Transactional
public void deleteAsset(long qr) {
var genericAsset = genericRepository.findByQr(qr);
if (genericAsset == null) {
throw new IllegalArgumentException("No asset found with QR code: " + qr);
}
var assetType = genericAsset.getType();
var assetRepository = getRepositoryFor(assetType);
assetRepository.deleteByAsset(genericAsset);
assetRepository.flush();
genericRepository.delete(genericAsset);
genericRepository.flush();
}
}