Implement collection item reordering with bulk operations and persistent HTML attributes

- Add bulk reorder API endpoint (PUT /api/collections/{id}/reorder) with atomic transactions
- Replace individual position updates with efficient bulk operations in frontend
- Implement unified ID generation and proper data-item-id injection during enhancement
- Fix collection item position persistence through content edit cycles
- Add optimistic UI with rollback capability for better user experience
- Update sqlc queries to include last_edited_by fields in position updates
- Remove obsolete data-content-type attributes and unify naming conventions
This commit is contained in:
2025-10-07 22:59:00 +02:00
parent c5754181f6
commit 824719f07d
13 changed files with 545 additions and 55 deletions

View File

@@ -22,7 +22,6 @@ import (
"github.com/insertr/insertr/internal/engine"
)
// ContentHandler handles all content-related HTTP requests
type ContentHandler struct {
database *db.Database
@@ -1094,7 +1093,7 @@ func (h *ContentHandler) UpdateCollectionItem(w http.ResponseWriter, r *http.Req
maxPos = 0
}
}
// Check if position is valid (1-based, within bounds)
if int64(req.Position) > maxPos {
http.Error(w, fmt.Sprintf("Invalid position: %d exceeds max position %d", req.Position, maxPos), http.StatusBadRequest)
@@ -1117,7 +1116,7 @@ func (h *ContentHandler) UpdateCollectionItem(w http.ResponseWriter, r *http.Req
return
}
}
// If only position update (no html_content), just get the updated item
if req.HTMLContent == "" {
updatedItem, err = h.database.GetSQLiteQueries().GetCollectionItem(context.Background(), sqlite.GetCollectionItemParams{
@@ -1149,7 +1148,7 @@ func (h *ContentHandler) UpdateCollectionItem(w http.ResponseWriter, r *http.Req
return
}
}
// If only position update (no html_content), just get the updated item
if req.HTMLContent == "" {
updatedItem, err = h.database.GetPostgreSQLQueries().GetCollectionItem(context.Background(), postgresql.GetCollectionItemParams{
@@ -1226,6 +1225,112 @@ func (h *ContentHandler) DeleteCollectionItem(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusNoContent)
}
// ReorderCollection handles PUT /api/collections/{id}/reorder
func (h *ContentHandler) ReorderCollection(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var req ReorderCollectionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if req.UpdatedBy == "" {
req.UpdatedBy = "api"
}
// Validate that all items belong to the collection and have valid positions
if len(req.Items) == 0 {
http.Error(w, "Items array cannot be empty", http.StatusBadRequest)
return
}
// Update positions for all items in a transaction for atomicity
switch h.database.GetDBType() {
case "sqlite3":
// Use transaction for atomic bulk updates
tx, err := h.database.GetSQLiteDB().Begin()
if err != nil {
http.Error(w, "Failed to begin transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// Create queries with transaction context
qtx := h.database.GetSQLiteQueries().WithTx(tx)
for _, item := range req.Items {
err = qtx.UpdateCollectionItemPosition(context.Background(), sqlite.UpdateCollectionItemPositionParams{
ItemID: item.ItemID,
CollectionID: collectionID,
SiteID: siteID,
Position: int64(item.Position),
LastEditedBy: req.UpdatedBy,
})
if err != nil {
http.Error(w, fmt.Sprintf("Failed to update position for item %s: %v", item.ItemID, err), http.StatusInternalServerError)
return
}
}
// Commit transaction
if err = tx.Commit(); err != nil {
http.Error(w, "Failed to commit bulk reorder transaction", http.StatusInternalServerError)
return
}
case "postgres":
// Use transaction for atomic bulk updates
tx, err := h.database.GetPostgreSQLDB().Begin()
if err != nil {
http.Error(w, "Failed to begin transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// Create queries with transaction context
qtx := h.database.GetPostgreSQLQueries().WithTx(tx)
for _, item := range req.Items {
err = qtx.UpdateCollectionItemPosition(context.Background(), postgresql.UpdateCollectionItemPositionParams{
ItemID: item.ItemID,
CollectionID: collectionID,
SiteID: siteID,
Position: int32(item.Position),
LastEditedBy: req.UpdatedBy,
})
if err != nil {
http.Error(w, fmt.Sprintf("Failed to update position for item %s: %v", item.ItemID, err), http.StatusInternalServerError)
return
}
}
// Commit transaction
if err = tx.Commit(); err != nil {
http.Error(w, "Failed to commit bulk reorder transaction", http.StatusInternalServerError)
return
}
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
// Return success response
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Successfully reordered %d items", len(req.Items)),
}
json.NewEncoder(w).Encode(response)
}
// Collection conversion helpers
// convertToAPICollection converts database collection to API model