feat: implement menu item management, update collection type to LinkedHashSet, and increase multipart upload limits

This commit is contained in:
2026-07-10 13:40:13 +07:00
parent 479d794b8e
commit 3e5432ba65
8 changed files with 216 additions and 191 deletions
Binary file not shown.
@@ -2,6 +2,7 @@ package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Menu; import com.sisvietnamvn.web.domain.Menu;
import com.sisvietnamvn.web.domain.MenuItem; import com.sisvietnamvn.web.domain.MenuItem;
import com.sisvietnamvn.web.repository.MenuItemRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants; import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.MenuService; import com.sisvietnamvn.web.service.MenuService;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -19,9 +20,11 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
public class ManageMenuController { public class ManageMenuController {
private final MenuService menuService; private final MenuService menuService;
private final MenuItemRepository menuItemRepository;
public ManageMenuController(MenuService menuService) { public ManageMenuController(MenuService menuService, MenuItemRepository menuItemRepository) {
this.menuService = menuService; this.menuService = menuService;
this.menuItemRepository = menuItemRepository;
} }
@GetMapping @GetMapping
@@ -87,4 +90,32 @@ public class ManageMenuController {
redirectAttributes.addFlashAttribute("successMessage", "Menu properties updated successfully."); redirectAttributes.addFlashAttribute("successMessage", "Menu properties updated successfully.");
return "redirect:/manage/menus/" + id + "/edit"; return "redirect:/manage/menus/" + id + "/edit";
} }
@PostMapping("/{id}/items/{itemId}/update")
public String updateMenuItem(@PathVariable Long id, @PathVariable Long itemId, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) Integer displayOrder, RedirectAttributes redirectAttributes) {
MenuItem item = menuItemRepository.findById(itemId).orElseThrow(() -> new IllegalArgumentException("Invalid item Id"));
item.setLabel(label);
item.setUrl(url);
if (displayOrder != null) {
item.setDisplayOrder(displayOrder);
}
if (parentId != null) {
MenuItem parentItem = menuItemRepository.findById(parentId).orElse(null);
item.setParent(parentItem);
} else {
item.setParent(null);
}
menuItemRepository.save(item);
redirectAttributes.addFlashAttribute("successMessage", "Menu item updated.");
return "redirect:/manage/menus/" + id + "/edit";
}
@PostMapping("/{id}/items/{itemId}/delete")
public String deleteMenuItem(@PathVariable Long id, @PathVariable Long itemId, RedirectAttributes redirectAttributes) {
menuItemRepository.deleteById(itemId);
redirectAttributes.addFlashAttribute("successMessage", "Menu item deleted.");
return "redirect:/manage/menus/" + id + "/edit";
}
} }
@@ -4,8 +4,6 @@ import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.io.Serial; import java.io.Serial;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
@@ -35,7 +33,7 @@ public class Menu extends AbstractAuditingEntity<Long> {
@OneToMany(mappedBy = "menu", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "menu", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("displayOrder ASC") @OrderBy("displayOrder ASC")
private List<MenuItem> items = new ArrayList<>(); private java.util.Set<MenuItem> items = new java.util.LinkedHashSet<>();
@Override @Override
public Long getId() { public Long getId() {
@@ -62,11 +60,11 @@ public class Menu extends AbstractAuditingEntity<Long> {
this.location = location; this.location = location;
} }
public List<MenuItem> getItems() { public java.util.Set<MenuItem> getItems() {
return items; return items;
} }
public void setItems(List<MenuItem> items) { public void setItems(java.util.Set<MenuItem> items) {
this.items = items; this.items = items;
} }
@@ -58,6 +58,9 @@ server:
port: 8080 port: 8080
# make sure requests the proxy uri instead of the server one # make sure requests the proxy uri instead of the server one
forward-headers-strategy: native forward-headers-strategy: native
tomcat:
max-swallow-size: -1
max-http-form-post-size: -1
# =================================================================== # ===================================================================
# JHipster specific properties # JHipster specific properties
@@ -91,6 +91,10 @@ management:
application: ${spring.application.name} application: ${spring.application.name}
spring: spring:
servlet:
multipart:
max-file-size: 5000MB
max-request-size: 5000MB
datasource: datasource:
type: com.zaxxer.hikari.HikariDataSource type: com.zaxxer.hikari.HikariDataSource
url: jdbc:oracle:thin:@localhost:1521/sisvietnam url: jdbc:oracle:thin:@localhost:1521/sisvietnam
@@ -80,23 +80,96 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center" th:each="item : ${menu.items}" th:style="${item.parent != null ? 'margin-left: 2rem; border-left: 4px solid #4e73df;' : ''}"> <th:block th:each="topItem : ${menu.items}" th:if="${topItem.parent == null}">
<!-- Top Level Item -->
<li class="list-group-item d-flex justify-content-between align-items-center">
<div> <div>
<strong th:text="${item.label}">Link Text</strong> <strong th:text="${topItem.label}">Link Text</strong>
<span class="badge badge-secondary ml-2" th:if="${item.parent != null}" th:text="'Child of: ' + ${item.parent.label}">Child</span>
<br> <br>
<small class="text-muted" th:text="${item.url}">URL</small> <small class="text-muted" th:text="${topItem.url}">URL</small>
</div>
<div>
<span class="badge badge-primary badge-pill mr-2" th:text="'Order: ' + ${topItem.displayOrder}">0</span>
<button class="btn btn-sm btn-info" data-toggle="modal" th:data-target="'#editModal-' + ${topItem.id}">Edit</button>
<form th:action="@{/manage/menus/{menuId}/items/{itemId}/delete(menuId=${menu.id}, itemId=${topItem.id})}" method="post" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this menu item?');">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</div> </div>
<!-- Future enhancement: reorder buttons and delete button -->
<span class="badge badge-primary badge-pill" th:text="'Order: ' + ${item.displayOrder}">0</span>
</li> </li>
<!-- Children Items -->
<th:block th:each="child : ${menu.items}" th:if="${child.parent != null and child.parent.id == topItem.id}">
<li class="list-group-item d-flex justify-content-between align-items-center bg-light" style="margin-left: 2rem; border-left: 4px solid #4e73df;">
<div>
<strong th:text="${child.label}">Link Text</strong>
<span class="badge badge-secondary ml-2">Child</span>
<br>
<small class="text-muted" th:text="${child.url}">URL</small>
</div>
<div>
<span class="badge badge-primary badge-pill mr-2" th:text="'Order: ' + ${child.displayOrder}">0</span>
<button class="btn btn-sm btn-info" data-toggle="modal" th:data-target="'#editModal-' + ${child.id}">Edit</button>
<form th:action="@{/manage/menus/{menuId}/items/{itemId}/delete(menuId=${menu.id}, itemId=${child.id})}" method="post" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this menu item?');">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</div>
</li>
</th:block>
</th:block>
</ul> </ul>
<div th:if="${#lists.isEmpty(menu.items)}" class="alert alert-info mt-3"> <div th:if="${#lists.isEmpty(menu.items)}" class="alert alert-info mt-3">
No items in this menu yet. No items in this menu yet.
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Edit Modals -->
<th:block th:each="item : ${menu.items}">
<div class="modal fade" th:id="'editModal-' + ${item.id}" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form th:action="@{/manage/menus/{menuId}/items/{itemId}/update(menuId=${menu.id}, itemId=${item.id})}" method="post">
<div class="modal-header">
<h5 class="modal-title">Edit Menu Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Link Text</label>
<input type="text" class="form-control" name="label" th:value="${item.label}" required>
</div>
<div class="form-group">
<label>URL</label>
<input type="text" class="form-control" name="url" th:value="${item.url}" required>
</div>
<div class="form-group">
<label>Parent Item (Optional)</label>
<select class="form-control" name="parentId">
<option value="">-- None (Top Level) --</option>
<th:block th:each="parentOpt : ${menu.items}" th:if="${parentOpt.parent == null and parentOpt.id != item.id}">
<option th:value="${parentOpt.id}" th:text="${parentOpt.label}" th:selected="${item.parent != null and item.parent.id == parentOpt.id}"></option>
</th:block>
</select>
</div>
<div class="form-group">
<label>Display Order</label>
<input type="number" class="form-control" name="displayOrder" th:value="${item.displayOrder}">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</th:block>
</div> </div>
</div> </div>
</body> </body>
@@ -1,44 +1,17 @@
<div class="content-top"> <div class="content-top">
<div class="lc--layout-container lc--full"> <div class="lc--layout-container lc--full">
<div class="l--layout l--full"> <div class="l--layout l--full">
<div class="lr--layout-region lr--main"> <div class="lr--layout-region lr--main">
<div class="cc--component-container cc--homepage-hero "> <div class="cc--component-container cc--homepage-hero ">
<div class="c--component c--homepage-hero"> <div class="c--component c--homepage-hero">
<div class="slides-container"> <div class="slides-container">
<div class="image-video-container has-video"> <div class="image-video-container has-video">
<img src="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg" alt="Students collaborate using a driving simulator in the UMass Center for Transportation." srcset="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg?h=9855f42d&amp;itok=d27r8GaT 1920w"> <img src="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg" alt="Students collaborate using a driving simulator in the UMass Center for Transportation." srcset="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg?h=9855f42d&amp;itok=d27r8GaT 1920w">
<div class="f--ambient-video"> <div class="f--ambient-video">
<video role="presentation" tabindex="-1" loop="" autoplay="" playsinline="" muted=""> <video role="presentation" tabindex="-1" loop="" autoplay="" playsinline="" muted="">
<source src="https://api-files.sproutvideo.com/file/aa9ad8b61e11ebc420/de09e4945c49091f/1080.mp4" type="video/mp4"></video> <source src="http://localhost:8080/uploads/2026/07/9375f45b-027a-4fb4-bd18-66889309280d.mp4" type="video/mp4">
</video>
</div> </div>
<div class="video-controls" data-once="ambientVideo"> <div class="video-controls" data-once="ambientVideo">
<div class="video-controls-inner"> <div class="video-controls-inner">
<button aria-labelledby="pauseBtn" class="video-button video-pause-button"> <button aria-labelledby="pauseBtn" class="video-button video-pause-button">
@@ -55,130 +28,73 @@
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="text-container centered"> <div class="text-container centered">
<div class="slide-text-container-inner"> <div class="slide-text-container-inner">
<h3> <h3>
<a href="https://www.umass.edu/gateway/why-umass">Be the Future You Want to See.</a> <a href="https://www.umass.edu/gateway/why-umass">Be the Future You Want to See.</a>
</h3> </h3>
<div class="f--field f--button"> <div class="f--field f--button">
<a href="https://www.umass.edu/gateway/why-umass" class="button-secondary button-context-dark " aria-label="Learn More Learn more about UMass Amherst." data-component-id="umass_base:button"> <a href="https://www.umass.edu/gateway/why-umass" class="button-secondary button-context-dark " aria-label="Learn More Learn more about UMass Amherst." data-component-id="umass_base:button">
<span class="button-text"> Learn More </span>
<span class="button-text">
Learn More
</span>
</a> </a>
</div> </div>
<nav class="mc--menu mc--info-menu"> <nav class="mc--menu mc--info-menu">
<ul class="menu m--menu m--info-menu"> <ul class="menu m--menu m--info-menu">
<li class="menu-item menu-item--expanded"> <li class="menu-item menu-item--expanded">
<details class="utility-button-wrapper mc--info-menu-container"> <details class="utility-button-wrapper mc--info-menu-container">
<summary type="button" class="utility-button arrow-toggle information-for-toggle" aria-label="Display submenu for Information menu" aria-expanded="false" aria-haspopup="true"> <summary type="button" class="utility-button arrow-toggle information-for-toggle" aria-label="Display submenu for Information menu" aria-expanded="false" aria-haspopup="true"> Info For <svg version="1.1" class="arrow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 7" enable-background="new 0 0 16 7" xml:space="preserve" width="16" height="7">
Info For
<svg version="1.1" class="arrow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 7" enable-background="new 0 0 16 7" xml:space="preserve" width="16" height="7">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff" points="7,7 0,0 16,0 "></polygon> <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff" points="7,7 0,0 16,0 "></polygon>
</svg> </svg>
</summary> </summary>
<div class="submenu-wrapper"> <div class="submenu-wrapper">
<ul class="submenu"> <ul class="submenu">
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/prospective-students">Prospective Students</a> <a href="https://www.umass.edu/gateway/info/prospective-students">Prospective Students</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/current-students">Current Students</a> <a href="https://www.umass.edu/gateway/info/current-students">Current Students</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/parents-and-families">Parents &amp; Family</a> <a href="https://www.umass.edu/gateway/info/parents-and-families">Parents &amp; Family</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty &amp; Staff</a> <a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty &amp; Staff</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umassalumni.com/">Alumni</a> <a href="https://www.umassalumni.com/">Alumni</a>
</li> </li>
</ul> </ul>
</div> </div>
</details> </details>
<div class="submenu-wrapper-desktop"> <div class="submenu-wrapper-desktop">
<ul class="submenu"> <ul class="submenu">
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/prospective-students">Prospective Students</a> <a href="https://www.umass.edu/gateway/info/prospective-students">Prospective Students</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/current-students">Current Students</a> <a href="https://www.umass.edu/gateway/info/current-students">Current Students</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/parents-and-families">Parents &amp; Family</a> <a href="https://www.umass.edu/gateway/info/parents-and-families">Parents &amp; Family</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty &amp; Staff</a> <a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty &amp; Staff</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="https://www.umassalumni.com/">Alumni</a> <a href="https://www.umassalumni.com/">Alumni</a>
</li> </li>
</ul> </ul>
</div> </div>
</li> </li>
</ul> </ul>
</nav> </nav>
</div> </div>
</div> </div>
</div>
</div></div>
</div>
</div> </div>
</div> </div>
</div>
</div>
</div>
</div>
</div> </div>