From 302785b60b81a48dfc6ec6900a1f74711bbabbf7 Mon Sep 17 00:00:00 2001
From: Victor Phan
Date: Thu, 2 Jul 2026 18:48:00 +0700
Subject: [PATCH] =?UTF-8?q?b=E1=BB=95=20sung=20th=C3=AAm=20c=C3=A1c=20ch?=
=?UTF-8?q?=E1=BB=A9c=20n=C4=83ng=20c=C3=B2n=20thi=E1=BA=BFu=20c=E1=BB=A7a?=
=?UTF-8?q?=20appearance?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/GlobalControllerAdvice.java | 48 +++++++-
.../manage/ManageCustomizeController.java | 44 +++++++
.../manage/ManageMenuController.java | 72 +++++++++++
.../manage/ManageThemeController.java | 68 +++++++++++
.../manage/ManageThemeEditorController.java | 66 ++++++++++
.../manage/ManageWidgetController.java | 85 +++++++++++++
.../com/sisvietnamvn/web/domain/Menu.java | 85 +++++++++++++
.../com/sisvietnamvn/web/domain/MenuItem.java | 106 ++++++++++++++++
.../web/repository/MenuItemRepository.java | 12 ++
.../web/repository/MenuRepository.java | 15 +++
.../sisvietnamvn/web/service/MenuService.java | 43 +++++++
.../web/service/dto/WidgetDto.java | 40 ++++++
.../20260702184000_add_menu_entity.xml | 57 +++++++++
.../resources/config/liquibase/master.xml | 1 +
.../templates/fragments/manage-layout.html | 4 +
.../templates/manage/customize/index.html | 60 +++++++++
.../templates/manage/menus/edit.html | 70 +++++++++++
.../templates/manage/menus/list.html | 85 +++++++++++++
.../templates/manage/themes/editor.html | 62 ++++++++++
.../templates/manage/themes/list.html | 56 ++++++---
.../templates/manage/widgets/list.html | 115 ++++++++++++++++++
.../src/main/resources/templates/page.html | 12 ++
.../templates/themes/default/footer.html | 22 +++-
.../templates/themes/default/header.html | 8 +-
.../templates/themes/default/layout.html | 15 ++-
.../templates/themes/modern/footer.html | 20 ++-
.../templates/themes/modern/header.html | 7 +-
.../templates/themes/modern/layout.html | 16 ++-
28 files changed, 1265 insertions(+), 29 deletions(-)
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageCustomizeController.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeEditorController.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageWidgetController.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Menu.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MenuItem.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/MenuItemRepository.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/MenuRepository.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/MenuService.java
create mode 100644 sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/dto/WidgetDto.java
create mode 100644 sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702184000_add_menu_entity.xml
create mode 100644 sisvietnamvn_main/src/main/resources/templates/manage/customize/index.html
create mode 100644 sisvietnamvn_main/src/main/resources/templates/manage/menus/edit.html
create mode 100644 sisvietnamvn_main/src/main/resources/templates/manage/menus/list.html
create mode 100644 sisvietnamvn_main/src/main/resources/templates/manage/themes/editor.html
create mode 100644 sisvietnamvn_main/src/main/resources/templates/manage/widgets/list.html
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/GlobalControllerAdvice.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/GlobalControllerAdvice.java
index 9025e604..5869334c 100644
--- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/GlobalControllerAdvice.java
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/GlobalControllerAdvice.java
@@ -15,10 +15,12 @@ import java.util.Map;
public class GlobalControllerAdvice {
private final SettingService settingService;
+ private final com.sisvietnamvn.web.service.MenuService menuService;
private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
- public GlobalControllerAdvice(SettingService settingService) {
+ public GlobalControllerAdvice(SettingService settingService, com.sisvietnamvn.web.service.MenuService menuService) {
this.settingService = settingService;
+ this.menuService = menuService;
}
/**
@@ -41,4 +43,48 @@ public class GlobalControllerAdvice {
return activeTheme;
}
+
+ @ModelAttribute("primaryMenu")
+ public com.sisvietnamvn.web.domain.Menu getPrimaryMenu() {
+ return menuService.findByLocation("PRIMARY").orElse(null);
+ }
+
+ @ModelAttribute("footerMenu")
+ public com.sisvietnamvn.web.domain.Menu getFooterMenu() {
+ return menuService.findByLocation("FOOTER").orElse(null);
+ }
+
+ @ModelAttribute("sidebarWidgets")
+ public java.util.List getSidebarWidgets() {
+ return getWidgets("sidebar");
+ }
+
+ @ModelAttribute("footerWidgets")
+ public java.util.List getFooterWidgets() {
+ return getWidgets("footer");
+ }
+
+ @ModelAttribute("themeModPrimaryColor")
+ public String getThemeModPrimaryColor() {
+ return settingService.getValue("theme_mod_primaryColor", "#007bff");
+ }
+
+ @ModelAttribute("themeModFontFamily")
+ public String getThemeModFontFamily() {
+ return settingService.getValue("theme_mod_fontFamily", "sans-serif");
+ }
+
+ @ModelAttribute("themeModCustomCss")
+ public String getThemeModCustomCss() {
+ return settingService.getValue("theme_mod_customCss", "");
+ }
+
+ private java.util.List getWidgets(String area) {
+ String json = settingService.getValue("theme_widgets_" + area, "[]");
+ try {
+ return new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference>() {});
+ } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
+ return new java.util.ArrayList<>();
+ }
+ }
}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageCustomizeController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageCustomizeController.java
new file mode 100644
index 00000000..e6ddf3cf
--- /dev/null
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageCustomizeController.java
@@ -0,0 +1,44 @@
+package com.sisvietnamvn.web.controller.manage;
+
+import com.sisvietnamvn.web.security.AuthoritiesConstants;
+import com.sisvietnamvn.web.service.SettingService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+/**
+ * Controller for the Theme Customizer in the Admin panel.
+ */
+@Controller
+@RequestMapping("/manage/customize")
+@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
+public class ManageCustomizeController {
+
+ private final SettingService settingService;
+
+ public ManageCustomizeController(SettingService settingService) {
+ this.settingService = settingService;
+ }
+
+ @GetMapping
+ public String customizerView(Model model) {
+ // Load current customizer settings
+ model.addAttribute("primaryColor", settingService.getValue("theme_mod_primaryColor", "#007bff"));
+ model.addAttribute("fontFamily", settingService.getValue("theme_mod_fontFamily", "sans-serif"));
+ model.addAttribute("customCss", settingService.getValue("theme_mod_customCss", ""));
+
+ return "manage/customize/index";
+ }
+
+ @PostMapping("/save")
+ public String saveCustomizer(@RequestParam String primaryColor, @RequestParam String fontFamily, @RequestParam String customCss, RedirectAttributes redirectAttributes) {
+ settingService.setValue("theme_mod_primaryColor", primaryColor);
+ settingService.setValue("theme_mod_fontFamily", fontFamily);
+ settingService.setValue("theme_mod_customCss", customCss);
+
+ redirectAttributes.addFlashAttribute("successMessage", "Customizations saved securely.");
+ return "redirect:/manage/customize";
+ }
+}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java
new file mode 100644
index 00000000..1a0c3a93
--- /dev/null
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java
@@ -0,0 +1,72 @@
+package com.sisvietnamvn.web.controller.manage;
+
+import com.sisvietnamvn.web.domain.Menu;
+import com.sisvietnamvn.web.domain.MenuItem;
+import com.sisvietnamvn.web.security.AuthoritiesConstants;
+import com.sisvietnamvn.web.service.MenuService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+/**
+ * Controller for managing Navigation Menus in the Admin panel.
+ */
+@Controller
+@RequestMapping("/manage/menus")
+@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
+public class ManageMenuController {
+
+ private final MenuService menuService;
+
+ public ManageMenuController(MenuService menuService) {
+ this.menuService = menuService;
+ }
+
+ @GetMapping
+ public String listMenus(Model model) {
+ model.addAttribute("menus", menuService.findAll());
+ return "manage/menus/list";
+ }
+
+ @PostMapping("/create")
+ public String createMenu(@RequestParam String name, @RequestParam(required = false) String location, RedirectAttributes redirectAttributes) {
+ Menu menu = new Menu();
+ menu.setName(name);
+ menu.setLocation(location);
+ menuService.save(menu);
+ redirectAttributes.addFlashAttribute("successMessage", "Menu created successfully.");
+ return "redirect:/manage/menus";
+ }
+
+ @GetMapping("/{id}/edit")
+ public String editMenu(@PathVariable Long id, Model model) {
+ Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
+ model.addAttribute("menu", menu);
+ return "manage/menus/edit";
+ }
+
+ @PostMapping("/{id}/items/add")
+ public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, RedirectAttributes redirectAttributes) {
+ Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
+ MenuItem item = new MenuItem();
+ item.setLabel(label);
+ item.setUrl(url);
+ item.setMenu(menu);
+ item.setDisplayOrder(menu.getItems().size());
+
+ menu.getItems().add(item);
+ menuService.save(menu);
+
+ redirectAttributes.addFlashAttribute("successMessage", "Menu item added.");
+ return "redirect:/manage/menus/" + id + "/edit";
+ }
+
+ @PostMapping("/{id}/delete")
+ public String deleteMenu(@PathVariable Long id, RedirectAttributes redirectAttributes) {
+ menuService.deleteById(id);
+ redirectAttributes.addFlashAttribute("successMessage", "Menu deleted successfully.");
+ return "redirect:/manage/menus";
+ }
+}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeController.java
index b96bb16f..f8151823 100644
--- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeController.java
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeController.java
@@ -95,4 +95,72 @@ public class ManageThemeController {
redirectAttributes.addFlashAttribute("successMessage", "New theme activated successfully.");
return "redirect:/manage/themes";
}
+
+ @PostMapping("/delete")
+ public String deleteTheme(@RequestParam("themeKey") String themeKey, RedirectAttributes redirectAttributes) {
+ String activeTheme = settingService.getValue("active_theme", "default");
+ if (themeKey.equals(activeTheme)) {
+ redirectAttributes.addFlashAttribute("errorMessage", "Cannot delete the active theme.");
+ return "redirect:/manage/themes";
+ }
+
+ try {
+ java.nio.file.Path themePath = java.nio.file.Paths.get("src/main/resources/templates/themes/", themeKey);
+ if (java.nio.file.Files.exists(themePath)) {
+ // Delete directory recursively
+ org.springframework.util.FileSystemUtils.deleteRecursively(themePath);
+ redirectAttributes.addFlashAttribute("successMessage", "Theme deleted successfully.");
+ } else {
+ redirectAttributes.addFlashAttribute("errorMessage", "Theme folder not found.");
+ }
+ } catch (java.io.IOException e) {
+ redirectAttributes.addFlashAttribute("errorMessage", "Error deleting theme: " + e.getMessage());
+ }
+ return "redirect:/manage/themes";
+ }
+
+ @PostMapping("/upload")
+ public String uploadTheme(@RequestParam("file") org.springframework.web.multipart.MultipartFile file, RedirectAttributes redirectAttributes) {
+ if (file.isEmpty() || !file.getOriginalFilename().endsWith(".zip")) {
+ redirectAttributes.addFlashAttribute("errorMessage", "Please select a valid .zip file.");
+ return "redirect:/manage/themes";
+ }
+
+ try {
+ java.nio.file.Path targetDir = java.nio.file.Paths.get("src/main/resources/templates/themes/");
+ java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(file.getInputStream());
+ java.util.zip.ZipEntry zipEntry = zis.getNextEntry();
+
+ while (zipEntry != null) {
+ java.nio.file.Path newPath = zipSlipProtect(zipEntry, targetDir);
+ if (zipEntry.isDirectory()) {
+ java.nio.file.Files.createDirectories(newPath);
+ } else {
+ if (newPath.getParent() != null) {
+ if (java.nio.file.Files.notExists(newPath.getParent())) {
+ java.nio.file.Files.createDirectories(newPath.getParent());
+ }
+ }
+ java.nio.file.Files.copy(zis, newPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ }
+ zipEntry = zis.getNextEntry();
+ }
+ zis.closeEntry();
+ zis.close();
+
+ redirectAttributes.addFlashAttribute("successMessage", "Theme uploaded and extracted successfully.");
+ } catch (Exception e) {
+ redirectAttributes.addFlashAttribute("errorMessage", "Failed to upload theme: " + e.getMessage());
+ }
+ return "redirect:/manage/themes";
+ }
+
+ private java.nio.file.Path zipSlipProtect(java.util.zip.ZipEntry zipEntry, java.nio.file.Path targetDir) throws java.io.IOException {
+ java.nio.file.Path targetDirResolved = targetDir.resolve(zipEntry.getName());
+ java.nio.file.Path normalizePath = targetDirResolved.normalize();
+ if (!normalizePath.startsWith(targetDir)) {
+ throw new java.io.IOException("Bad zip entry: " + zipEntry.getName());
+ }
+ return normalizePath;
+ }
}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeEditorController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeEditorController.java
new file mode 100644
index 00000000..11a19f25
--- /dev/null
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageThemeEditorController.java
@@ -0,0 +1,66 @@
+package com.sisvietnamvn.web.controller.manage;
+
+import com.sisvietnamvn.web.security.AuthoritiesConstants;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Controller for the Theme File Editor.
+ */
+@Controller
+@RequestMapping("/manage/theme-editor")
+@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
+public class ManageThemeEditorController {
+
+ private final Path themesBaseDir = Paths.get("src/main/resources/templates/themes").toAbsolutePath().normalize();
+
+ @GetMapping
+ public String editorView(@RequestParam(required = false) String file, Model model) {
+ try (Stream walk = Files.walk(themesBaseDir)) {
+ List fileTree = walk.filter(Files::isRegularFile)
+ .map(path -> themesBaseDir.relativize(path).toString())
+ .collect(Collectors.toList());
+ model.addAttribute("files", fileTree);
+
+ if (file != null && !file.isEmpty()) {
+ Path targetPath = themesBaseDir.resolve(file).normalize();
+ if (targetPath.startsWith(themesBaseDir) && Files.exists(targetPath)) {
+ String content = Files.readString(targetPath);
+ model.addAttribute("selectedFile", file);
+ model.addAttribute("fileContent", content);
+ } else {
+ model.addAttribute("errorMessage", "Invalid file path.");
+ }
+ }
+ } catch (IOException e) {
+ model.addAttribute("errorMessage", "Error loading theme files: " + e.getMessage());
+ }
+ return "manage/themes/editor";
+ }
+
+ @PostMapping("/save")
+ public String saveFile(@RequestParam("selectedFile") String file, @RequestParam("fileContent") String content, RedirectAttributes redirectAttributes) {
+ try {
+ Path targetPath = themesBaseDir.resolve(file).normalize();
+ if (targetPath.startsWith(themesBaseDir) && Files.exists(targetPath)) {
+ Files.writeString(targetPath, content, StandardOpenOption.TRUNCATE_EXISTING);
+ redirectAttributes.addFlashAttribute("successMessage", "File saved successfully.");
+ return "redirect:/manage/theme-editor?file=" + file;
+ } else {
+ redirectAttributes.addFlashAttribute("errorMessage", "Security violation: Invalid file path.");
+ }
+ } catch (IOException e) {
+ redirectAttributes.addFlashAttribute("errorMessage", "Error saving file: " + e.getMessage());
+ }
+ return "redirect:/manage/theme-editor";
+ }
+}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageWidgetController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageWidgetController.java
new file mode 100644
index 00000000..86b3b97a
--- /dev/null
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageWidgetController.java
@@ -0,0 +1,85 @@
+package com.sisvietnamvn.web.controller.manage;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sisvietnamvn.web.security.AuthoritiesConstants;
+import com.sisvietnamvn.web.service.SettingService;
+import com.sisvietnamvn.web.service.dto.WidgetDto;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Controller for managing Theme Widgets in the Admin panel.
+ */
+@Controller
+@RequestMapping("/manage/widgets")
+@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
+public class ManageWidgetController {
+
+ private final SettingService settingService;
+ private final ObjectMapper objectMapper;
+
+ public ManageWidgetController(SettingService settingService, ObjectMapper objectMapper) {
+ this.settingService = settingService;
+ this.objectMapper = objectMapper;
+ }
+
+ private List getWidgets(String area) {
+ String json = settingService.getValue("theme_widgets_" + area, "[]");
+ try {
+ return objectMapper.readValue(json, new TypeReference>() {});
+ } catch (JsonProcessingException e) {
+ return new ArrayList<>();
+ }
+ }
+
+ private void saveWidgets(String area, List widgets) {
+ try {
+ String json = objectMapper.writeValueAsString(widgets);
+ settingService.setValue("theme_widgets_" + area, json);
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @GetMapping
+ public String listWidgets(Model model) {
+ model.addAttribute("sidebarWidgets", getWidgets("sidebar"));
+ model.addAttribute("footerWidgets", getWidgets("footer"));
+ return "manage/widgets/list";
+ }
+
+ @PostMapping("/add")
+ public String addWidget(@RequestParam String area, @RequestParam String type, @RequestParam String title, @RequestParam String content, RedirectAttributes redirectAttributes) {
+ List widgets = getWidgets(area);
+ WidgetDto widget = new WidgetDto();
+ widget.setId(UUID.randomUUID().toString());
+ widget.setType(type);
+ widget.setTitle(title);
+ widget.setContent(content);
+
+ widgets.add(widget);
+ saveWidgets(area, widgets);
+
+ redirectAttributes.addFlashAttribute("successMessage", "Widget added to " + area);
+ return "redirect:/manage/widgets";
+ }
+
+ @PostMapping("/delete")
+ public String deleteWidget(@RequestParam String area, @RequestParam String id, RedirectAttributes redirectAttributes) {
+ List widgets = getWidgets(area);
+ widgets.removeIf(w -> w.getId().equals(id));
+ saveWidgets(area, widgets);
+
+ redirectAttributes.addFlashAttribute("successMessage", "Widget removed.");
+ return "redirect:/manage/widgets";
+ }
+}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Menu.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Menu.java
new file mode 100644
index 00000000..665cea85
--- /dev/null
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Menu.java
@@ -0,0 +1,85 @@
+package com.sisvietnamvn.web.domain;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.io.Serial;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A Menu entity for managing navigation menus.
+ * Maps to the "sis_menu" database table.
+ */
+@Entity
+@Table(name = "sis_menu")
+public class Menu extends AbstractAuditingEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
+ @SequenceGenerator(name = "sequenceGenerator")
+ private Long id;
+
+ @NotNull
+ @Size(max = 255)
+ @Column(name = "name", length = 255, nullable = false)
+ private String name;
+
+ @Size(max = 100)
+ @Column(name = "location", length = 100)
+ private String location;
+
+ @OneToMany(mappedBy = "menu", cascade = CascadeType.ALL, orphanRemoval = true)
+ @OrderBy("displayOrder ASC")
+ private List
+
+
+
Widgets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Title
+
+
+
Type
+
Content...
+
+
+ No widgets in Sidebar.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Title
+
+
+
Type
+
Content...
+
+
+ No widgets in Footer.
+
+
+
+
+
+
+
+
diff --git a/sisvietnamvn_main/src/main/resources/templates/manage/widgets/list.html b/sisvietnamvn_main/src/main/resources/templates/manage/widgets/list.html
new file mode 100644
index 00000000..c9717a65
--- /dev/null
+++ b/sisvietnamvn_main/src/main/resources/templates/manage/widgets/list.html
@@ -0,0 +1,115 @@
+
+
+