diff --git a/backend/fix_taxonomy_relationships.py b/backend/fix_taxonomy_relationships.py new file mode 100644 index 00000000..8e10b110 --- /dev/null +++ b/backend/fix_taxonomy_relationships.py @@ -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) diff --git a/backend/igny8_core/ai/functions/generate_content.py b/backend/igny8_core/ai/functions/generate_content.py index e3bb3b57..60cb25f5 100644 --- a/backend/igny8_core/ai/functions/generate_content.py +++ b/backend/igny8_core/ai/functions/generate_content.py @@ -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' diff --git a/backend/igny8_core/business/content/models.py b/backend/igny8_core/business/content/models.py index 4e4167b9..5aeaa301 100644 --- a/backend/igny8_core/business/content/models.py +++ b/backend/igny8_core/business/content/models.py @@ -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) diff --git a/backend/igny8_core/modules/writer/admin.py b/backend/igny8_core/modules/writer/admin.py index bfa8d8f6..d9caace6 100644 --- a/backend/igny8_core/modules/writer/admin.py +++ b/backend/igny8_core/modules/writer/admin.py @@ -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: diff --git a/backend/igny8_core/modules/writer/serializers.py b/backend/igny8_core/modules/writer/serializers.py index af14bdfc..0bfd38ea 100644 --- a/backend/igny8_core/modules/writer/serializers.py +++ b/backend/igny8_core/modules/writer/serializers.py @@ -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() diff --git a/backend/test_tags_categories.py b/backend/test_tags_categories.py new file mode 100644 index 00000000..7e5b506a --- /dev/null +++ b/backend/test_tags_categories.py @@ -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") + diff --git a/backend/verify_taxonomy.py b/backend/verify_taxonomy.py new file mode 100644 index 00000000..1ee03b6c --- /dev/null +++ b/backend/verify_taxonomy.py @@ -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) diff --git a/frontend/src/components/ui/badge/Badge.tsx b/frontend/src/components/ui/badge/Badge.tsx index 7e562157..de282038 100644 --- a/frontend/src/components/ui/badge/Badge.tsx +++ b/frontend/src/components/ui/badge/Badge.tsx @@ -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 = { - 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 = ({ @@ -98,6 +134,12 @@ const Badge: React.FC = ({ "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 = ({ return ( ( {value} diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index c6f7a23f..43fbcf17 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -104,12 +104,12 @@ export const createContentPageConfig = ( {handlers.onRowClick ? ( ) : ( -
+
{row.title || `Content #${row.id}`}
)} @@ -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 -; } return (
- {taxonomyTerms.slice(0, 2).map((term) => ( - - {term.name} + {tags.slice(0, 2).map((tag, index) => ( + + {tag} ))} - {taxonomyTerms.length > 2 && ( - +{taxonomyTerms.length - 2} + {tags.length > 2 && ( + +{tags.length - 2} + )} +
+ ); + }, + }, + { + key: 'categories', + label: 'Categories', + sortable: false, + width: '150px', + render: (_value: any, row: Content) => { + const categories = row.categories || []; + if (!categories || categories.length === 0) { + return -; + } + return ( +
+ {categories.slice(0, 2).map((category, index) => ( + + {category} + + ))} + {categories.length > 2 && ( + +{categories.length - 2} )}
); diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index 8899be36..6837ad28 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -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) => ( - {value} + {value} ), }, // Sector column - only show when viewing all sectors diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index bab21efc..602cdd41 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -69,7 +69,7 @@ export const createImagesPageConfig = (
{row.content_title} diff --git a/frontend/src/config/pages/published.config.tsx b/frontend/src/config/pages/published.config.tsx index 94417082..ababce2a 100644 --- a/frontend/src/config/pages/published.config.tsx +++ b/frontend/src/config/pages/published.config.tsx @@ -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) => (
- - {value || `Content #${row.id}`} - + {params.onRowClick ? ( + + ) : ( + + {value || `Content #${row.id}`} + + )} {row.external_url && ( 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}`} ) : ( - + {value || `Content #${row.id}`} )} diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index aa1335b9..d1240488 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -112,7 +112,7 @@ export const createTasksPageConfig = ( return (
- + {displayTitle} {isSiteBuilder && ( diff --git a/frontend/src/pages/Writer/Published.tsx b/frontend/src/pages/Writer/Published.tsx index f79b7f00..4c610f45 100644 --- a/frontend/src/pages/Writer/Published.tsx +++ b/frontend/src/pages/Writer/Published.tsx @@ -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(() => { diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index a24de4a1..252318de 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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; diff --git a/frontend/src/store/columnVisibilityStore.ts b/frontend/src/store/columnVisibilityStore.ts index dd796106..cbe2fc95 100644 --- a/frontend/src/store/columnVisibilityStore.ts +++ b/frontend/src/store/columnVisibilityStore.ts @@ -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()( persist( (set, get) => ({ @@ -61,6 +86,7 @@ export const useColumnVisibilityStore = create()( }), { name: 'igny8-column-visibility', + storage: createJSONStorage(() => userSpecificStorage), partialize: (state) => ({ pageColumns: state.pageColumns, }),