Update RestoreSystemCollection handler to support form-encoded requests from HTMX: - Add 'form' struct tags to CollectionName and ResetType fields to enable binding from both JSON payloads and form submissions (required for HTMX compatibility) - Add conditional HTMX redirect handling that sets HX-Redirect header when the request originates from HTMX, directing users to /collections after successful restoration This change enables the system collection restore functionality to work seamlessly with HTMX-based modal forms, improving the user experience by providing proper navigation after the restore operation completes without requiring JavaScript redirect logic.
219 lines
6.6 KiB
Go
219 lines
6.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/internal/utils"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
type DashboardHandler struct {
|
|
db *database.Queries
|
|
dashboardService *services.DashboardService
|
|
}
|
|
|
|
func NewDashboardHandler(db *database.Queries) *DashboardHandler {
|
|
return &DashboardHandler{
|
|
db: db,
|
|
dashboardService: services.NewDashboardService(db),
|
|
}
|
|
}
|
|
|
|
func (h *DashboardHandler) GetSections(c echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
libraryID := c.QueryParam("library_id")
|
|
if libraryID == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
|
|
}
|
|
libUUID, err := uuid.Parse(libraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
|
}
|
|
|
|
prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
|
|
|
limit := 20
|
|
if prefs.ItemsPerSection.Valid {
|
|
limit = int(prefs.ItemsPerSection.Int32)
|
|
}
|
|
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
limit = l
|
|
}
|
|
}
|
|
|
|
sections, err := h.dashboardService.GetDashboardSections(
|
|
c.Request().Context(),
|
|
userUUID,
|
|
libUUID,
|
|
limit,
|
|
prefs.CollectionOrder,
|
|
prefs.HiddenCollections,
|
|
)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"})
|
|
}
|
|
|
|
sectionData := BuildSections(sections)
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData})
|
|
}
|
|
|
|
func (h *DashboardHandler) UpdatePreferences(c echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
var req struct {
|
|
LibraryID string `json:"library_id"`
|
|
HiddenCollections []string `json:"hidden_collections"`
|
|
CollectionOrder []string `json:"collection_order"`
|
|
ItemsPerSection int `json:"items_per_section"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
|
}
|
|
|
|
libUUID, err := uuid.Parse(req.LibraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
|
}
|
|
|
|
// Sanitize preferences (remove duplicates)
|
|
cleanHidden, cleanOrder := h.dashboardService.SanitizeDashboardPreferences(req.HiddenCollections, req.CollectionOrder)
|
|
req.HiddenCollections = cleanHidden
|
|
req.CollectionOrder = cleanOrder
|
|
|
|
prefs, err := h.dashboardService.UpsertDashboardPreferences(c.Request().Context(), database.UpsertDashboardPreferencesParams{
|
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
|
HiddenCollections: req.HiddenCollections,
|
|
CollectionOrder: req.CollectionOrder,
|
|
ItemsPerSection: pgtype.Int4{Int32: int32(req.ItemsPerSection), Valid: true},
|
|
})
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to save preferences"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, prefs)
|
|
}
|
|
|
|
func (h *DashboardHandler) RestoreSystemCollection(c echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
var req struct {
|
|
CollectionName string `form:"collection_name" json:"collection_name"`
|
|
ResetType string `form:"reset_type" json:"reset_type"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
|
}
|
|
|
|
if req.CollectionName == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "collection_name required"})
|
|
}
|
|
|
|
if req.ResetType == "" {
|
|
req.ResetType = "full"
|
|
}
|
|
|
|
validCollections := map[string]bool{
|
|
"continue-reading": true,
|
|
"recently-added": true,
|
|
"recently-read": true,
|
|
"not-started": true,
|
|
}
|
|
if !validCollections[req.CollectionName] {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid system collection name"})
|
|
}
|
|
|
|
if req.ResetType != "full" && req.ResetType != "keep_books" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "reset_type must be 'full' or 'keep_books'"})
|
|
}
|
|
|
|
err := h.dashboardService.RestoreSystemCollection(c.Request().Context(), userUUID, req.CollectionName, req.ResetType)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to restore system collection"})
|
|
}
|
|
|
|
// Add redirect header for HTMX requests
|
|
if c.Request().Header.Get("HX-Request") == "true" {
|
|
c.Response().Header().Set("HX-Redirect", "/collections")
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"})
|
|
}
|
|
|
|
func BuildSections(sections []services.DashboardSection) []SectionData {
|
|
var result []SectionData
|
|
|
|
for _, ds := range sections {
|
|
bookCards := make([]BookInfo, len(ds.Items))
|
|
for i, item := range ds.Items {
|
|
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
|
bookCards[i] = BookInfo{
|
|
MediaItemID: itemUUID.String(),
|
|
Title: item.Title,
|
|
Author: textToString(item.Author),
|
|
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
|
}
|
|
}
|
|
|
|
result = append(result, SectionData{
|
|
ID: ds.CollectionName,
|
|
CollectionID: ds.CollectionID.String(),
|
|
IsSystem: ds.IsSystem,
|
|
Title: ds.Title,
|
|
Description: ds.Description,
|
|
Icon: ds.Icon,
|
|
Items: bookCards,
|
|
ViewAllURL: getViewAllURL(ds.CollectionID.String()),
|
|
Priority: ds.Priority,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getViewAllURL(collectionID string) string {
|
|
if collectionID != "" {
|
|
return "/collections/" + collectionID
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (h *DashboardHandler) GetPreferences(c echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
libraryID := c.QueryParam("library_id")
|
|
|
|
// ✅ Add validation
|
|
if libraryID == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
|
|
}
|
|
|
|
libUUID, err := uuid.Parse(libraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
|
}
|
|
prefs, err := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Preferences not found"})
|
|
}
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"hidden_collections": prefs.HiddenCollections,
|
|
"collection_order": prefs.CollectionOrder,
|
|
"items_per_section": prefs.ItemsPerSection.Int32,
|
|
})
|
|
}
|