This commit addresses multiple collection management issues to improve user experience: ## Template Selection Modal Improvements - Replace inline styles with CSS classes for reliable visual feedback - Fix default template selection conflicts that showed multiple templates as selected - Add styled template previews that show actual CSS styling differences - Improve modal responsiveness and visual hierarchy ## Collection Item Creation Fixes - Fix empty collection items with no content/height that were unclickable - Preserve template placeholder content during item creation instead of clearing it - Implement proper positioning system using GetMaxPosition to place new items at collection end - Add position calculation logic to prevent new items from jumping to beginning ## Backend Positioning System - Add GetMaxPosition method to all repository implementations (SQLite, PostgreSQL, HTTPClient) - Update CreateCollectionItemFromTemplate to calculate correct position (maxPos + 1) - Maintain reconstruction ordering by position ASC for consistent item placement ## Frontend Template Selection - CSS class-based selection states replace problematic inline style manipulation - Template previews now render actual HTML with real page styling - Improved hover states and selection visual feedback - Fixed auto-selection interference with user interaction These changes ensure collection items appear in expected order and template selection provides clear visual feedback with actual styling previews.
369 lines
12 KiB
Go
369 lines
12 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/insertr/insertr/internal/db/postgresql"
|
|
)
|
|
|
|
// PostgreSQLRepository implements ContentRepository for PostgreSQL databases
|
|
type PostgreSQLRepository struct {
|
|
queries *postgresql.Queries
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewPostgreSQLRepository creates a new PostgreSQL repository
|
|
func NewPostgreSQLRepository(db *sql.DB) *PostgreSQLRepository {
|
|
return &PostgreSQLRepository{
|
|
queries: postgresql.New(db),
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// GetContent retrieves a single content item
|
|
func (r *PostgreSQLRepository) GetContent(ctx context.Context, siteID, contentID string) (*ContentItem, error) {
|
|
content, err := r.queries.GetContent(ctx, postgresql.GetContentParams{
|
|
ID: contentID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// GetBulkContent retrieves multiple content items
|
|
func (r *PostgreSQLRepository) GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error) {
|
|
contents, err := r.queries.GetBulkContent(ctx, postgresql.GetBulkContentParams{
|
|
SiteID: siteID,
|
|
Ids: contentIDs,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make(map[string]ContentItem)
|
|
for _, content := range contents {
|
|
items[content.ID] = ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetAllContent retrieves all content items for a site
|
|
func (r *PostgreSQLRepository) GetAllContent(ctx context.Context, siteID string) (map[string]ContentItem, error) {
|
|
contents, err := r.queries.GetAllContent(ctx, siteID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make(map[string]ContentItem)
|
|
for _, content := range contents {
|
|
items[content.ID] = ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// CreateContent creates a new content item
|
|
func (r *PostgreSQLRepository) CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) {
|
|
content, err := r.queries.CreateContent(ctx, postgresql.CreateContentParams{
|
|
ID: contentID,
|
|
SiteID: siteID,
|
|
HtmlContent: htmlContent,
|
|
OriginalTemplate: ToNullString(originalTemplate),
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// GetCollection retrieves a collection container
|
|
func (r *PostgreSQLRepository) GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error) {
|
|
collection, err := r.queries.GetCollection(ctx, postgresql.GetCollectionParams{
|
|
ID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionItem{
|
|
ID: collection.ID,
|
|
SiteID: collection.SiteID,
|
|
ContainerHTML: collection.ContainerHtml,
|
|
LastEditedBy: collection.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// CreateCollection creates a new collection container
|
|
func (r *PostgreSQLRepository) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) {
|
|
collection, err := r.queries.CreateCollection(ctx, postgresql.CreateCollectionParams{
|
|
ID: collectionID,
|
|
SiteID: siteID,
|
|
ContainerHtml: containerHTML,
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionItem{
|
|
ID: collection.ID,
|
|
SiteID: collection.SiteID,
|
|
ContainerHTML: collection.ContainerHtml,
|
|
LastEditedBy: collection.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// GetCollectionItems retrieves all items in a collection with template information
|
|
func (r *PostgreSQLRepository) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error) {
|
|
items, err := r.queries.GetCollectionItemsWithTemplate(ctx, postgresql.GetCollectionItemsWithTemplateParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]CollectionItemWithTemplate, len(items))
|
|
for i, item := range items {
|
|
result[i] = CollectionItemWithTemplate{
|
|
ItemID: item.ItemID,
|
|
CollectionID: item.CollectionID,
|
|
SiteID: item.SiteID,
|
|
TemplateID: int(item.TemplateID),
|
|
HTMLContent: item.HtmlContent,
|
|
Position: int(item.Position),
|
|
LastEditedBy: item.LastEditedBy,
|
|
TemplateName: item.TemplateName,
|
|
HTMLTemplate: item.HtmlTemplate,
|
|
IsDefault: item.IsDefault, // PostgreSQL uses BOOLEAN
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// CreateCollectionTemplate creates a new template for a collection
|
|
func (r *PostgreSQLRepository) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) {
|
|
template, err := r.queries.CreateCollectionTemplate(ctx, postgresql.CreateCollectionTemplateParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
Name: name,
|
|
HtmlTemplate: htmlTemplate,
|
|
IsDefault: isDefault,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionTemplateItem{
|
|
TemplateID: int(template.TemplateID),
|
|
CollectionID: template.CollectionID,
|
|
SiteID: template.SiteID,
|
|
Name: template.Name,
|
|
HTMLTemplate: template.HtmlTemplate,
|
|
IsDefault: template.IsDefault,
|
|
}, nil
|
|
}
|
|
|
|
// GetCollectionTemplates retrieves all templates for a collection
|
|
func (r *PostgreSQLRepository) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error) {
|
|
templates, err := r.queries.GetCollectionTemplates(ctx, postgresql.GetCollectionTemplatesParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]CollectionTemplateItem, len(templates))
|
|
for i, template := range templates {
|
|
result[i] = 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
|
|
}
|
|
|
|
// 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{
|
|
ItemID: itemID,
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
TemplateID: int32(templateID),
|
|
HtmlContent: htmlContent,
|
|
Position: int32(position),
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionItemWithTemplate{
|
|
ItemID: item.ItemID,
|
|
CollectionID: item.CollectionID,
|
|
SiteID: item.SiteID,
|
|
TemplateID: int(item.TemplateID),
|
|
HTMLContent: item.HtmlContent,
|
|
Position: int(item.Position),
|
|
LastEditedBy: item.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// CreateCollectionItemAtomic creates a collection item with all its content entries atomically
|
|
func (r *PostgreSQLRepository) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
|
|
// Get template HTML for processing
|
|
templates, err := r.GetCollectionTemplates(ctx, siteID, collectionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get templates: %w", err)
|
|
}
|
|
|
|
var templateHTML string
|
|
for _, template := range templates {
|
|
if template.TemplateID == templateID {
|
|
templateHTML = template.HTMLTemplate
|
|
break
|
|
}
|
|
}
|
|
|
|
if templateHTML == "" {
|
|
return nil, fmt.Errorf("template %d not found", templateID)
|
|
}
|
|
|
|
// TODO: Implement using unified engine approach
|
|
// This requires circular dependency resolution
|
|
return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL")
|
|
}
|
|
|
|
// GetMaxPosition returns the maximum position for items in a collection
|
|
func (r *PostgreSQLRepository) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) {
|
|
result, err := r.queries.GetMaxPosition(ctx, postgresql.GetMaxPositionParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Convert interface{} to int (PostgreSQL returns int64)
|
|
if maxPos, ok := result.(int64); ok {
|
|
return int(maxPos), nil
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
// UpdateContent updates an existing content item
|
|
func (r *PostgreSQLRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
|
|
content, err := r.queries.UpdateContent(ctx, postgresql.UpdateContentParams{
|
|
HtmlContent: htmlContent,
|
|
LastEditedBy: lastEditedBy,
|
|
ID: contentID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: FromNullString(content.OriginalTemplate),
|
|
UpdatedAt: fmt.Sprintf("%d", content.UpdatedAt),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// ReorderCollectionItems reorders collection items in bulk
|
|
func (r *PostgreSQLRepository) ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error {
|
|
// Use transaction for atomic bulk updates
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
qtx := r.queries.WithTx(tx)
|
|
for _, item := range items {
|
|
err = qtx.UpdateCollectionItemPosition(ctx, postgresql.UpdateCollectionItemPositionParams{
|
|
ItemID: item.ItemID,
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
Position: int32(item.Position),
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update position for item %s: %w", item.ItemID, err)
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// WithTransaction executes a function within a database transaction
|
|
func (r *PostgreSQLRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txRepo := &PostgreSQLRepository{
|
|
queries: r.queries.WithTx(tx),
|
|
db: r.db,
|
|
}
|
|
|
|
if err := fn(txRepo); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|