Implement atomic collection item creation API with unified content engine approach

Updates collection creation to use database-first atomic operations for reliable collection item management. Replaces manual database calls with unified content engine methods that handle content extraction, storage, and structural template generation consistently.

Key changes:
- Replace manual database operations in CreateCollectionItem handler with DatabaseClient.CreateCollectionItemAtomic()
- Implement unified content engine approach for API-based collection item creation
- Add atomic collection item creation methods across all content clients
- Enhance reconstruction to use stored structural templates with content ID hydration
- Add comprehensive collection management API methods in JavaScript client
- Implement collection manager UI with create, delete, and reorder functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-23 18:39:37 +02:00
parent 5f494b8aa8
commit 1ae4176f23
8 changed files with 755 additions and 202 deletions

View File

@@ -17,6 +17,13 @@ export class CollectionManager {
this.apiClient = apiClient;
this.auth = auth;
// Extract collection ID from container
this.collectionId = this.container.getAttribute('data-content-id');
if (!this.collectionId) {
console.error('❌ Collection container missing data-content-id attribute');
return;
}
// Collection state
this.template = null;
this.items = [];
@@ -26,19 +33,22 @@ export class CollectionManager {
this.addButton = null;
this.itemControls = new Map(); // Map item element to its controls
console.log('🔄 CollectionManager initialized for:', this.container);
console.log('🔄 CollectionManager initialized for:', this.container, 'Collection ID:', this.collectionId);
}
/**
* Initialize the collection manager
*/
initialize() {
async initialize() {
if (this.isActive) return;
console.log('🚀 Starting collection management for:', this.container.className);
// Analyze existing content to detect template
this.analyzeTemplate();
// Sync with backend to map existing items to collection item IDs
await this.syncWithBackend();
// Add collection management UI only when in edit mode
this.setupEditModeDetection();
@@ -135,10 +145,12 @@ export class CollectionManager {
console.log('📋 Template detected:', this.template);
// Store reference to current items
// For existing items, try to extract collection item IDs if they exist
this.items = children.map((child, index) => ({
element: child,
index: index,
id: this.generateItemId(index)
id: this.generateItemId(index),
collectionItemId: this.extractCollectionItemId(child)
}));
}
@@ -227,6 +239,61 @@ export class CollectionManager {
generateItemId(index) {
return `item-${Date.now()}-${index}`;
}
/**
* Extract collection item ID from existing DOM element
* 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');
if (itemId) {
return itemId;
}
// For existing items reconstructed from database, try to infer from data-content-id
// The backend should have generated collection item IDs based on collection ID
const contentId = element.getAttribute('data-content-id');
if (contentId && this.collectionId) {
// This is a heuristic - we'll need to fetch the actual mapping from the backend
// For now, return null and let the backend operations handle missing IDs
return null;
}
return null;
}
/**
* Sync frontend state with backend collection items
* This maps existing DOM elements to their collection item IDs
*/
async syncWithBackend() {
if (!this.collectionId) {
console.warn('⚠️ Cannot sync with backend: no collection ID');
return;
}
try {
// Fetch current collection items from backend
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
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}`);
}
});
console.log('✅ Frontend-backend sync completed');
} catch (error) {
console.error('❌ Failed to sync with backend:', error);
// Continue without backend sync - collection management will still work for new items
}
}
/**
* Create the "+ Add" button positioned in top right of container
@@ -334,45 +401,85 @@ export class CollectionManager {
}
/**
* Add a new item to the collection
* Add a new item to the collection (backend-first approach)
*/
addNewItem() {
async addNewItem() {
console.log(' Adding new item to collection');
if (!this.template) {
console.error('❌ No template available for creating new items');
if (!this.template || !this.collectionId) {
console.error('❌ No template or collection ID available for creating new items');
return;
}
// Create new item from template
const newItem = this.createItemFromTemplate();
// Add to DOM
this.container.insertBefore(newItem, this.addButton);
// Update items array
const newItemData = {
element: newItem,
index: this.items.length,
id: this.generateItemId(this.items.length)
};
this.items.push(newItemData);
// Add controls to new item
this.addItemControls(newItem, this.items.length - 1);
// Re-initialize any .insertr elements in the new item
// This allows the existing editor system to handle individual field editing
this.initializeInsertrElements(newItem);
// Update all item controls (indices may have changed)
this.updateAllItemControls();
console.log('✅ New item added successfully');
try {
// 1. Create collection item in database first (backend-first approach)
const templateId = 1; // Use first template by default
const collectionItem = await this.apiClient.createCollectionItem(this.collectionId, templateId);
// 2. Create DOM element from the returned collection item data
const newItem = this.createItemFromCollectionData(collectionItem);
// 3. Add to DOM
this.container.insertBefore(newItem, this.addButton);
// 4. Update items array with backend data
const newItemData = {
element: newItem,
index: this.items.length,
id: collectionItem.item_id,
collectionItem: collectionItem
};
this.items.push(newItemData);
// 5. Add controls to new item
this.addItemControls(newItem, this.items.length - 1);
// 6. Re-initialize any .insertr elements in the new item
this.initializeInsertrElements(newItem);
// 7. Update all item controls (indices may have changed)
this.updateAllItemControls();
// 8. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ New item added successfully:', collectionItem.item_id);
} catch (error) {
console.error('❌ Failed to add new collection item:', error);
alert('Failed to add new item. Please try again.');
}
}
/**
* Create a new item from the template
* Create a DOM element from collection item data returned by backend
* Backend is the source of truth - use its HTML content directly
*/
createItemFromCollectionData(collectionItem) {
// Use backend HTML content directly (database is source of truth)
if (collectionItem.html_content && collectionItem.html_content.trim()) {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = collectionItem.html_content;
const newItem = tempContainer.firstElementChild;
// Set the collection item ID as data attribute for future reference
newItem.setAttribute('data-collection-item-id', collectionItem.item_id);
return newItem;
} else {
// Fallback: create from frontend template if backend content is empty
const tempContainer = document.createElement('div');
tempContainer.innerHTML = this.template.htmlTemplate;
const newItem = tempContainer.firstElementChild;
// Set the collection item ID as data attribute for future reference
newItem.setAttribute('data-collection-item-id', collectionItem.item_id);
return newItem;
}
}
/**
* Create a new item from the template (legacy method, kept for compatibility)
*/
createItemFromTemplate() {
// Create element from template HTML
@@ -511,38 +618,60 @@ export class CollectionManager {
}
/**
* Remove an item from the collection
* Remove an item from the collection (backend-first approach)
*/
removeItem(itemElement) {
async removeItem(itemElement) {
if (!confirm('Are you sure you want to remove this item?')) {
return;
}
console.log('🗑️ Removing item from collection');
// Remove controls
const controls = this.itemControls.get(itemElement);
if (controls) {
controls.remove();
this.itemControls.delete(itemElement);
try {
// 1. Get the collection item ID from the element
const collectionItemId = itemElement.getAttribute('data-collection-item-id');
if (!collectionItemId) {
console.error('❌ Cannot remove item: missing data-collection-item-id attribute');
return;
}
// 2. Delete from database first (backend-first approach)
const success = await this.apiClient.deleteCollectionItem(this.collectionId, collectionItemId);
if (!success) {
alert('Failed to remove item from database. Please try again.');
return;
}
// 3. Remove controls
const controls = this.itemControls.get(itemElement);
if (controls) {
controls.remove();
this.itemControls.delete(itemElement);
}
// 4. Remove from items array
this.items = this.items.filter(item => item.element !== itemElement);
// 5. Remove from DOM
itemElement.remove();
// 6. Update all item controls (indices changed)
this.updateAllItemControls();
// 7. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ Item removed successfully:', collectionItemId);
} catch (error) {
console.error('❌ Failed to remove collection item:', error);
alert('Failed to remove item. Please try again.');
}
// Remove from items array
this.items = this.items.filter(item => item.element !== itemElement);
// Remove from DOM
itemElement.remove();
// Update all item controls (indices changed)
this.updateAllItemControls();
console.log('✅ Item removed successfully');
}
/**
* Move an item up or down in the collection
* Move an item up or down in the collection (backend-first approach)
*/
moveItem(itemElement, direction) {
async moveItem(itemElement, direction) {
console.log(`🔄 Moving item ${direction}`);
const currentIndex = this.items.findIndex(item => item.element === itemElement);
@@ -556,26 +685,49 @@ export class CollectionManager {
} else {
return; // Can't move in that direction
}
// Get the target position in DOM
const targetItem = this.items[newIndex];
// Move in DOM
if (direction === 'up') {
this.container.insertBefore(itemElement, targetItem.element);
} else {
this.container.insertBefore(itemElement, targetItem.element.nextSibling);
try {
// 1. Get the collection item ID
const collectionItemId = itemElement.getAttribute('data-collection-item-id');
if (!collectionItemId) {
console.error('❌ Cannot move item: missing data-collection-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;
}
// 3. Get the target position in DOM
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
[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
this.updateAllItemControls();
// 7. Trigger site enhancement to update static files
await this.apiClient.enhanceSite();
console.log('✅ Item moved successfully:', collectionItemId, '→ position', newIndex);
} catch (error) {
console.error('❌ Failed to move collection item:', error);
alert('Failed to move item. Please try again.');
}
// 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;
// Update all item controls
this.updateAllItemControls();
console.log('✅ Item moved successfully');
}
/**