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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user