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

@@ -243,21 +243,19 @@ export class ApiClient {
}
/**
* Update collection item position (for reordering)
* Reorder collection items in bulk
* @param {string} collectionId - Collection ID
* @param {string} itemId - Item ID to update
* @param {number} newPosition - New position index
* @param {Array} itemOrder - Array of {itemId, position} objects
* @returns {Promise<boolean>} Success status
*/
async updateCollectionItemPosition(collectionId, itemId, newPosition) {
async reorderCollection(collectionId, itemOrder) {
try {
const collectionsUrl = this.getCollectionsUrl();
const payload = {
site_id: this.siteId,
position: newPosition
items: itemOrder
};
const response = await fetch(`${collectionsUrl}/${collectionId}/items/${itemId}`, {
const response = await fetch(`${collectionsUrl}/${collectionId}/reorder?site_id=${this.siteId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -270,11 +268,11 @@ export class ApiClient {
return true;
} else {
const errorText = await response.text();
console.error(`❌ Failed to update collection item position (${response.status}): ${errorText}`);
console.error(`❌ Failed to reorder collection (${response.status}): ${errorText}`);
return false;
}
} catch (error) {
console.error('❌ Error updating collection item position:', error);
console.error('❌ Error reordering collection:', error);
return false;
}
}

View File

@@ -245,8 +245,8 @@ export class CollectionManager {
* This is used for existing items that were reconstructed from database
*/
extractCollectionItemId(element) {
// Look for data-collection-item-id attribute first (newly created items)
let itemId = element.getAttribute('data-collection-item-id');
// Look for data-item-id attribute first (from server enhancement)
let itemId = element.getAttribute('data-item-id');
if (itemId) {
return itemId;
}
@@ -278,12 +278,11 @@ export class CollectionManager {
const backendItems = await this.apiClient.getCollectionItems(this.collectionId);
console.log('📋 Backend collection items:', backendItems);
// Map backend items to existing DOM elements by position
// This assumes the DOM order matches the database order
// Items already have data-item-id from server enhancement
// Just update the collectionItemId in our internal items array
backendItems.forEach((backendItem, index) => {
if (this.items[index]) {
this.items[index].collectionItemId = backendItem.item_id;
this.items[index].element.setAttribute('data-collection-item-id', backendItem.item_id);
console.log(`🔗 Mapped DOM element ${index} to collection item ${backendItem.item_id}`);
}
});
@@ -462,7 +461,7 @@ export class CollectionManager {
const newItem = tempContainer.firstElementChild;
// Set the collection item ID as data attribute for future reference
newItem.setAttribute('data-collection-item-id', collectionItem.item_id);
newItem.setAttribute('data-item-id', collectionItem.item_id);
return newItem;
} else {
@@ -472,7 +471,7 @@ export class CollectionManager {
const newItem = tempContainer.firstElementChild;
// Set the collection item ID as data attribute for future reference
newItem.setAttribute('data-collection-item-id', collectionItem.item_id);
newItem.setAttribute('data-item-id', collectionItem.item_id);
return newItem;
}
@@ -629,9 +628,9 @@ export class CollectionManager {
try {
// 1. Get the collection item ID from the element
const collectionItemId = itemElement.getAttribute('data-collection-item-id');
const collectionItemId = itemElement.getAttribute('data-item-id');
if (!collectionItemId) {
console.error('❌ Cannot remove item: missing data-collection-item-id attribute');
console.error('❌ Cannot remove item: missing data-item-id attribute');
return;
}
@@ -688,42 +687,54 @@ export class CollectionManager {
try {
// 1. Get the collection item ID
const collectionItemId = itemElement.getAttribute('data-collection-item-id');
const collectionItemId = itemElement.getAttribute('data-item-id');
if (!collectionItemId) {
console.error('❌ Cannot move item: missing data-collection-item-id attribute');
console.error('❌ Cannot move item: missing data-item-id attribute');
return;
}
// 2. Update position in database first (backend-first approach)
// Note: Backend expects 0-based positions, but we may need to adjust based on backend implementation
const success = await this.apiClient.updateCollectionItemPosition(this.collectionId, collectionItemId, newIndex);
if (!success) {
alert('Failed to update item position in database. Please try again.');
return;
}
// 2. Store original state for potential rollback
const originalItems = [...this.items];
// 3. Get the target position in DOM
// 3. Perform DOM move (optimistic UI)
const targetItem = this.items[newIndex];
// 4. Move in DOM
if (direction === 'up') {
this.container.insertBefore(itemElement, targetItem.element);
} else {
this.container.insertBefore(itemElement, targetItem.element.nextSibling);
}
// 5. Update items array
// 4. Update items array
[this.items[currentIndex], this.items[newIndex]] = [this.items[newIndex], this.items[currentIndex]];
this.items[currentIndex].index = currentIndex;
this.items[newIndex].index = newIndex;
// 6. Update all item controls
// 5. Update all item controls
this.updateAllItemControls();
// 7. Trigger site enhancement to update static files
// 6. Build bulk reorder payload from current DOM state
const itemOrder = this.items.map((item, index) => ({
itemId: item.element.getAttribute('data-item-id'),
position: index + 1 // 1-based positions
}));
// 7. Send bulk reorder to backend
const success = await this.apiClient.reorderCollection(this.collectionId, itemOrder);
if (!success) {
// Rollback DOM changes
this.items = originalItems;
this.items.forEach(item => {
this.container.appendChild(item.element);
});
this.updateAllItemControls();
alert('Failed to update item position in database. Please try again.');
return;
}
// 8. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ Item moved successfully:', collectionItemId, '→ position', newIndex);
console.log('✅ Item moved successfully:', collectionItemId, '→ position', newIndex + 1);
} catch (error) {
console.error('❌ Failed to move collection item:', error);
alert('Failed to move item. Please try again.');