Section 1 & 2 - #Migration Run
This commit is contained in:
@@ -1472,13 +1472,13 @@ class AutomationService:
|
||||
time.sleep(delay)
|
||||
|
||||
def run_stage_7(self):
|
||||
"""Stage 7: Auto-Approve and Publish Review Content
|
||||
"""Stage 7: Auto-Approve Review Content
|
||||
|
||||
This stage automatically approves content in 'review' status and
|
||||
marks it as 'published' (or queues for WordPress sync).
|
||||
marks it as 'approved' (ready for publishing to WordPress).
|
||||
"""
|
||||
stage_number = 7
|
||||
stage_name = "Review → Published"
|
||||
stage_name = "Review → Approved"
|
||||
start_time = time.time()
|
||||
|
||||
# Query content ready for review
|
||||
@@ -1538,7 +1538,7 @@ class AutomationService:
|
||||
'review_total': total_count,
|
||||
'approved_count': approved_count,
|
||||
'content_ids': list(Content.objects.filter(
|
||||
site=self.site, status='published', updated_at__gte=self.run.started_at
|
||||
site=self.site, status='approved', updated_at__gte=self.run.started_at
|
||||
).values_list('id', flat=True)),
|
||||
'partial': True,
|
||||
'stopped_reason': reason,
|
||||
@@ -1553,8 +1553,8 @@ class AutomationService:
|
||||
stage_number, f"Approving content {idx}/{total_count}: {content.title}"
|
||||
)
|
||||
|
||||
# Approve content by changing status to 'published'
|
||||
content.status = 'published'
|
||||
# Approve content by changing status to 'approved' (ready for publishing)
|
||||
content.status = 'approved'
|
||||
content.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
approved_count += 1
|
||||
@@ -1593,7 +1593,7 @@ class AutomationService:
|
||||
time_elapsed = self._format_time_elapsed(start_time)
|
||||
content_ids = list(Content.objects.filter(
|
||||
site=self.site,
|
||||
status='published',
|
||||
status='approved',
|
||||
updated_at__gte=self.run.started_at
|
||||
).values_list('id', flat=True))
|
||||
|
||||
@@ -1617,7 +1617,7 @@ class AutomationService:
|
||||
# Release lock
|
||||
cache.delete(f'automation_lock_{self.site.id}')
|
||||
|
||||
logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved and published")
|
||||
logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved (ready for publishing)")
|
||||
|
||||
def pause_automation(self):
|
||||
"""Pause current automation run"""
|
||||
|
||||
@@ -271,7 +271,8 @@ class Content(SoftDeletableModel, SiteSectorBaseModel):
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Draft'),
|
||||
('review', 'Review'),
|
||||
('published', 'Published'),
|
||||
('approved', 'Approved'), # Ready for publishing to external site
|
||||
('published', 'Published'), # Actually published on external site
|
||||
]
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.9 on 2026-01-01 06:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('writer', '0013_add_caption_to_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImagePrompts',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Image Prompt',
|
||||
'verbose_name_plural': 'Image Prompts',
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('writer.images',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('draft', 'Draft'), ('review', 'Review'), ('approved', 'Approved'), ('published', 'Published')], db_index=True, default='draft', help_text='Content status', max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -11,8 +11,8 @@ class TasksSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Tasks model - Stage 1 refactored"""
|
||||
cluster_name = serializers.SerializerMethodField()
|
||||
sector_name = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
site_id = serializers.IntegerField(read_only=True)
|
||||
sector_id = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tasks
|
||||
@@ -162,8 +162,8 @@ class ContentSerializer(serializers.ModelSerializer):
|
||||
has_image_prompts = serializers.SerializerMethodField()
|
||||
image_status = serializers.SerializerMethodField()
|
||||
has_generated_images = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
site_id = serializers.IntegerField(read_only=True)
|
||||
sector_id = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Content
|
||||
@@ -300,8 +300,8 @@ class ContentSerializer(serializers.ModelSerializer):
|
||||
class ContentTaxonomySerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ContentTaxonomy model - Stage 1 refactored"""
|
||||
content_count = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
site_id = serializers.IntegerField(read_only=True)
|
||||
sector_id = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContentTaxonomy
|
||||
|
||||
@@ -751,6 +751,19 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
'source',
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override to support status__in filtering for multiple statuses"""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Support status__in query param (comma-separated list of statuses)
|
||||
status_in = self.request.query_params.get('status__in', None)
|
||||
if status_in:
|
||||
statuses = [s.strip() for s in status_in.split(',') if s.strip()]
|
||||
if statuses:
|
||||
queryset = queryset.filter(status__in=statuses)
|
||||
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Override to check monthly word limit and set account"""
|
||||
user = getattr(self.request, 'user', None)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
interface CreditAvailabilityWidgetProps {
|
||||
availableCredits: number;
|
||||
totalCredits: number;
|
||||
usedCredits?: number; // Actual credits used this month from API
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -29,10 +30,17 @@ const OPERATION_COSTS = {
|
||||
export default function CreditAvailabilityWidget({
|
||||
availableCredits,
|
||||
totalCredits,
|
||||
usedCredits: usedCreditsFromApi,
|
||||
loading = false
|
||||
}: CreditAvailabilityWidgetProps) {
|
||||
const usedCredits = totalCredits - availableCredits;
|
||||
const usagePercent = totalCredits > 0 ? Math.round((usedCredits / totalCredits) * 100) : 0;
|
||||
// Use actual used credits from API if provided, otherwise calculate
|
||||
// Note: availableCredits may include purchased credits beyond plan allocation
|
||||
const usedCredits = usedCreditsFromApi ?? 0;
|
||||
|
||||
// Calculate usage percentage based on plan allocation
|
||||
// If available > plan, user has extra credits (purchased or carried over)
|
||||
const usagePercent = totalCredits > 0 ? Math.min(Math.round((usedCredits / totalCredits) * 100), 100) : 0;
|
||||
const remainingPercent = Math.max(100 - usagePercent, 0);
|
||||
|
||||
// Calculate available operations
|
||||
const availableOps = Object.entries(OPERATION_COSTS).map(([key, config]) => ({
|
||||
@@ -72,11 +80,11 @@ export default function CreditAvailabilityWidget({
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
usagePercent > 90 ? 'bg-error-500' : usagePercent > 75 ? 'bg-warning-500' : 'bg-success-500'
|
||||
}`}
|
||||
style={{ width: `${Math.max(100 - usagePercent, 0)}%` }}
|
||||
style={{ width: `${remainingPercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used (${usagePercent}%)` : 'No credits allocated'}
|
||||
{totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used this month (${usagePercent}%)` : 'No credits allocated'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ export default function StandardizedModuleWidget({
|
||||
];
|
||||
|
||||
// Define Writer pipeline - using correct content structure
|
||||
// Content has status: draft, review, published
|
||||
// totalContent = drafts + review + published
|
||||
// Content has status: draft, review, approved, published
|
||||
// totalContent = drafts + review + approved + published
|
||||
// Get writer colors from config
|
||||
const writerColors = useMemo(() => getPipelineColors('writer'), []);
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function WorkflowCompletionWidget({
|
||||
];
|
||||
|
||||
// Define writer items - using "Content Pages" not "Articles"
|
||||
// Total content = drafts + review + published
|
||||
// Total content = drafts + review + approved + published
|
||||
const totalContent = writer.contentDrafts + writer.contentReview + writer.contentPublished;
|
||||
const writerItems = [
|
||||
{ label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` },
|
||||
|
||||
@@ -90,6 +90,27 @@ export function createApprovedPageConfig(params: {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
sortable: true,
|
||||
sortField: 'status',
|
||||
width: '130px',
|
||||
render: (value: string, row: Content) => {
|
||||
// Map internal status to user-friendly labels
|
||||
const statusConfig: Record<string, { color: 'success' | 'blue' | 'amber' | 'gray'; label: string }> = {
|
||||
'approved': { color: 'blue', label: 'Ready to Publish' },
|
||||
'published': { color: 'success', label: row.external_id ? 'On Site' : 'Approved' },
|
||||
};
|
||||
const config = statusConfig[value] || { color: 'gray' as const, label: value || '-' };
|
||||
|
||||
return (
|
||||
<Badge color={config.color} size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">{config.label}</span>
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'wordpress_status',
|
||||
label: 'Site Content Status',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* IMPORTANT: Content table structure
|
||||
* - Tasks is separate table (has status: queued, processing, completed, failed)
|
||||
* - Content table has status field: 'draft', 'review', 'published' (approved)
|
||||
* - Content table has status field: 'draft', 'review', 'approved', 'published'
|
||||
* - Images is separate table linked to content
|
||||
*
|
||||
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint
|
||||
@@ -43,7 +43,7 @@ export interface WriterModuleStats {
|
||||
tasksCompleted: number; // Tasks with status='completed'
|
||||
contentDrafts: number; // Content with status='draft'
|
||||
contentReview: number; // Content with status='review'
|
||||
contentPublished: number; // Content with status='published' (approved)
|
||||
contentPublished: number; // Content with status='approved' or 'published' (ready for publishing or on site)
|
||||
totalContent: number; // All content regardless of status
|
||||
totalImages: number;
|
||||
}
|
||||
@@ -158,8 +158,8 @@ export function useModuleStats() {
|
||||
fetchContent({ ...baseFilters, status: 'draft' }),
|
||||
// Content with status='review'
|
||||
fetchContent({ ...baseFilters, status: 'review' }),
|
||||
// Content with status='published' (approved)
|
||||
fetchContent({ ...baseFilters, status: 'published' }),
|
||||
// Content with status='approved' or 'published' (ready for publishing or on site)
|
||||
fetchContent({ ...baseFilters, status__in: 'approved,published' }),
|
||||
// Total content (all statuses)
|
||||
fetchContent({ ...baseFilters }),
|
||||
// Total images
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* IMPORTANT: Content table structure
|
||||
* - Tasks is separate table
|
||||
* - Content table has status field: 'draft', 'review', 'published' (approved)
|
||||
* - Content table has status field: 'draft', 'review', 'approved', 'published'
|
||||
* - Images is separate table linked to content
|
||||
*
|
||||
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint
|
||||
@@ -57,7 +57,7 @@ export interface WorkflowStats {
|
||||
tasksTotal: number;
|
||||
contentDrafts: number; // Content with status='draft'
|
||||
contentReview: number; // Content with status='review'
|
||||
contentPublished: number; // Content with status='published' (approved)
|
||||
contentPublished: number; // Content with status='approved' or 'published' (ready for publishing or on site)
|
||||
imagesCreated: number;
|
||||
};
|
||||
// Credit consumption stats - detailed breakdown by operation
|
||||
@@ -208,10 +208,10 @@ export function useWorkflowStats(timeFilter: TimeFilter = 'all') {
|
||||
dateFilter
|
||||
? fetchAPI(`/v1/writer/content/?page_size=1&status=review${baseParams}${dateParam}`)
|
||||
: fetchContent({ ...baseFilters, status: 'review' }),
|
||||
// Content with status='published' (approved)
|
||||
// Content with status='approved' or 'published' (ready for publishing or on site)
|
||||
dateFilter
|
||||
? fetchAPI(`/v1/writer/content/?page_size=1&status=published${baseParams}${dateParam}`)
|
||||
: fetchContent({ ...baseFilters, status: 'published' }),
|
||||
? fetchAPI(`/v1/writer/content/?page_size=1&status__in=approved,published${baseParams}${dateParam}`)
|
||||
: fetchContent({ ...baseFilters, status__in: 'approved,published' }),
|
||||
// Total images
|
||||
dateFilter
|
||||
? fetchAPI(`/v1/writer/images/?page_size=1${baseParams}${dateParam}`)
|
||||
|
||||
@@ -139,7 +139,7 @@ const AutomationPage: React.FC = () => {
|
||||
fetchContent({ page_size: 1, site_id: siteId }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status__in: 'approved,published' }),
|
||||
fetchImages({ page_size: 1 }),
|
||||
fetchImages({ page_size: 1, status: 'pending' }),
|
||||
]);
|
||||
@@ -258,7 +258,7 @@ const AutomationPage: React.FC = () => {
|
||||
fetchContent({ page_size: 1, site_id: siteId }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }),
|
||||
fetchContent({ page_size: 1, site_id: siteId, status__in: 'approved,published' }),
|
||||
fetchImages({ page_size: 1 }),
|
||||
fetchImages({ page_size: 1, status: 'pending' }),
|
||||
]);
|
||||
|
||||
@@ -7,6 +7,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI, fetchSiteSectors } from '../../services/api';
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
BoltIcon,
|
||||
PageIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
} from '../../icons';
|
||||
|
||||
interface Site {
|
||||
@@ -251,6 +254,7 @@ export default function SiteDashboard() {
|
||||
<CreditAvailabilityWidget
|
||||
availableCredits={balance?.credits_remaining ?? 0}
|
||||
totalCredits={balance?.plan_credits_per_month ?? 0}
|
||||
usedCredits={balance?.credits_used_this_month ?? 0}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -59,9 +59,9 @@ export default function Approved() {
|
||||
// Load total metrics for footer widget and header metrics (not affected by pagination)
|
||||
const loadTotalMetrics = useCallback(async () => {
|
||||
try {
|
||||
// Fetch all approved content to calculate totals
|
||||
// Fetch all approved+published content to calculate totals
|
||||
const data = await fetchContent({
|
||||
status: 'published', // Backend uses 'published' for approved content
|
||||
status__in: 'approved,published', // Both approved and published content
|
||||
page_size: 1000, // Fetch enough to count
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function Approved() {
|
||||
loadTotalMetrics();
|
||||
}, [loadTotalMetrics]);
|
||||
|
||||
// Load content - filtered for approved status (API still uses 'published' internally)
|
||||
// Load content - filtered for approved+published status
|
||||
const loadContent = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setShowContent(false);
|
||||
@@ -95,7 +95,7 @@ export default function Approved() {
|
||||
|
||||
const filters: ContentFilters = {
|
||||
...(searchTerm && { search: searchTerm }),
|
||||
status: 'published', // Backend uses 'published' for approved content
|
||||
status__in: 'approved,published', // Both approved and published content
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
ordering,
|
||||
@@ -221,8 +221,13 @@ export default function Approved() {
|
||||
toast.warning('WordPress URL not available');
|
||||
}
|
||||
} else if (action === 'edit') {
|
||||
// Navigate to content editor (if exists) or show edit modal
|
||||
navigate(`/writer/content?id=${row.id}`);
|
||||
// Navigate to content editor
|
||||
if (row.site_id) {
|
||||
navigate(`/sites/${row.site_id}/posts/${row.id}/edit`);
|
||||
} else {
|
||||
// Fallback if site_id not available
|
||||
toast.warning('Unable to edit: Site information not available');
|
||||
}
|
||||
}
|
||||
}, [toast, loadContent, navigate]);
|
||||
|
||||
|
||||
@@ -81,11 +81,11 @@ export default function Content() {
|
||||
});
|
||||
setTotalReview(reviewRes.count || 0);
|
||||
|
||||
// Get content with status='published'
|
||||
// Get content with status='approved' or 'published' (ready for publishing or on site)
|
||||
const publishedRes = await fetchContent({
|
||||
page_size: 1,
|
||||
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||
status: 'published',
|
||||
status__in: 'approved,published',
|
||||
});
|
||||
setTotalPublished(publishedRes.count || 0);
|
||||
|
||||
|
||||
@@ -1385,7 +1385,7 @@ export interface ContentImage {
|
||||
export interface ContentImagesGroup {
|
||||
content_id: number;
|
||||
content_title: string;
|
||||
content_status: 'draft' | 'review' | 'publish';
|
||||
content_status: 'draft' | 'review' | 'approved' | 'published';
|
||||
featured_image: ContentImage | null;
|
||||
in_article_images: ContentImage[];
|
||||
overall_status: 'pending' | 'partial' | 'complete' | 'failed';
|
||||
@@ -2197,6 +2197,7 @@ export async function deleteAuthorProfile(id: number): Promise<void> {
|
||||
export interface ContentFilters {
|
||||
search?: string;
|
||||
status?: string;
|
||||
status__in?: string; // Comma-separated list of statuses (e.g., 'approved,published')
|
||||
content_type?: string;
|
||||
content_structure?: string;
|
||||
source?: string;
|
||||
@@ -2215,9 +2216,11 @@ export interface Content {
|
||||
content_html: string;
|
||||
content_type: string;
|
||||
content_structure: string;
|
||||
status: 'draft' | 'published';
|
||||
status: 'draft' | 'review' | 'approved' | 'published';
|
||||
source: 'igny8' | 'wordpress';
|
||||
// Relations
|
||||
site_id?: number;
|
||||
sector_id?: number;
|
||||
cluster_id: number;
|
||||
cluster_name?: string | null;
|
||||
sector_name?: string | null;
|
||||
@@ -2282,6 +2285,7 @@ export async function fetchContent(filters: ContentFilters = {}): Promise<Conten
|
||||
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.status) params.append('status', filters.status);
|
||||
if (filters.status__in) params.append('status__in', filters.status__in);
|
||||
if (filters.task_id) params.append('task_id', filters.task_id.toString());
|
||||
if (filters.site_id) params.append('site_id', filters.site_id.toString());
|
||||
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
|
||||
|
||||
@@ -792,6 +792,12 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
if (statusLower === 'generated' || statusLower === 'published' || statusLower === 'complete') {
|
||||
return 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400';
|
||||
}
|
||||
if (statusLower === 'approved') {
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
|
||||
}
|
||||
if (statusLower === 'review') {
|
||||
return 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400';
|
||||
}
|
||||
if (statusLower === 'pending' || statusLower === 'draft') {
|
||||
return 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400';
|
||||
}
|
||||
@@ -801,6 +807,18 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string, hasExternalId?: boolean) => {
|
||||
const statusLower = status.toLowerCase();
|
||||
// Map status to user-friendly labels
|
||||
const statusLabels: Record<string, string> = {
|
||||
'draft': 'Draft',
|
||||
'review': 'In Review',
|
||||
'approved': 'Ready to Publish',
|
||||
'published': hasExternalId ? 'On Site' : 'Approved',
|
||||
};
|
||||
return statusLabels[statusLower] || status.charAt(0).toUpperCase() + status.slice(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -824,7 +842,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<FileTextIcon className="w-6 h-6" />
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(content.status)}`}>
|
||||
{content.status}
|
||||
{getStatusLabel(content.status, !!content.external_id)}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
|
||||
Reference in New Issue
Block a user