fix(collections): add form binding and HTMX redirect support

Add form:"" tags to CreateCollectionRequest and UpdateCollectionRequest
structs to enable proper form data binding with Echo's c.Bind().

This change aligns with the pattern used in auth handlers where both
form:"" and json:"" tags are present, allowing the same request structs
to work with both JSON payloads (API) and form data (HTMX).

Changes:
- Add form:"name", form:"description", form:"color", form:"icon",
  form:"auto_assign_rules", and form:"view_settings" tags to both
  CreateCollectionRequest and UpdateCollectionRequest

Additionally, add HTMX redirect support to CreateCollection and
UpdateCollection handlers:
- Add HX-Redirect header for HTMX requests after successful create/update
- Add HTML redirect response to DeleteCollection for HTMX requests
  (follows pattern from auth.go: inline script with window.location.href)

This ensures HTMX form submissions properly redirect to /collections
after successful operations, while maintaining API compatibility for
JSON requests.
This commit is contained in:
2026-03-01 20:59:56 -05:00
parent 42a20e3be3
commit 511ae66688
+35 -16
View File
@@ -35,21 +35,21 @@ func NewCollectionHandler(db *database.Queries, libraryService *services.Library
} }
type CreateCollectionRequest struct { type CreateCollectionRequest struct {
Name string `json:"name" validate:"required"` Name string `form:"name" json:"name" validate:"required"`
Description string `json:"description"` Description string `form:"description" json:"description"`
Color string `json:"color"` Color string `form:"color" json:"color"`
Icon string `json:"icon"` Icon string `form:"icon" json:"icon"`
AutoAssignRules []services.Rule `json:"auto_assign_rules"` AutoAssignRules []services.Rule `form:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings map[string]interface{} `json:"view_settings"` ViewSettings map[string]interface{} `form:"view_settings" json:"view_settings"`
} }
type UpdateCollectionRequest struct { type UpdateCollectionRequest struct {
Name string `json:"name" validate:"required"` Name string `form:"name" json:"name" validate:"required"`
Description string `json:"description"` Description string `form:"description" json:"description"`
Color string `json:"color"` Color string `form:"color" json:"color"`
Icon string `json:"icon"` Icon string `form:"icon" json:"icon"`
AutoAssignRules []services.Rule `json:"auto_assign_rules"` AutoAssignRules []services.Rule `form:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings map[string]interface{} `json:"view_settings"` ViewSettings map[string]interface{} `form:"view_settings" json:"view_settings"`
} }
type AddBooksRequest struct { type AddBooksRequest struct {
@@ -113,7 +113,7 @@ func (h *CollectionHandler) CreateCollection(c echo.Context) error {
} }
bookCount := int32(0) bookCount := int32(0)
return c.JSON(http.StatusCreated, map[string]interface{}{ response := map[string]interface{}{
"id": uuid.UUID(collection.ID.Bytes).String(), "id": uuid.UUID(collection.ID.Bytes).String(),
"user_id": uuid.UUID(collection.UserID.Bytes).String(), "user_id": uuid.UUID(collection.UserID.Bytes).String(),
"name": collection.Name, "name": collection.Name,
@@ -124,7 +124,14 @@ func (h *CollectionHandler) CreateCollection(c echo.Context) error {
"view_settings": collection.ViewSettings, "view_settings": collection.ViewSettings,
"book_count": bookCount, "book_count": bookCount,
"created_at": collection.CreatedAt.Time.String(), "created_at": collection.CreatedAt.Time.String(),
}) }
// 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.StatusCreated, response)
} }
func (h *CollectionHandler) GetCollections(c echo.Context) error { func (h *CollectionHandler) GetCollections(c echo.Context) error {
@@ -252,7 +259,7 @@ func (h *CollectionHandler) UpdateCollection(c echo.Context) error {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
} }
return c.JSON(http.StatusOK, map[string]interface{}{ response := map[string]interface{}{
"id": uuid.UUID(collection.ID.Bytes).String(), "id": uuid.UUID(collection.ID.Bytes).String(),
"user_id": uuid.UUID(collection.UserID.Bytes).String(), "user_id": uuid.UUID(collection.UserID.Bytes).String(),
"name": collection.Name, "name": collection.Name,
@@ -262,7 +269,14 @@ func (h *CollectionHandler) UpdateCollection(c echo.Context) error {
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules), "auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
"view_settings": json.RawMessage(collection.ViewSettings), "view_settings": json.RawMessage(collection.ViewSettings),
"created_at": collection.CreatedAt.Time.String(), "created_at": collection.CreatedAt.Time.String(),
}) }
// 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.StatusCreated, response)
} }
func (h *CollectionHandler) DeleteCollection(c echo.Context) error { func (h *CollectionHandler) DeleteCollection(c echo.Context) error {
@@ -276,6 +290,11 @@ func (h *CollectionHandler) DeleteCollection(c echo.Context) error {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
} }
if c.Request().Header.Get("HX-Request") == "true" {
html := `<div class="text-green-500">Collection deleted! Redirecting...</div><script>window.location.href = '/collections';</script>`
return c.HTML(http.StatusOK, html)
}
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
} }