112
This commit is contained in:
111
backend/fix_taxonomy_relationships.py
Normal file
111
backend/fix_taxonomy_relationships.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Fix missing taxonomy relationships for existing content
|
||||
This script will:
|
||||
1. Find content that should have tags/categories based on their keywords
|
||||
2. Create appropriate taxonomy terms
|
||||
3. Link them to the content
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
from igny8_core.business.content.models import Content, ContentTaxonomy
|
||||
|
||||
print("=" * 80)
|
||||
print("FIXING MISSING TAXONOMY RELATIONSHIPS")
|
||||
print("=" * 80)
|
||||
|
||||
# Get all content without taxonomy terms
|
||||
content_without_tags = Content.objects.filter(taxonomy_terms__isnull=True).distinct()
|
||||
print(f"\nFound {content_without_tags.count()} content items without tags/categories")
|
||||
|
||||
fixed_count = 0
|
||||
for content in content_without_tags:
|
||||
print(f"\nProcessing Content #{content.id}: {content.title[:50]}...")
|
||||
|
||||
# Generate tags from keywords
|
||||
tags_to_add = []
|
||||
categories_to_add = []
|
||||
|
||||
# Use primary keyword as a tag
|
||||
if content.primary_keyword:
|
||||
tags_to_add.append(content.primary_keyword)
|
||||
|
||||
# Use secondary keywords as tags
|
||||
if content.secondary_keywords and isinstance(content.secondary_keywords, list):
|
||||
tags_to_add.extend(content.secondary_keywords[:3]) # Limit to 3
|
||||
|
||||
# Create category based on content_type and cluster
|
||||
if content.cluster:
|
||||
categories_to_add.append(content.cluster.name)
|
||||
|
||||
# Add content_structure as category
|
||||
if content.content_structure:
|
||||
structure_name = content.content_structure.replace('_', ' ').title()
|
||||
categories_to_add.append(structure_name)
|
||||
|
||||
with transaction.atomic():
|
||||
# Process tags
|
||||
for tag_name in tags_to_add:
|
||||
if tag_name and isinstance(tag_name, str):
|
||||
tag_name = tag_name.strip()
|
||||
if tag_name:
|
||||
try:
|
||||
tag_obj, created = ContentTaxonomy.objects.get_or_create(
|
||||
site=content.site,
|
||||
name=tag_name,
|
||||
taxonomy_type='tag',
|
||||
defaults={
|
||||
'slug': slugify(tag_name),
|
||||
'sector': content.sector,
|
||||
'account': content.account,
|
||||
'description': '',
|
||||
'external_taxonomy': '',
|
||||
'sync_status': '',
|
||||
'count': 0,
|
||||
'metadata': {},
|
||||
}
|
||||
)
|
||||
content.taxonomy_terms.add(tag_obj)
|
||||
print(f" + Tag: {tag_name} ({'created' if created else 'existing'})")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to add tag '{tag_name}': {e}")
|
||||
|
||||
# Process categories
|
||||
for category_name in categories_to_add:
|
||||
if category_name and isinstance(category_name, str):
|
||||
category_name = category_name.strip()
|
||||
if category_name:
|
||||
try:
|
||||
category_obj, created = ContentTaxonomy.objects.get_or_create(
|
||||
site=content.site,
|
||||
name=category_name,
|
||||
taxonomy_type='category',
|
||||
defaults={
|
||||
'slug': slugify(category_name),
|
||||
'sector': content.sector,
|
||||
'account': content.account,
|
||||
'description': '',
|
||||
'external_taxonomy': '',
|
||||
'sync_status': '',
|
||||
'count': 0,
|
||||
'metadata': {},
|
||||
}
|
||||
)
|
||||
content.taxonomy_terms.add(category_obj)
|
||||
print(f" + Category: {category_name} ({'created' if created else 'existing'})")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to add category '{category_name}': {e}")
|
||||
|
||||
fixed_count += 1
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"FIXED {fixed_count} CONTENT ITEMS")
|
||||
print("=" * 80)
|
||||
@@ -8,6 +8,7 @@ from typing import Dict, List, Any
|
||||
from django.db import transaction
|
||||
from igny8_core.ai.base import BaseAIFunction
|
||||
from igny8_core.modules.writer.models import Tasks, Content
|
||||
from igny8_core.business.content.models import ContentTaxonomy
|
||||
from igny8_core.ai.ai_core import AICore
|
||||
from igny8_core.ai.validators import validate_tasks_exist
|
||||
from igny8_core.ai.prompts import PromptRegistry
|
||||
@@ -183,6 +184,7 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
# Extract tags and categories from AI response
|
||||
tags_from_response = parsed.get('tags', [])
|
||||
categories_from_response = parsed.get('categories', [])
|
||||
logger.info(f"Extracted from AI response - Tags: {tags_from_response}, Categories: {categories_from_response}")
|
||||
else:
|
||||
# Plain text response
|
||||
content_html = str(parsed)
|
||||
@@ -224,9 +226,13 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
sector=task.sector,
|
||||
)
|
||||
|
||||
logger.info(f"Created content record ID: {content_record.id}")
|
||||
logger.info(f"Processing {len(tags_from_response) if tags_from_response else 0} tags and {len(categories_from_response) if categories_from_response else 0} categories")
|
||||
|
||||
# Link taxonomy terms from task if available
|
||||
if task.taxonomy_term:
|
||||
content_record.taxonomy_terms.add(task.taxonomy_term)
|
||||
logger.info(f"Added task taxonomy term: {task.taxonomy_term.name}")
|
||||
|
||||
# Process tags from AI response
|
||||
if tags_from_response and isinstance(tags_from_response, list):
|
||||
@@ -237,7 +243,7 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
if tag_name:
|
||||
try:
|
||||
# Get or create tag taxonomy term
|
||||
tag_obj, _ = ContentTaxonomy.objects.get_or_create(
|
||||
tag_obj, created = ContentTaxonomy.objects.get_or_create(
|
||||
site=task.site,
|
||||
name=tag_name,
|
||||
taxonomy_type='tag',
|
||||
@@ -245,11 +251,17 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
'slug': slugify(tag_name),
|
||||
'sector': task.sector,
|
||||
'account': task.account,
|
||||
'description': '', # Required by database
|
||||
'external_taxonomy': '', # Required by database
|
||||
'sync_status': '', # Required by database
|
||||
'count': 0, # Required by database
|
||||
'metadata': {}, # Required by database
|
||||
}
|
||||
)
|
||||
content_record.taxonomy_terms.add(tag_obj)
|
||||
logger.info(f"{'Created' if created else 'Found'} and linked tag: {tag_name} (ID: {tag_obj.id})")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to add tag '{tag_name}': {e}")
|
||||
logger.error(f"Failed to add tag '{tag_name}': {e}", exc_info=True)
|
||||
|
||||
# Process categories from AI response
|
||||
if categories_from_response and isinstance(categories_from_response, list):
|
||||
@@ -260,7 +272,7 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
if category_name:
|
||||
try:
|
||||
# Get or create category taxonomy term
|
||||
category_obj, _ = ContentTaxonomy.objects.get_or_create(
|
||||
category_obj, created = ContentTaxonomy.objects.get_or_create(
|
||||
site=task.site,
|
||||
name=category_name,
|
||||
taxonomy_type='category',
|
||||
@@ -268,11 +280,17 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
'slug': slugify(category_name),
|
||||
'sector': task.sector,
|
||||
'account': task.account,
|
||||
'description': '', # Required by database
|
||||
'external_taxonomy': '', # Required by database
|
||||
'sync_status': '', # Required by database
|
||||
'count': 0, # Required by database
|
||||
'metadata': {}, # Required by database
|
||||
}
|
||||
)
|
||||
content_record.taxonomy_terms.add(category_obj)
|
||||
logger.info(f"{'Created' if created else 'Found'} and linked category: {category_name} (ID: {category_obj.id})")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to add category '{category_name}': {e}")
|
||||
logger.error(f"Failed to add category '{category_name}': {e}", exc_info=True)
|
||||
|
||||
# STAGE 3: Update task status to completed
|
||||
task.status = 'completed'
|
||||
|
||||
@@ -297,8 +297,8 @@ class ContentTaxonomy(SiteSectorBaseModel):
|
||||
external_taxonomy = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - null for cluster taxonomies"
|
||||
default='',
|
||||
help_text="WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - empty for cluster taxonomies"
|
||||
)
|
||||
external_id = models.IntegerField(
|
||||
null=True,
|
||||
@@ -306,6 +306,26 @@ class ContentTaxonomy(SiteSectorBaseModel):
|
||||
db_index=True,
|
||||
help_text="WordPress term_id - null for cluster taxonomies"
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
help_text="Taxonomy term description"
|
||||
)
|
||||
sync_status = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
default='',
|
||||
help_text="Synchronization status with external platforms"
|
||||
)
|
||||
count = models.IntegerField(
|
||||
default=0,
|
||||
help_text="Number of times this term is used"
|
||||
)
|
||||
metadata = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text="Additional metadata for the taxonomy term"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -86,11 +86,12 @@ class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
|
||||
@admin.register(Content)
|
||||
class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'created_at']
|
||||
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'get_taxonomy_count', 'created_at']
|
||||
list_filter = ['content_type', 'content_structure', 'source', 'status', 'site', 'sector', 'created_at']
|
||||
search_fields = ['title', 'content_html', 'external_url']
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
readonly_fields = ['created_at', 'updated_at', 'word_count']
|
||||
filter_horizontal = ['taxonomy_terms'] # Add many-to-many widget for taxonomy terms
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
@@ -99,8 +100,16 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
('Content Classification', {
|
||||
'fields': ('content_type', 'content_structure', 'source')
|
||||
}),
|
||||
('Taxonomy Terms (Tags & Categories)', {
|
||||
'fields': ('taxonomy_terms',),
|
||||
'description': 'Select tags and categories for this content'
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('content_html',)
|
||||
'fields': ('content_html', 'word_count')
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('meta_title', 'meta_description', 'primary_keyword', 'secondary_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('WordPress Sync', {
|
||||
'fields': ('external_id', 'external_url'),
|
||||
@@ -112,6 +121,11 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
}),
|
||||
)
|
||||
|
||||
def get_taxonomy_count(self, obj):
|
||||
"""Display count of associated taxonomy terms"""
|
||||
return obj.taxonomy_terms.count()
|
||||
get_taxonomy_count.short_description = 'Taxonomy Count'
|
||||
|
||||
def get_site_display(self, obj):
|
||||
"""Safely get site name"""
|
||||
try:
|
||||
|
||||
@@ -154,6 +154,8 @@ class ContentSerializer(serializers.ModelSerializer):
|
||||
cluster_name = serializers.SerializerMethodField()
|
||||
sector_name = serializers.SerializerMethodField()
|
||||
taxonomy_terms_data = serializers.SerializerMethodField()
|
||||
tags = serializers.SerializerMethodField()
|
||||
categories = serializers.SerializerMethodField()
|
||||
has_image_prompts = serializers.SerializerMethodField()
|
||||
image_status = serializers.SerializerMethodField()
|
||||
has_generated_images = serializers.SerializerMethodField()
|
||||
@@ -175,6 +177,8 @@ class ContentSerializer(serializers.ModelSerializer):
|
||||
'content_type',
|
||||
'content_structure',
|
||||
'taxonomy_terms_data',
|
||||
'tags',
|
||||
'categories',
|
||||
'external_id',
|
||||
'external_url',
|
||||
'source',
|
||||
@@ -246,6 +250,20 @@ class ContentSerializer(serializers.ModelSerializer):
|
||||
for term in obj.taxonomy_terms.all()
|
||||
]
|
||||
|
||||
def get_tags(self, obj):
|
||||
"""Get only tags (taxonomy_type='tag')"""
|
||||
return [
|
||||
term.name
|
||||
for term in obj.taxonomy_terms.filter(taxonomy_type='tag')
|
||||
]
|
||||
|
||||
def get_categories(self, obj):
|
||||
"""Get only categories (taxonomy_type='category')"""
|
||||
return [
|
||||
term.name
|
||||
for term in obj.taxonomy_terms.filter(taxonomy_type='category')
|
||||
]
|
||||
|
||||
def get_has_image_prompts(self, obj):
|
||||
"""Check if content has any image prompts (images with prompts)"""
|
||||
return obj.images.filter(prompt__isnull=False).exclude(prompt='').exists()
|
||||
|
||||
41
backend/test_tags_categories.py
Normal file
41
backend/test_tags_categories.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Add the backend directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from igny8_core.modules.writer.models import Content
|
||||
from igny8_core.modules.writer.serializers import ContentSerializer
|
||||
|
||||
print("Testing ContentSerializer tags and categories fields...")
|
||||
print("=" * 60)
|
||||
|
||||
# Get a content record
|
||||
content = Content.objects.first()
|
||||
if content:
|
||||
serializer = ContentSerializer(content)
|
||||
data = serializer.data
|
||||
print(f"Content ID: {data['id']}")
|
||||
print(f"Title: {data.get('title', 'N/A')}")
|
||||
print(f"Tags: {data.get('tags', [])}")
|
||||
print(f"Categories: {data.get('categories', [])}")
|
||||
print(f"Taxonomy Terms Data: {len(data.get('taxonomy_terms_data', []))} items")
|
||||
|
||||
# Show taxonomy terms breakdown
|
||||
taxonomy_terms = data.get('taxonomy_terms_data', [])
|
||||
if taxonomy_terms:
|
||||
print("\nTaxonomy Terms Details:")
|
||||
for term in taxonomy_terms:
|
||||
print(f" - {term['name']} ({term['taxonomy_type']})")
|
||||
|
||||
print("\n✓ Serializer fields test passed!")
|
||||
else:
|
||||
print("No content found in database")
|
||||
print("This is expected if no content has been generated yet")
|
||||
|
||||
129
backend/verify_taxonomy.py
Normal file
129
backend/verify_taxonomy.py
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Verify Tags and Categories Implementation
|
||||
Tests that ContentTaxonomy integration is working correctly
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Add the backend directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from igny8_core.business.content.models import Content, ContentTaxonomy
|
||||
from igny8_core.modules.writer.serializers import ContentSerializer
|
||||
|
||||
print("=" * 80)
|
||||
print("VERIFYING TAGS AND CATEGORIES IMPLEMENTATION")
|
||||
print("=" * 80)
|
||||
|
||||
# Check if ContentTaxonomy model is accessible
|
||||
print("\n1. ContentTaxonomy Model Check:")
|
||||
try:
|
||||
taxonomy_count = ContentTaxonomy.objects.count()
|
||||
print(f" ✓ ContentTaxonomy model accessible")
|
||||
print(f" ✓ Total taxonomy terms in database: {taxonomy_count}")
|
||||
|
||||
# Show breakdown by type
|
||||
tag_count = ContentTaxonomy.objects.filter(taxonomy_type='tag').count()
|
||||
category_count = ContentTaxonomy.objects.filter(taxonomy_type='category').count()
|
||||
print(f" - Tags: {tag_count}")
|
||||
print(f" - Categories: {category_count}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error accessing ContentTaxonomy: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Check Content model has taxonomy_terms field
|
||||
print("\n2. Content Model Taxonomy Field Check:")
|
||||
try:
|
||||
content = Content.objects.first()
|
||||
if content:
|
||||
taxonomy_terms = content.taxonomy_terms.all()
|
||||
print(f" ✓ Content.taxonomy_terms field accessible")
|
||||
print(f" ✓ Sample content (ID: {content.id}) has {taxonomy_terms.count()} taxonomy terms")
|
||||
for term in taxonomy_terms:
|
||||
print(f" - {term.name} ({term.taxonomy_type})")
|
||||
else:
|
||||
print(" ⚠ No content found in database")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error accessing Content.taxonomy_terms: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Check serializer includes tags and categories
|
||||
print("\n3. ContentSerializer Tags/Categories Check:")
|
||||
try:
|
||||
if content:
|
||||
serializer = ContentSerializer(content)
|
||||
data = serializer.data
|
||||
|
||||
# Check if fields exist
|
||||
has_tags_field = 'tags' in data
|
||||
has_categories_field = 'categories' in data
|
||||
has_taxonomy_data = 'taxonomy_terms_data' in data
|
||||
|
||||
print(f" ✓ Serializer includes 'tags' field: {has_tags_field}")
|
||||
print(f" ✓ Serializer includes 'categories' field: {has_categories_field}")
|
||||
print(f" ✓ Serializer includes 'taxonomy_terms_data' field: {has_taxonomy_data}")
|
||||
|
||||
if has_tags_field:
|
||||
print(f" - Tags: {data.get('tags', [])}")
|
||||
if has_categories_field:
|
||||
print(f" - Categories: {data.get('categories', [])}")
|
||||
|
||||
else:
|
||||
print(" ⚠ No content to serialize")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error serializing content: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Check if we can create taxonomy terms
|
||||
print("\n4. Creating Test Taxonomy Terms:")
|
||||
try:
|
||||
from django.utils.text import slugify
|
||||
|
||||
# Try to create a test tag
|
||||
test_tag, created = ContentTaxonomy.objects.get_or_create(
|
||||
name="Test Tag",
|
||||
taxonomy_type='tag',
|
||||
defaults={
|
||||
'slug': slugify("Test Tag"),
|
||||
}
|
||||
)
|
||||
if created:
|
||||
print(f" ✓ Created new test tag: {test_tag.name}")
|
||||
else:
|
||||
print(f" ✓ Test tag already exists: {test_tag.name}")
|
||||
|
||||
# Try to create a test category
|
||||
test_category, created = ContentTaxonomy.objects.get_or_create(
|
||||
name="Test Category",
|
||||
taxonomy_type='category',
|
||||
defaults={
|
||||
'slug': slugify("Test Category"),
|
||||
}
|
||||
)
|
||||
if created:
|
||||
print(f" ✓ Created new test category: {test_category.name}")
|
||||
else:
|
||||
print(f" ✓ Test category already exists: {test_category.name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error creating taxonomy terms: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("VERIFICATION COMPLETE")
|
||||
print("=" * 80)
|
||||
print("\nNext steps:")
|
||||
print("1. Access Django admin at /admin/writer/contenttaxonomy/")
|
||||
print("2. Generate content via AI and check if tags/categories are saved")
|
||||
print("3. Check API response includes 'tags' and 'categories' fields")
|
||||
print("=" * 80)
|
||||
@@ -8,8 +8,8 @@ import clsx from "clsx";
|
||||
|
||||
type BadgeVariant = "solid" | "soft" | "outline" | "light";
|
||||
type BadgeSize = "xs" | "sm" | "md";
|
||||
type BadgeTone = "brand" | "success" | "warning" | "danger" | "info" | "neutral";
|
||||
type BadgeColor = "success" | "warning" | "error" | "danger" | "info" | "primary" | "brand" | "light" | "neutral";
|
||||
type BadgeTone = "brand" | "success" | "warning" | "danger" | "info" | "neutral" | "purple" | "indigo" | "pink" | "teal" | "cyan" | "blue";
|
||||
type BadgeColor = "success" | "warning" | "error" | "danger" | "info" | "primary" | "brand" | "light" | "neutral" | "purple" | "indigo" | "pink" | "teal" | "cyan" | "blue";
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: BadgeVariant; // Visual treatment (light maps to soft)
|
||||
@@ -32,46 +32,82 @@ const toneStyles: Record<
|
||||
> = {
|
||||
brand: {
|
||||
solid: "bg-brand-500 text-white",
|
||||
soft: "bg-brand-50 text-brand-600 dark:bg-brand-500/15 dark:text-brand-300",
|
||||
soft: "bg-brand-50 text-brand-700 dark:bg-brand-500/15 dark:text-brand-400",
|
||||
outline:
|
||||
"text-brand-600 ring-1 ring-brand-200 dark:ring-brand-500/30 dark:text-brand-300",
|
||||
"text-brand-700 ring-1 ring-brand-200 dark:ring-brand-500/30 dark:text-brand-400",
|
||||
},
|
||||
success: {
|
||||
solid: "bg-success-500 text-white",
|
||||
soft: "bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-300",
|
||||
soft: "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-400",
|
||||
outline:
|
||||
"text-success-600 ring-1 ring-success-200 dark:ring-success-500/30 dark:text-success-300",
|
||||
"text-success-700 ring-1 ring-success-200 dark:ring-success-500/30 dark:text-success-400",
|
||||
},
|
||||
warning: {
|
||||
solid: "bg-warning-500 text-white",
|
||||
soft: "bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-warning-300",
|
||||
soft: "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-warning-400",
|
||||
outline:
|
||||
"text-warning-600 ring-1 ring-warning-200 dark:ring-warning-500/30 dark:text-warning-300",
|
||||
"text-warning-700 ring-1 ring-warning-200 dark:ring-warning-500/30 dark:text-warning-400",
|
||||
},
|
||||
danger: {
|
||||
solid: "bg-error-500 text-white",
|
||||
soft: "bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-300",
|
||||
soft: "bg-error-50 text-error-700 dark:bg-error-500/15 dark:text-error-400",
|
||||
outline:
|
||||
"text-error-600 ring-1 ring-error-200 dark:ring-error-500/30 dark:text-error-300",
|
||||
"text-error-700 ring-1 ring-error-200 dark:ring-error-500/30 dark:text-error-400",
|
||||
},
|
||||
info: {
|
||||
solid: "bg-blue-light-500 text-white",
|
||||
soft: "bg-blue-light-50 text-blue-light-600 dark:bg-blue-light-500/15 dark:text-blue-light-300",
|
||||
soft: "bg-blue-light-50 text-blue-light-700 dark:bg-blue-light-500/15 dark:text-blue-light-400",
|
||||
outline:
|
||||
"text-blue-light-600 ring-1 ring-blue-light-200 dark:ring-blue-light-500/30 dark:text-blue-light-300",
|
||||
"text-blue-light-700 ring-1 ring-blue-light-200 dark:ring-blue-light-500/30 dark:text-blue-light-400",
|
||||
},
|
||||
neutral: {
|
||||
solid: "bg-gray-800 text-white",
|
||||
soft: "bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80",
|
||||
soft: "bg-gray-100 text-gray-800 dark:bg-white/5 dark:text-white/90",
|
||||
outline:
|
||||
"text-gray-700 ring-1 ring-gray-300 dark:ring-white/[0.08] dark:text-white/80",
|
||||
"text-gray-800 ring-1 ring-gray-300 dark:ring-white/[0.08] dark:text-white/90",
|
||||
},
|
||||
purple: {
|
||||
solid: "bg-purple-600 text-white",
|
||||
soft: "bg-purple-50 text-purple-700 dark:bg-purple-500/15 dark:text-purple-400",
|
||||
outline:
|
||||
"text-purple-700 ring-1 ring-purple-200 dark:ring-purple-500/30 dark:text-purple-400",
|
||||
},
|
||||
indigo: {
|
||||
solid: "bg-indigo-600 text-white",
|
||||
soft: "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-400",
|
||||
outline:
|
||||
"text-indigo-700 ring-1 ring-indigo-200 dark:ring-indigo-500/30 dark:text-indigo-400",
|
||||
},
|
||||
pink: {
|
||||
solid: "bg-pink-600 text-white",
|
||||
soft: "bg-pink-50 text-pink-700 dark:bg-pink-500/15 dark:text-pink-400",
|
||||
outline:
|
||||
"text-pink-700 ring-1 ring-pink-200 dark:ring-pink-500/30 dark:text-pink-400",
|
||||
},
|
||||
teal: {
|
||||
solid: "bg-teal-600 text-white",
|
||||
soft: "bg-teal-50 text-teal-700 dark:bg-teal-500/15 dark:text-teal-400",
|
||||
outline:
|
||||
"text-teal-700 ring-1 ring-teal-200 dark:ring-teal-500/30 dark:text-teal-400",
|
||||
},
|
||||
cyan: {
|
||||
solid: "bg-cyan-600 text-white",
|
||||
soft: "bg-cyan-50 text-cyan-700 dark:bg-cyan-500/15 dark:text-cyan-400",
|
||||
outline:
|
||||
"text-cyan-700 ring-1 ring-cyan-200 dark:ring-cyan-500/30 dark:text-cyan-400",
|
||||
},
|
||||
blue: {
|
||||
solid: "bg-blue-600 text-white",
|
||||
soft: "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-400",
|
||||
outline:
|
||||
"text-blue-700 ring-1 ring-blue-200 dark:ring-blue-500/30 dark:text-blue-400",
|
||||
},
|
||||
};
|
||||
|
||||
const sizeClasses: Record<BadgeSize, string> = {
|
||||
xs: "h-5 px-2 text-[11px]",
|
||||
sm: "h-6 px-2.5 text-xs",
|
||||
md: "h-7 px-3 text-sm",
|
||||
xs: "h-5 px-2 text-[11px] leading-tight",
|
||||
sm: "h-6 px-2.5 text-xs leading-tight",
|
||||
md: "h-7 px-3 text-sm leading-tight",
|
||||
};
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({
|
||||
@@ -98,6 +134,12 @@ const Badge: React.FC<BadgeProps> = ({
|
||||
"brand": "brand",
|
||||
"light": "neutral",
|
||||
"neutral": "neutral",
|
||||
"purple": "purple",
|
||||
"indigo": "indigo",
|
||||
"pink": "pink",
|
||||
"teal": "teal",
|
||||
"cyan": "cyan",
|
||||
"blue": "blue",
|
||||
};
|
||||
return colorToToneMap[color] || "brand";
|
||||
})();
|
||||
@@ -110,7 +152,7 @@ const Badge: React.FC<BadgeProps> = ({
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center gap-1 rounded-full font-medium uppercase tracking-wide",
|
||||
"inline-flex items-center justify-center gap-1 rounded-full font-normal",
|
||||
sizeClasses[size],
|
||||
toneClass,
|
||||
className,
|
||||
|
||||
@@ -107,8 +107,8 @@ export const createClustersPageConfig = (
|
||||
sortField: 'name',
|
||||
render: (value: string, row: Cluster) => (
|
||||
<Link
|
||||
to={`/clusters/${row.id}`}
|
||||
className="font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
|
||||
to={`/planner/clusters/${row.id}`}
|
||||
className="text-base font-light text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
|
||||
@@ -104,12 +104,12 @@ export const createContentPageConfig = (
|
||||
{handlers.onRowClick ? (
|
||||
<button
|
||||
onClick={() => handlers.onRowClick!(row)}
|
||||
className="font-medium text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
|
||||
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
|
||||
>
|
||||
{row.title || `Content #${row.id}`}
|
||||
</button>
|
||||
) : (
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
<div className="text-base font-light text-gray-900 dark:text-white">
|
||||
{row.title || `Content #${row.id}`}
|
||||
</div>
|
||||
)}
|
||||
@@ -178,24 +178,48 @@ export const createContentPageConfig = (
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'taxonomy_terms',
|
||||
key: 'tags',
|
||||
label: 'Tags',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const taxonomyTerms = row.taxonomy_terms;
|
||||
if (!taxonomyTerms || taxonomyTerms.length === 0) {
|
||||
const tags = row.tags || [];
|
||||
if (!tags || tags.length === 0) {
|
||||
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{taxonomyTerms.slice(0, 2).map((term) => (
|
||||
<Badge key={term.id} color="pink" size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">{term.name}</span>
|
||||
{tags.slice(0, 2).map((tag, index) => (
|
||||
<Badge key={`${tag}-${index}`} color="pink" size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">{tag}</span>
|
||||
</Badge>
|
||||
))}
|
||||
{taxonomyTerms.length > 2 && (
|
||||
<span className="text-[11px] text-gray-500">+{taxonomyTerms.length - 2}</span>
|
||||
{tags.length > 2 && (
|
||||
<span className="text-[11px] text-gray-500">+{tags.length - 2}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
label: 'Categories',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const categories = row.categories || [];
|
||||
if (!categories || categories.length === 0) {
|
||||
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{categories.slice(0, 2).map((category, index) => (
|
||||
<Badge key={`${category}-${index}`} color="blue" size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">{category}</span>
|
||||
</Badge>
|
||||
))}
|
||||
{categories.length > 2 && (
|
||||
<span className="text-[11px] text-gray-500">+{categories.length - 2}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -101,7 +101,7 @@ export const createIdeasPageConfig = (
|
||||
toggleContentKey: 'description', // Use description field for toggle content
|
||||
toggleContentLabel: 'Content Outline', // Label for expanded content
|
||||
render: (value: string) => (
|
||||
<span className="text-gray-800 dark:text-white font-medium">{value}</span>
|
||||
<span className="text-gray-800 dark:text-white text-base font-light">{value}</span>
|
||||
),
|
||||
},
|
||||
// Sector column - only show when viewing all sectors
|
||||
|
||||
@@ -69,7 +69,7 @@ export const createImagesPageConfig = (
|
||||
<div>
|
||||
<a
|
||||
href={`/writer/content/${row.content_id}`}
|
||||
className="font-medium text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||
className="text-base font-light text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||
>
|
||||
{row.content_title}
|
||||
</a>
|
||||
|
||||
@@ -54,6 +54,7 @@ export function createPublishedPageConfig(params: {
|
||||
setPublishStatusFilter: (value: string) => void;
|
||||
setCurrentPage: (page: number) => void;
|
||||
activeSector: { id: number; name: string } | null;
|
||||
onRowClick?: (row: Content) => void;
|
||||
}): PublishedPageConfig {
|
||||
const showSectorColumn = !params.activeSector;
|
||||
|
||||
@@ -63,14 +64,20 @@ export function createPublishedPageConfig(params: {
|
||||
label: 'Title',
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
toggleable: true,
|
||||
toggleContentKey: 'content_html',
|
||||
toggleContentLabel: 'Generated Content',
|
||||
render: (value: string, row: Content) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{params.onRowClick ? (
|
||||
<button
|
||||
onClick={() => params.onRowClick!(row)}
|
||||
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
|
||||
>
|
||||
{value || `Content #${row.id}`}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-base font-light text-gray-900 dark:text-white">
|
||||
{value || `Content #${row.id}`}
|
||||
</span>
|
||||
)}
|
||||
{row.external_url && (
|
||||
<a
|
||||
href={row.external_url}
|
||||
|
||||
@@ -67,12 +67,12 @@ export function createReviewPageConfig(params: {
|
||||
{params.onRowClick ? (
|
||||
<button
|
||||
onClick={() => params.onRowClick!(row)}
|
||||
className="font-medium text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
|
||||
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
|
||||
>
|
||||
{value || `Content #${row.id}`}
|
||||
</button>
|
||||
) : (
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
<span className="text-base font-light text-gray-900 dark:text-white">
|
||||
{value || `Content #${row.id}`}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -112,7 +112,7 @@ export const createTasksPageConfig = (
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
<span className="text-base font-light text-gray-900 dark:text-white">
|
||||
{displayTitle}
|
||||
</span>
|
||||
{isSiteBuilder && (
|
||||
|
||||
@@ -256,8 +256,11 @@ export default function Published() {
|
||||
setPublishStatusFilter,
|
||||
setCurrentPage,
|
||||
activeSector,
|
||||
onRowClick: (row: Content) => {
|
||||
navigate(`/writer/content/${row.id}`);
|
||||
},
|
||||
});
|
||||
}, [searchTerm, statusFilter, publishStatusFilter, activeSector]);
|
||||
}, [searchTerm, statusFilter, publishStatusFilter, activeSector, navigate]);
|
||||
|
||||
// Calculate header metrics
|
||||
const headerMetrics = useMemo(() => {
|
||||
|
||||
@@ -2028,6 +2028,8 @@ export interface Content {
|
||||
name: string;
|
||||
taxonomy_type: string;
|
||||
}>;
|
||||
tags?: string[];
|
||||
categories?: string[];
|
||||
// WordPress integration
|
||||
external_id?: string | null;
|
||||
external_url?: string | null;
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
* Column Visibility Store (Zustand)
|
||||
* Manages column visibility settings per page with localStorage persistence
|
||||
* Uses the same pattern as siteStore and sectorStore
|
||||
* Stores preferences per user for multi-user support
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
|
||||
import { useAuthStore } from './authStore';
|
||||
|
||||
interface ColumnVisibilityState {
|
||||
// Map of page pathname to Set of visible column keys
|
||||
@@ -17,6 +19,29 @@ interface ColumnVisibilityState {
|
||||
resetPageColumns: (pathname: string) => void;
|
||||
}
|
||||
|
||||
// Custom storage that uses user-specific keys
|
||||
const userSpecificStorage: StateStorage = {
|
||||
getItem: (name: string) => {
|
||||
const user = useAuthStore.getState().user;
|
||||
const userId = user?.id || 'anonymous';
|
||||
const key = `igny8-column-visibility-user-${userId}`;
|
||||
const value = localStorage.getItem(key);
|
||||
return value;
|
||||
},
|
||||
setItem: (name: string, value: string) => {
|
||||
const user = useAuthStore.getState().user;
|
||||
const userId = user?.id || 'anonymous';
|
||||
const key = `igny8-column-visibility-user-${userId}`;
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
removeItem: (name: string) => {
|
||||
const user = useAuthStore.getState().user;
|
||||
const userId = user?.id || 'anonymous';
|
||||
const key = `igny8-column-visibility-user-${userId}`;
|
||||
localStorage.removeItem(key);
|
||||
},
|
||||
};
|
||||
|
||||
export const useColumnVisibilityStore = create<ColumnVisibilityState>()(
|
||||
persist<ColumnVisibilityState>(
|
||||
(set, get) => ({
|
||||
@@ -61,6 +86,7 @@ export const useColumnVisibilityStore = create<ColumnVisibilityState>()(
|
||||
}),
|
||||
{
|
||||
name: 'igny8-column-visibility',
|
||||
storage: createJSONStorage(() => userSpecificStorage),
|
||||
partialize: (state) => ({
|
||||
pageColumns: state.pageColumns,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user