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

@@ -3,7 +3,6 @@ package engine
import (
"fmt"
"strings"
"time"
"golang.org/x/net/html"
)
@@ -103,8 +102,8 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
// Generate structural ID for the collection container
collectionID := e.idGenerator.Generate(collectionElem.Node, input.FilePath)
// Add data-content-id attribute to the collection container
e.setAttribute(collectionElem.Node, "data-content-id", collectionID)
// Add data-collection-id attribute to the collection container
e.setAttribute(collectionElem.Node, "data-collection-id", collectionID)
// Process collection during enhancement or content injection
if input.Mode == Enhancement || input.Mode == ContentInjection {
@@ -243,7 +242,6 @@ func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string)
e.setAttribute(node, "data-content-id", contentID)
}
// setAttribute sets an attribute on an HTML node
func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
// Remove existing attribute if it exists
@@ -377,8 +375,6 @@ func (e *ContentEngine) extractHTMLContent(node *html.Node) string {
return strings.TrimSpace(content.String())
}
// extractOriginalTemplate extracts the outer HTML of the element (including the element itself)
func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string {
var buf strings.Builder
@@ -549,6 +545,12 @@ func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.No
return fmt.Errorf("failed to store initial collection items: %w", err)
}
// Reconstruct items from database to ensure proper data-item-id injection
err = e.reconstructCollectionItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to reconstruct initial collection items: %w", err)
}
return nil
}
@@ -619,6 +621,12 @@ func (e *ContentEngine) reconstructCollectionItems(collectionNode *html.Node, co
for structuralChild := structuralBody.FirstChild; structuralChild != nil; {
next := structuralChild.NextSibling
structuralBody.RemoveChild(structuralChild)
// Inject data-item-id attribute for collection item identification
if structuralChild.Type == html.ElementNode {
e.setAttribute(structuralChild, "data-item-id", item.ItemID)
}
collectionNode.AppendChild(structuralChild)
structuralChild = next
}
@@ -730,15 +738,18 @@ func (e *ContentEngine) CreateCollectionItemFromTemplate(
templateHTML string,
lastEditedBy string,
) (*CollectionItemWithTemplate, error) {
// Generate unique item ID
itemID := fmt.Sprintf("%s-item-%d", collectionID, time.Now().Unix())
// Create virtual element from template (like enhancement path)
virtualElement, err := e.createVirtualElementFromTemplate(templateHTML)
if err != nil {
return nil, fmt.Errorf("failed to create virtual element: %w", err)
}
// Generate unique item ID using unified generator with collection context
itemID := e.idGenerator.Generate(virtualElement, "collection-item")
if err != nil {
return nil, fmt.Errorf("failed to create virtual element: %w", err)
}
// Process .insertr elements and create content entries (unified approach)
contentEntries, err := e.processChildElementsAsContent(virtualElement, siteID, itemID)
if err != nil {
@@ -808,8 +819,8 @@ func (e *ContentEngine) storeChildrenAsCollectionItems(collectionNode *html.Node
// Store each child using unified .insertr approach (content table + structural template)
for i, childElement := range childElements {
// Generate item ID (like content ID generation)
itemID := fmt.Sprintf("%s-initial-%d", collectionID, i+1)
// Generate item ID using unified generator with collection context
itemID := e.idGenerator.Generate(childElement, "collection-item")
// Process .insertr elements within this child (unified approach)
contentEntries, err := e.processChildElementsAsContent(childElement, siteID, itemID)