Implement class-based template differentiation and fix collection item creation
- Add class-based template comparison to differentiate styling variants - Implement template deduplication based on structure + class signatures - Add GetCollectionTemplate method to repository interface and implementations - Fix collection item creation by replacing unimplemented CreateCollectionItemAtomic - Add template selection modal with auto-default selection in frontend - Generate meaningful template names from distinctive CSS classes - Fix unique constraint violations with timestamp-based collection item IDs - Add collection templates API endpoint for frontend template fetching - Update simple demo with featured/compact/dark testimonial variants for testing
This commit is contained in:
20
collection-example.html
Normal file
20
collection-example.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!-- insertr collections is defined with .insertr-add -->
|
||||
<div class="insertr-add">
|
||||
<!-- the collection containers children are collection items. -->
|
||||
<!-- this is meant mostly for structural elements such as cards, testimonials etc. -->
|
||||
<!-- it is not meant for text editing (such as multiple paragraphs) and you should instead use .insertr-content -->
|
||||
<div class="variant-1">
|
||||
<!-- The cards content can be edited as normal insertr elements -->
|
||||
<h2 class="insertr">Card 1</h2>
|
||||
<p class="insertr lead">This is a lead paragraph</p>
|
||||
<p class="insertr">This is the main paragraph that could be longer and give additional info</p>
|
||||
</div>
|
||||
<!-- Since the developer has defined two templates in the markup two templates must be generated. -->
|
||||
<!-- they might have different structure, or been styled differently. (imagine an alternating color on testimonial cards). the variant-x classnames are only for ilustration and is not a keyword -->
|
||||
<div class="variant-2">
|
||||
<h2 class="insertr">Card 2</h2>
|
||||
<p class="insertr">This is the main paragraph that could be longer and give additional info</p>
|
||||
<button type="button insertr">Call to action</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +142,29 @@
|
||||
.testimonial-item cite:before {
|
||||
content: "— ";
|
||||
}
|
||||
|
||||
/* Styling variants for template differentiation testing */
|
||||
.testimonial-item.featured {
|
||||
border-left: 4px solid #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.testimonial-item.compact {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.testimonial-item.dark {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.testimonial-item.dark blockquote {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.testimonial-item.dark cite {
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -225,11 +248,11 @@
|
||||
<blockquote class="insertr">Not all that is gold does glitter</blockquote>
|
||||
<cite class="insertr">Tolkien</cite>
|
||||
</div>
|
||||
<div class="testimonial-item">
|
||||
<div class="testimonial-item featured">
|
||||
<blockquote class="insertr">The journey of a thousand miles begins with one step</blockquote>
|
||||
<cite class="insertr">Lao Tzu</cite>
|
||||
</div>
|
||||
<div class="testimonial-item">
|
||||
<div class="testimonial-item compact dark">
|
||||
<blockquote class="insertr">Innovation distinguishes between a leader and a follower</blockquote>
|
||||
<cite class="insertr">Steve Jobs</cite>
|
||||
</div>
|
||||
|
||||
@@ -405,6 +405,30 @@ func (h *ContentHandler) GetCollectionItems(w http.ResponseWriter, r *http.Reque
|
||||
json.NewEncoder(w).Encode(items)
|
||||
}
|
||||
|
||||
// GetCollectionTemplates handles GET /api/collections/{id}/templates
|
||||
func (h *ContentHandler) GetCollectionTemplates(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
|
||||
}
|
||||
|
||||
templates, err := h.repository.GetCollectionTemplates(context.Background(), siteID, collectionID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"templates": templates,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// CreateCollectionItem handles POST /api/collections/{id}/items
|
||||
func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Request) {
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
@@ -437,15 +461,29 @@ func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if req.TemplateID == 0 {
|
||||
req.TemplateID = 1 // Default to first template
|
||||
http.Error(w, "template_id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Use atomic collection item creation from repository
|
||||
createdItem, err := h.repository.CreateCollectionItemAtomic(
|
||||
context.Background(),
|
||||
// Get the specific template by ID
|
||||
selectedTemplate, err := h.repository.GetCollectionTemplate(context.Background(), req.TemplateID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Template %d not found: %v", req.TemplateID, err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify template belongs to the requested collection and site
|
||||
if selectedTemplate.CollectionID != req.CollectionID || selectedTemplate.SiteID != req.SiteID {
|
||||
http.Error(w, fmt.Sprintf("Template %d not found in collection %s", req.TemplateID, req.CollectionID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Use engine's unified collection item creation
|
||||
createdItem, err := h.engine.CreateCollectionItemFromTemplate(
|
||||
req.SiteID,
|
||||
req.CollectionID,
|
||||
req.TemplateID,
|
||||
selectedTemplate.HTMLTemplate,
|
||||
userInfo.ID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -487,10 +525,10 @@ func (h *ContentHandler) RegisterRoutes(r chi.Router) {
|
||||
// COLLECTION MANAGEMENT - Groups of related content
|
||||
// =============================================================================
|
||||
r.Route("/collections", func(r chi.Router) {
|
||||
// Public routes
|
||||
r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X
|
||||
r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X
|
||||
r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X
|
||||
r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X
|
||||
r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X
|
||||
r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X
|
||||
r.Get("/{id}/templates", h.GetCollectionTemplates) // GET /api/collections/{id}/templates?site_id=X
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
|
||||
@@ -192,6 +192,10 @@ func (c *HTTPClient) GetCollectionTemplates(ctx context.Context, siteID, collect
|
||||
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
|
||||
}
|
||||
|
||||
func (c *HTTPClient) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
|
||||
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
|
||||
}
|
||||
|
||||
func (c *HTTPClient) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
|
||||
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
|
||||
}
|
||||
|
||||
@@ -214,6 +214,24 @@ func (r *PostgreSQLRepository) GetCollectionTemplates(ctx context.Context, siteI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCollectionTemplate retrieves a single template by ID
|
||||
func (r *PostgreSQLRepository) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
|
||||
template, err := r.queries.GetCollectionTemplate(ctx, int32(templateID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &CollectionTemplateItem{
|
||||
TemplateID: int(template.TemplateID),
|
||||
CollectionID: template.CollectionID,
|
||||
SiteID: template.SiteID,
|
||||
Name: template.Name,
|
||||
HTMLTemplate: template.HtmlTemplate,
|
||||
IsDefault: template.IsDefault, // PostgreSQL uses BOOLEAN
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateCollectionItem creates a new collection item
|
||||
func (r *PostgreSQLRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
|
||||
item, err := r.queries.CreateCollectionItem(ctx, postgresql.CreateCollectionItemParams{
|
||||
|
||||
@@ -18,6 +18,7 @@ type ContentRepository interface {
|
||||
CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error)
|
||||
GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error)
|
||||
GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error)
|
||||
GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error)
|
||||
CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error)
|
||||
CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error)
|
||||
CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error)
|
||||
|
||||
@@ -219,6 +219,24 @@ func (r *SQLiteRepository) GetCollectionTemplates(ctx context.Context, siteID, c
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCollectionTemplate retrieves a single template by ID
|
||||
func (r *SQLiteRepository) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
|
||||
template, err := r.queries.GetCollectionTemplate(ctx, int64(templateID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &CollectionTemplateItem{
|
||||
TemplateID: int(template.TemplateID),
|
||||
CollectionID: template.CollectionID,
|
||||
SiteID: template.SiteID,
|
||||
Name: template.Name,
|
||||
HTMLTemplate: template.HtmlTemplate,
|
||||
IsDefault: template.IsDefault != 0, // SQLite uses INTEGER for boolean
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateCollectionItem creates a new collection item
|
||||
func (r *SQLiteRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
|
||||
item, err := r.queries.CreateCollectionItem(ctx, sqlite.CreateCollectionItemParams{
|
||||
|
||||
@@ -3,7 +3,9 @@ package engine
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/insertr/insertr/internal/db"
|
||||
"golang.org/x/net/html"
|
||||
@@ -79,21 +81,35 @@ func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.No
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create templates for each unique child structure
|
||||
// Create templates for each unique child structure and styling (deduplicated)
|
||||
seenTemplates := make(map[string]int) // templateSignature -> templateID
|
||||
templateIndex := 0
|
||||
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
|
||||
if child.Type == html.ElementNode {
|
||||
templateName := fmt.Sprintf("template_%d", templateIndex+1)
|
||||
templateHTML := e.extractCleanTemplate(child)
|
||||
isDefault := templateIndex == 0
|
||||
templateSignature := e.generateTemplateSignature(child)
|
||||
|
||||
template, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, templateName, templateHTML, isDefault)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create template %s: %w", templateName, err)
|
||||
// Check if we've already seen this exact template structure + styling
|
||||
if existingTemplateID, exists := seenTemplates[templateSignature]; exists {
|
||||
// Reuse existing template
|
||||
templateIDs = append(templateIDs, existingTemplateID)
|
||||
fmt.Printf("✅ Reusing existing template for identical structure+styling in collection %s\n", collectionID)
|
||||
} else {
|
||||
// Create new template for unique structure+styling combination
|
||||
templateName := e.generateTemplateNameFromSignature(child, templateIndex+1)
|
||||
isDefault := templateIndex == 0
|
||||
|
||||
template, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, templateName, templateHTML, isDefault)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create template %s: %w", templateName, err)
|
||||
}
|
||||
|
||||
// Store the mapping and append to results
|
||||
seenTemplates[templateSignature] = template.TemplateID
|
||||
templateIDs = append(templateIDs, template.TemplateID)
|
||||
templateIndex++
|
||||
fmt.Printf("✅ Created new template '%s' for collection %s\n", templateName, collectionID)
|
||||
}
|
||||
templateIDs = append(templateIDs, template.TemplateID)
|
||||
templateIndex++
|
||||
fmt.Printf("✅ Created template '%s' for collection %s\n", templateName, collectionID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +228,8 @@ func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, s
|
||||
// Walk through the child element and find .insertr elements
|
||||
e.walkNodes(childElement, func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
|
||||
// Generate content ID for this .insertr element
|
||||
contentID := e.idGenerator.Generate(n, "collection-item")
|
||||
// Generate content ID for this .insertr element, including item ID for uniqueness
|
||||
contentID := e.idGenerator.Generate(n, fmt.Sprintf("%s-content", itemID))
|
||||
|
||||
// Extract the content
|
||||
htmlContent := e.extractHTMLContent(n)
|
||||
@@ -312,8 +328,9 @@ func (e *ContentEngine) CreateCollectionItemFromTemplate(
|
||||
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")
|
||||
// Generate unique item ID using unified generator with collection context + timestamp for uniqueness
|
||||
baseID := e.idGenerator.Generate(virtualElement, "collection-item")
|
||||
itemID := fmt.Sprintf("%s-%d", baseID, time.Now().UnixNano()%1000000) // Add 6-digit unique suffix
|
||||
|
||||
// Process any .insertr elements within the template and store as content
|
||||
contentEntries, err := e.processChildElementsAsContent(virtualElement, siteID, itemID)
|
||||
@@ -456,3 +473,95 @@ func (e *ContentEngine) cloneNode(node *html.Node) *html.Node {
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
// generateTemplateSignature creates a unique signature for template comparison
|
||||
// This combines structural HTML + class-based styling differences
|
||||
func (e *ContentEngine) generateTemplateSignature(element *html.Node) string {
|
||||
// Get the clean template HTML (structure)
|
||||
structuralHTML := e.extractCleanTemplate(element)
|
||||
|
||||
// Extract class-based styling signature
|
||||
stylingSignature := e.extractClassSignature(element)
|
||||
|
||||
// Combine both for a unique signature
|
||||
return fmt.Sprintf("%s|%s", structuralHTML, stylingSignature)
|
||||
}
|
||||
|
||||
// extractClassSignature recursively extracts and normalizes class attributes
|
||||
func (e *ContentEngine) extractClassSignature(element *html.Node) string {
|
||||
var signature strings.Builder
|
||||
|
||||
e.walkNodes(element, func(n *html.Node) {
|
||||
if n.Type == html.ElementNode {
|
||||
// Get classes for this element
|
||||
classes := GetClasses(n)
|
||||
if len(classes) > 0 {
|
||||
// Sort classes for consistent comparison
|
||||
sortedClasses := make([]string, len(classes))
|
||||
copy(sortedClasses, classes)
|
||||
sort.Strings(sortedClasses)
|
||||
|
||||
// Add to signature: element[class1,class2,...]
|
||||
signature.WriteString(fmt.Sprintf("%s[%s];", n.Data, strings.Join(sortedClasses, ",")))
|
||||
} else {
|
||||
// Element with no classes
|
||||
signature.WriteString(fmt.Sprintf("%s[];", n.Data))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return signature.String()
|
||||
}
|
||||
|
||||
// generateTemplateNameFromSignature creates human-readable template names
|
||||
func (e *ContentEngine) generateTemplateNameFromSignature(element *html.Node, fallbackIndex int) string {
|
||||
// Extract root element classes for naming
|
||||
rootClasses := GetClasses(element)
|
||||
|
||||
if len(rootClasses) > 0 {
|
||||
// Find distinctive classes (exclude common structural and base classes)
|
||||
var distinctiveClasses []string
|
||||
commonClasses := map[string]bool{
|
||||
"insertr": true, "insertr-add": true,
|
||||
// Common base classes that don't indicate variants
|
||||
"testimonial-item": true, "card": true, "item": true, "post": true,
|
||||
"container": true, "wrapper": true, "content": true,
|
||||
}
|
||||
|
||||
for _, class := range rootClasses {
|
||||
if !commonClasses[class] {
|
||||
distinctiveClasses = append(distinctiveClasses, class)
|
||||
}
|
||||
}
|
||||
|
||||
if len(distinctiveClasses) > 0 {
|
||||
// Use distinctive classes for naming
|
||||
name := strings.Join(distinctiveClasses, "_")
|
||||
// Capitalize and clean up
|
||||
name = strings.ReplaceAll(name, "-", "_")
|
||||
if len(name) > 20 {
|
||||
name = name[:20]
|
||||
}
|
||||
return strings.Title(strings.ToLower(name))
|
||||
} else if len(rootClasses) > 1 {
|
||||
// If only common classes, use the last non-insertr class
|
||||
for i := len(rootClasses) - 1; i >= 0; i-- {
|
||||
if rootClasses[i] != "insertr" && rootClasses[i] != "insertr-add" {
|
||||
name := strings.ReplaceAll(rootClasses[i], "-", "_")
|
||||
return strings.Title(strings.ToLower(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to numbered template
|
||||
return fmt.Sprintf("template_%d", fallbackIndex)
|
||||
}
|
||||
|
||||
// min returns the smaller of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -242,6 +242,29 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available templates for a collection
|
||||
* @param {string} collectionId - Collection ID
|
||||
* @returns {Promise<Array>} Array of collection templates
|
||||
*/
|
||||
async getCollectionTemplates(collectionId) {
|
||||
try {
|
||||
const collectionsUrl = this.getCollectionsUrl();
|
||||
const response = await fetch(`${collectionsUrl}/${collectionId}/templates?site_id=${this.siteId}`);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
return result.templates || [];
|
||||
} else {
|
||||
console.warn(`⚠️ Failed to fetch collection templates (${response.status}): ${collectionId}`);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch collection templates:', collectionId, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder collection items in bulk
|
||||
* @param {string} collectionId - Collection ID
|
||||
|
||||
@@ -28,6 +28,7 @@ export class CollectionManager {
|
||||
this.template = null;
|
||||
this.items = [];
|
||||
this.isActive = false;
|
||||
this.cachedTemplates = null; // Cache for available templates
|
||||
|
||||
// UI elements
|
||||
this.addButton = null;
|
||||
@@ -411,17 +412,31 @@ export class CollectionManager {
|
||||
}
|
||||
|
||||
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);
|
||||
// 1. Get available templates for this collection
|
||||
const templates = await this.getAvailableTemplates();
|
||||
if (!templates || templates.length === 0) {
|
||||
console.error('❌ No templates available for collection:', this.collectionId);
|
||||
alert('No templates available for this collection. Please refresh the page.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Select template (auto-select if only one, otherwise present options)
|
||||
const selectedTemplate = await this.selectTemplate(templates);
|
||||
if (!selectedTemplate) {
|
||||
console.log('Template selection cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Create collection item in database first (backend-first approach)
|
||||
const collectionItem = await this.apiClient.createCollectionItem(this.collectionId, selectedTemplate.template_id);
|
||||
|
||||
// 2. Create DOM element from the returned collection item data
|
||||
// 4. Create DOM element from the returned collection item data
|
||||
const newItem = this.createItemFromCollectionData(collectionItem);
|
||||
|
||||
// 3. Add to DOM
|
||||
// 5. Add to DOM
|
||||
this.container.insertBefore(newItem, this.addButton);
|
||||
|
||||
// 4. Update items array with backend data
|
||||
// 6. Update items array with backend data
|
||||
const newItemData = {
|
||||
element: newItem,
|
||||
index: this.items.length,
|
||||
@@ -430,16 +445,16 @@ export class CollectionManager {
|
||||
};
|
||||
this.items.push(newItemData);
|
||||
|
||||
// 5. Add controls to new item
|
||||
// 7. Add controls to new item
|
||||
this.addItemControls(newItem, this.items.length - 1);
|
||||
|
||||
// 6. Re-initialize any .insertr elements in the new item
|
||||
// 8. Re-initialize any .insertr elements in the new item
|
||||
this.initializeInsertrElements(newItem);
|
||||
|
||||
// 7. Update all item controls (indices may have changed)
|
||||
// 9. Update all item controls (indices may have changed)
|
||||
this.updateAllItemControls();
|
||||
|
||||
// 8. Trigger site enhancement to update static files
|
||||
// 10. Trigger site enhancement to update static files
|
||||
await this.apiClient.enhanceSite();
|
||||
|
||||
console.log('✅ New item added successfully:', collectionItem.item_id);
|
||||
@@ -448,6 +463,190 @@ export class CollectionManager {
|
||||
alert('Failed to add new item. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available templates for this collection
|
||||
* @returns {Promise<Array>} Array of template objects
|
||||
*/
|
||||
async getAvailableTemplates() {
|
||||
try {
|
||||
if (!this.cachedTemplates) {
|
||||
console.log('🔍 Fetching templates for collection:', this.collectionId);
|
||||
this.cachedTemplates = await this.apiClient.getCollectionTemplates(this.collectionId);
|
||||
console.log('📋 Templates fetched:', this.cachedTemplates);
|
||||
}
|
||||
return this.cachedTemplates;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch templates for collection:', this.collectionId, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a template for creating new items
|
||||
* @param {Array} templates - Available templates
|
||||
* @returns {Promise<Object|null>} Selected template or null if cancelled
|
||||
*/
|
||||
async selectTemplate(templates) {
|
||||
// Auto-select if only one template
|
||||
if (templates.length === 1) {
|
||||
console.log('🎯 Auto-selecting single template:', templates[0].name);
|
||||
return templates[0];
|
||||
}
|
||||
|
||||
// Present selection UI for multiple templates
|
||||
console.log('🎨 Multiple templates available, showing selection UI');
|
||||
return this.showTemplateSelectionModal(templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show template selection modal
|
||||
* @param {Array} templates - Available templates
|
||||
* @returns {Promise<Object|null>} Selected template or null if cancelled
|
||||
*/
|
||||
async showTemplateSelectionModal(templates) {
|
||||
return new Promise((resolve) => {
|
||||
// Create modal overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'insertr-modal-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999999;
|
||||
`;
|
||||
|
||||
// Create modal content
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'insertr-template-selector';
|
||||
modal.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
|
||||
// Create modal HTML
|
||||
modal.innerHTML = `
|
||||
<h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600;">Choose Template</h3>
|
||||
<div class="template-options">
|
||||
${templates.map(template => `
|
||||
<div class="template-option" data-template-id="${template.template_id}" style="
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
${template.is_default ? 'border-color: #3b82f6; background: #eff6ff;' : ''}
|
||||
">
|
||||
<div style="font-weight: 500; margin-bottom: 4px;">
|
||||
${template.name}${template.is_default ? ' (default)' : ''}
|
||||
</div>
|
||||
<div style="font-size: 14px; color: #6b7280; font-family: monospace; background: #f9fafb; padding: 8px; border-radius: 4px; overflow: hidden;">
|
||||
${this.truncateHtml(template.html_template, 100)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
|
||||
<button class="cancel-btn" style="
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
">Cancel</button>
|
||||
<button class="select-btn" style="
|
||||
padding: 8px 16px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
" disabled>Add Item</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let selectedTemplate = null;
|
||||
|
||||
// Add event listeners
|
||||
modal.querySelectorAll('.template-option').forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
// Remove previous selection
|
||||
modal.querySelectorAll('.template-option').forEach(opt => {
|
||||
opt.style.borderColor = '#e5e7eb';
|
||||
opt.style.background = 'white';
|
||||
});
|
||||
|
||||
// Apply selection styling
|
||||
option.style.borderColor = '#3b82f6';
|
||||
option.style.background = '#eff6ff';
|
||||
|
||||
// Find selected template
|
||||
const templateId = parseInt(option.dataset.templateId);
|
||||
selectedTemplate = templates.find(t => t.template_id === templateId);
|
||||
|
||||
// Enable select button
|
||||
const selectBtn = modal.querySelector('.select-btn');
|
||||
selectBtn.disabled = false;
|
||||
selectBtn.style.opacity = '1';
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-select default template after all event listeners are set up
|
||||
const defaultTemplate = templates.find(t => t.is_default);
|
||||
if (defaultTemplate) {
|
||||
const defaultOption = modal.querySelector(`[data-template-id="${defaultTemplate.template_id}"]`);
|
||||
if (defaultOption) {
|
||||
defaultOption.click();
|
||||
}
|
||||
}
|
||||
|
||||
modal.querySelector('.cancel-btn').addEventListener('click', () => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
modal.querySelector('.select-btn').addEventListener('click', () => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve(selectedTemplate);
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
document.body.removeChild(overlay);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate HTML for preview
|
||||
* @param {string} html - HTML string
|
||||
* @param {number} maxLength - Maximum character length
|
||||
* @returns {string} Truncated HTML
|
||||
*/
|
||||
truncateHtml(html, maxLength) {
|
||||
if (html.length <= maxLength) return html;
|
||||
return html.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DOM element from collection item data returned by backend
|
||||
|
||||
Reference in New Issue
Block a user