feat: Complete Stage 2 frontend refactor
- Removed deprecated fields from Content and Task models, including entity_type, sync_status, and cluster_role. - Updated Content model to include new fields: content_type, content_structure, taxonomy_terms, source, external_id, and cluster_id. - Refactored Writer module components (Content, ContentView, Dashboard, Tasks) to align with new schema. - Enhanced Dashboard metrics and removed unused filters. - Implemented ClusterDetail page to display cluster information and associated content. - Updated API service interfaces to reflect changes in data structure. - Adjusted sorting and filtering logic across various components to accommodate new field names and types. - Improved user experience by providing loading states and error handling in data fetching.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
titleColumn,
|
||||
sectorColumn,
|
||||
@@ -104,6 +105,14 @@ export const createClustersPageConfig = (
|
||||
label: 'Cluster Name',
|
||||
sortable: true,
|
||||
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"
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
// Sector column - only show when viewing all sectors
|
||||
...(showSectorColumn ? [{
|
||||
|
||||
@@ -14,9 +14,6 @@ import {
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { formatRelativeDate } from '../../utils/date';
|
||||
import { Content } from '../../services/api';
|
||||
import { FileIcon, MoreDotIcon } from '../../icons';
|
||||
import { SourceBadge, ContentSource } from '../../components/content/SourceBadge';
|
||||
import { SyncStatusBadge, SyncStatus } from '../../components/content/SyncStatusBadge';
|
||||
|
||||
export interface ColumnConfig {
|
||||
key: string;
|
||||
@@ -101,18 +98,13 @@ export const createContentPageConfig = (
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
toggleable: true,
|
||||
toggleContentKey: 'html_content',
|
||||
toggleContentKey: 'content_html',
|
||||
toggleContentLabel: 'Generated Content',
|
||||
render: (value: string, row: Content) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{row.meta_title || row.title || row.task_title || `Task #${row.task_id}`}
|
||||
{row.title || `Content #${row.id}`}
|
||||
</div>
|
||||
{row.meta_description && (
|
||||
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
{row.meta_description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -125,119 +117,53 @@ export const createContentPageConfig = (
|
||||
),
|
||||
}] : []),
|
||||
{
|
||||
key: 'primary_keyword',
|
||||
label: 'Primary Keyword',
|
||||
key: 'content_type',
|
||||
label: 'Content Type',
|
||||
sortable: true,
|
||||
sortField: 'primary_keyword',
|
||||
width: '150px',
|
||||
render: (value: string, row: Content) => (
|
||||
row.primary_keyword ? (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{row.primary_keyword}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'secondary_keywords',
|
||||
label: 'Secondary Keywords',
|
||||
sortable: false,
|
||||
width: '200px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const secondaryKeywords = getList(
|
||||
row.secondary_keywords,
|
||||
row.metadata?.secondary_keywords
|
||||
);
|
||||
return renderBadgeList(secondaryKeywords);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
label: 'Tags',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const tags = getList(row.tags, row.metadata?.tags);
|
||||
return renderBadgeList(tags);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
label: 'Categories',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const categories = getList(row.categories, row.metadata?.categories);
|
||||
return renderBadgeList(categories);
|
||||
},
|
||||
},
|
||||
{
|
||||
...wordCountColumn,
|
||||
sortable: true,
|
||||
sortField: 'word_count',
|
||||
render: (value: number) => value?.toLocaleString() ?? '-',
|
||||
},
|
||||
{
|
||||
...statusColumn,
|
||||
sortable: true,
|
||||
sortField: 'status',
|
||||
sortField: 'content_type',
|
||||
width: '120px',
|
||||
render: (value: string) => {
|
||||
const status = value || 'draft';
|
||||
const color = statusColors[status] || 'primary';
|
||||
const label = status.replace('_', ' ').replace(/^\w/, (c) => c.toUpperCase());
|
||||
return (
|
||||
<Badge color={color} size="sm" variant="light">
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: 'Source',
|
||||
sortable: true,
|
||||
sortField: 'source',
|
||||
width: '120px',
|
||||
render: (_value: any, row: Content) => (
|
||||
<SourceBadge source={(row.source as ContentSource) || 'igny8'} />
|
||||
),
|
||||
},
|
||||
// Stage 3: Metadata columns
|
||||
{
|
||||
key: 'entity_type',
|
||||
label: 'Entity Type',
|
||||
sortable: true,
|
||||
sortField: 'entity_type',
|
||||
width: '120px',
|
||||
defaultVisible: true,
|
||||
render: (value: string, row: Content) => {
|
||||
const entityType = value || row.entity_type;
|
||||
if (!entityType) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
const typeLabels: Record<string, string> = {
|
||||
'blog_post': 'Blog Post',
|
||||
'article': 'Article',
|
||||
'post': 'Post',
|
||||
'page': 'Page',
|
||||
'product': 'Product',
|
||||
'service': 'Service',
|
||||
'taxonomy': 'Taxonomy',
|
||||
'page': 'Page',
|
||||
'category': 'Category',
|
||||
'tag': 'Tag',
|
||||
};
|
||||
return (
|
||||
<Badge color="primary" size="sm" variant="light">
|
||||
{typeLabels[value] || value || '-'}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'content_structure',
|
||||
label: 'Structure',
|
||||
sortable: true,
|
||||
sortField: 'content_structure',
|
||||
width: '150px',
|
||||
render: (value: string) => {
|
||||
const structureLabels: Record<string, string> = {
|
||||
'article': 'Article',
|
||||
'listicle': 'Listicle',
|
||||
'guide': 'Guide',
|
||||
'comparison': 'Comparison',
|
||||
'product_page': 'Product Page',
|
||||
};
|
||||
return (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{typeLabels[entityType] || entityType}
|
||||
{structureLabels[value] || value || '-'}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'cluster',
|
||||
key: 'cluster_name',
|
||||
label: 'Cluster',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
defaultVisible: true,
|
||||
render: (_value: any, row: Content) => {
|
||||
const clusterName = row.cluster_name;
|
||||
if (!clusterName) {
|
||||
@@ -251,65 +177,96 @@ export const createContentPageConfig = (
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'cluster_role',
|
||||
label: 'Role',
|
||||
sortable: true,
|
||||
sortField: 'cluster_role',
|
||||
width: '100px',
|
||||
defaultVisible: false,
|
||||
render: (value: string, row: Content) => {
|
||||
const role = value || row.cluster_role;
|
||||
if (!role) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
const roleColors: Record<string, 'primary' | 'success' | 'warning'> = {
|
||||
'hub': 'primary',
|
||||
'supporting': 'success',
|
||||
'attribute': 'warning',
|
||||
};
|
||||
return (
|
||||
<Badge color={roleColors[role] || 'primary'} size="sm" variant="light">
|
||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'taxonomy',
|
||||
key: 'taxonomy_terms',
|
||||
label: 'Taxonomy',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
defaultVisible: false,
|
||||
width: '180px',
|
||||
render: (_value: any, row: Content) => {
|
||||
const taxonomyName = row.taxonomy_name;
|
||||
if (!taxonomyName) {
|
||||
const taxonomyTerms = row.taxonomy_terms;
|
||||
if (!taxonomyTerms || taxonomyTerms.length === 0) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
return (
|
||||
<Badge color="purple" size="sm" variant="light">
|
||||
{taxonomyName}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{taxonomyTerms.map((term) => (
|
||||
<Badge key={term.id} color="purple" size="sm" variant="light">
|
||||
{term.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...statusColumn,
|
||||
sortable: true,
|
||||
sortField: 'status',
|
||||
render: (value: string) => {
|
||||
const statusColors: Record<string, 'warning' | 'success'> = {
|
||||
draft: 'warning',
|
||||
published: 'success',
|
||||
};
|
||||
const color = statusColors[value] || 'warning';
|
||||
const label = value === 'published' ? 'Published' : 'Draft';
|
||||
return (
|
||||
<Badge color={color} size="sm" variant="light">
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'sync_status',
|
||||
label: 'Sync Status',
|
||||
key: 'source',
|
||||
label: 'Source',
|
||||
sortable: true,
|
||||
sortField: 'sync_status',
|
||||
sortField: 'source',
|
||||
width: '120px',
|
||||
render: (_value: any, row: Content) => (
|
||||
<SyncStatusBadge status={(row.sync_status as SyncStatus) || 'native'} />
|
||||
),
|
||||
render: (value: any, row: Content) => {
|
||||
const source = value || row.source || 'igny8';
|
||||
const sourceColors: Record<string, 'primary' | 'info'> = {
|
||||
igny8: 'primary',
|
||||
wordpress: 'info',
|
||||
};
|
||||
const sourceLabels: Record<string, string> = {
|
||||
igny8: 'IGNY8',
|
||||
wordpress: 'WordPress',
|
||||
};
|
||||
return (
|
||||
<Badge color={sourceColors[source] || 'primary'} size="sm" variant="light">
|
||||
{sourceLabels[source] || source}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'external_url',
|
||||
label: 'URL',
|
||||
sortable: false,
|
||||
width: '200px',
|
||||
defaultVisible: false,
|
||||
render: (value: string | null, row: Content) => {
|
||||
const url = value || row.external_url || null;
|
||||
return url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 truncate block max-w-[200px]"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...createdColumn,
|
||||
sortable: true,
|
||||
sortField: 'generated_at',
|
||||
label: 'Generated',
|
||||
sortField: 'created_at',
|
||||
label: 'Created',
|
||||
align: 'right',
|
||||
render: (value: string, row: Content) => {
|
||||
const hasPrompts = row.has_image_prompts || false;
|
||||
const hasImages = row.has_generated_images || false;
|
||||
|
||||
return (
|
||||
@@ -317,42 +274,10 @@ export const createContentPageConfig = (
|
||||
<span className="text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
{formatRelativeDate(value)}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Prompt Icon */}
|
||||
{hasImages && (
|
||||
<div
|
||||
className={`w-5 h-5 flex items-center justify-center flex-shrink-0 ${
|
||||
hasPrompts
|
||||
? 'text-green-500 dark:text-green-400'
|
||||
: 'text-gray-300 dark:text-gray-600'
|
||||
}`}
|
||||
title={hasPrompts ? 'Image prompts generated' : 'No image prompts'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Image Icon */}
|
||||
<div
|
||||
className={`w-5 h-5 flex items-center justify-center flex-shrink-0 ${
|
||||
hasImages
|
||||
? 'text-green-500 dark:text-green-400'
|
||||
: 'text-gray-300 dark:text-gray-600'
|
||||
}`}
|
||||
title={hasImages ? 'Images generated' : 'No images generated'}
|
||||
className="w-5 h-5 flex items-center justify-center flex-shrink-0 text-green-500 dark:text-green-400"
|
||||
title="Images generated"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -369,47 +294,12 @@ export const createContentPageConfig = (
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
// Optional columns - hidden by default
|
||||
{
|
||||
key: 'task_title',
|
||||
label: 'Task Title',
|
||||
sortable: true,
|
||||
sortField: 'task_id',
|
||||
defaultVisible: false,
|
||||
width: '200px',
|
||||
render: (_value: string, row: Content) => (
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 truncate block max-w-[200px]">
|
||||
{row.task_title || '-'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'post_url',
|
||||
label: 'Post URL',
|
||||
sortable: false,
|
||||
defaultVisible: false,
|
||||
width: '200px',
|
||||
render: (value: string | null, row: Content) => {
|
||||
const url = value || row.post_url || null;
|
||||
return url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 truncate block max-w-[200px]"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
@@ -433,23 +323,34 @@ export const createContentPageConfig = (
|
||||
options: [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'review', label: 'Review' },
|
||||
{ value: 'publish', label: 'Publish' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
],
|
||||
},
|
||||
// Stage 3: Entity type filter
|
||||
{
|
||||
key: 'entity_type',
|
||||
label: 'Entity Type',
|
||||
key: 'content_type',
|
||||
label: 'Content Type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Types' },
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'taxonomy', label: 'Taxonomy' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'category', label: 'Category' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_structure',
|
||||
label: 'Content Structure',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Structures' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'listicle', label: 'Listicle' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'comparison', label: 'Comparison' },
|
||||
{ value: 'product_page', label: 'Product Page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -460,19 +361,6 @@ export const createContentPageConfig = (
|
||||
{ value: '', label: 'All Sources' },
|
||||
{ value: 'igny8', label: 'IGNY8' },
|
||||
{ value: 'wordpress', label: 'WordPress' },
|
||||
{ value: 'shopify', label: 'Shopify' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'sync_status',
|
||||
label: 'Sync Status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Sync Status' },
|
||||
{ value: 'native', label: 'Native' },
|
||||
{ value: 'imported', label: 'Imported' },
|
||||
{ value: 'synced', label: 'Synced' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -486,20 +374,14 @@ export const createContentPageConfig = (
|
||||
{
|
||||
label: 'Draft',
|
||||
value: 0,
|
||||
accentColor: 'warning' as const,
|
||||
accentColor: 'amber' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length,
|
||||
},
|
||||
{
|
||||
label: 'Review',
|
||||
value: 0,
|
||||
accentColor: 'info' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length,
|
||||
},
|
||||
{
|
||||
label: 'Published',
|
||||
value: 0,
|
||||
accentColor: 'success' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'publish').length,
|
||||
accentColor: 'green' as const,
|
||||
calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -156,58 +156,6 @@ export const createIdeasPageConfig = (
|
||||
width: '200px',
|
||||
render: (_value: string, row: ContentIdea) => row.keyword_cluster_name || '-',
|
||||
},
|
||||
// Stage 3: Metadata columns
|
||||
{
|
||||
key: 'site_entity_type',
|
||||
label: 'Entity Type',
|
||||
sortable: true,
|
||||
sortField: 'site_entity_type',
|
||||
width: '120px',
|
||||
defaultVisible: true,
|
||||
render: (value: string, row: ContentIdea) => {
|
||||
const entityType = value || (row as any).site_entity_type;
|
||||
if (!entityType) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
const typeLabels: Record<string, string> = {
|
||||
'blog_post': 'Blog Post',
|
||||
'article': 'Article',
|
||||
'product': 'Product',
|
||||
'service': 'Service',
|
||||
'taxonomy': 'Taxonomy',
|
||||
'page': 'Page',
|
||||
};
|
||||
return (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{typeLabels[entityType] || entityType}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'cluster_role',
|
||||
label: 'Role',
|
||||
sortable: true,
|
||||
sortField: 'cluster_role',
|
||||
width: '100px',
|
||||
defaultVisible: false,
|
||||
render: (value: string, row: ContentIdea) => {
|
||||
const role = value || (row as any).cluster_role;
|
||||
if (!role) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
const roleColors: Record<string, 'primary' | 'success' | 'warning'> = {
|
||||
'hub': 'primary',
|
||||
'supporting': 'success',
|
||||
'attribute': 'warning',
|
||||
};
|
||||
return (
|
||||
<Badge color={roleColors[role] || 'primary'} size="sm" variant="light">
|
||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...statusColumn,
|
||||
sortable: true,
|
||||
@@ -276,37 +224,25 @@ export const createIdeasPageConfig = (
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Structures' },
|
||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
||||
{ value: 'landing_page', label: 'Landing Page' },
|
||||
{ value: 'pillar_page', label: 'Pillar Page' },
|
||||
{ value: 'supporting_page', label: 'Supporting Page' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'listicle', label: 'Listicle' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'comparison', label: 'Comparison' },
|
||||
{ value: 'product_page', label: 'Product Page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_type',
|
||||
label: 'Type',
|
||||
label: 'Content Type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Types' },
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'tutorial', label: 'Tutorial' },
|
||||
],
|
||||
},
|
||||
// Stage 3: Entity type filter
|
||||
{
|
||||
key: 'site_entity_type',
|
||||
label: 'Entity Type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Entity Types' },
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'taxonomy', label: 'Taxonomy' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'category', label: 'Category' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -346,28 +282,31 @@ export const createIdeasPageConfig = (
|
||||
key: 'content_structure',
|
||||
label: 'Content Structure',
|
||||
type: 'select',
|
||||
value: handlers.formData.content_structure || 'blog_post',
|
||||
value: handlers.formData.content_structure || 'article',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, content_structure: value }),
|
||||
options: [
|
||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
||||
{ value: 'landing_page', label: 'Landing Page' },
|
||||
{ value: 'pillar_page', label: 'Pillar Page' },
|
||||
{ value: 'supporting_page', label: 'Supporting Page' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'listicle', label: 'Listicle' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'comparison', label: 'Comparison' },
|
||||
{ value: 'product_page', label: 'Product Page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_type',
|
||||
label: 'Content Type',
|
||||
type: 'select',
|
||||
value: handlers.formData.content_type || 'blog_post',
|
||||
value: handlers.formData.content_type || 'post',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, content_type: value }),
|
||||
options: [
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'tutorial', label: 'Tutorial' },
|
||||
{ value: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'category', label: 'Category' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -140,58 +140,6 @@ export const createTasksPageConfig = (
|
||||
width: '200px',
|
||||
render: (_value: string, row: Task) => row.cluster_name || '-',
|
||||
},
|
||||
// Stage 3: Metadata columns
|
||||
{
|
||||
key: 'entity_type',
|
||||
label: 'Entity Type',
|
||||
sortable: true,
|
||||
sortField: 'entity_type',
|
||||
width: '120px',
|
||||
defaultVisible: true,
|
||||
render: (value: string, row: Task) => {
|
||||
const entityType = value || row.entity_type;
|
||||
if (!entityType) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
const typeLabels: Record<string, string> = {
|
||||
'blog_post': 'Blog Post',
|
||||
'article': 'Article',
|
||||
'product': 'Product',
|
||||
'service': 'Service',
|
||||
'taxonomy': 'Taxonomy',
|
||||
'page': 'Page',
|
||||
};
|
||||
return (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{typeLabels[entityType] || entityType}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'cluster_role',
|
||||
label: 'Role',
|
||||
sortable: true,
|
||||
sortField: 'cluster_role',
|
||||
width: '100px',
|
||||
defaultVisible: false,
|
||||
render: (value: string, row: Task) => {
|
||||
const role = value || row.cluster_role;
|
||||
if (!role) {
|
||||
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||
}
|
||||
const roleColors: Record<string, 'primary' | 'success' | 'warning'> = {
|
||||
'hub': 'primary',
|
||||
'supporting': 'success',
|
||||
'attribute': 'warning',
|
||||
};
|
||||
return (
|
||||
<Badge color={roleColors[role] || 'primary'} size="sm" variant="light">
|
||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'taxonomy_name',
|
||||
label: 'Taxonomy',
|
||||
@@ -210,29 +158,48 @@ export const createTasksPageConfig = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'content_type',
|
||||
label: 'Content Type',
|
||||
sortable: true,
|
||||
sortField: 'content_type',
|
||||
width: '120px',
|
||||
render: (value: string) => {
|
||||
const typeLabels: Record<string, string> = {
|
||||
'post': 'Post',
|
||||
'page': 'Page',
|
||||
'product': 'Product',
|
||||
'service': 'Service',
|
||||
'category': 'Category',
|
||||
'tag': 'Tag',
|
||||
};
|
||||
return (
|
||||
<Badge color="primary" size="sm" variant="light">
|
||||
{typeLabels[value] || value || '-'}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'content_structure',
|
||||
label: 'Structure',
|
||||
sortable: true,
|
||||
sortField: 'content_structure',
|
||||
width: '150px',
|
||||
render: (value: string) => (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{value?.replace('_', ' ') || '-'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'content_type',
|
||||
label: 'Type',
|
||||
sortable: true,
|
||||
sortField: 'content_type',
|
||||
width: '120px',
|
||||
render: (value: string) => (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{value?.replace('_', ' ') || '-'}
|
||||
</Badge>
|
||||
),
|
||||
render: (value: string) => {
|
||||
const structureLabels: Record<string, string> = {
|
||||
'article': 'Article',
|
||||
'listicle': 'Listicle',
|
||||
'guide': 'Guide',
|
||||
'comparison': 'Comparison',
|
||||
'product_page': 'Product Page',
|
||||
};
|
||||
return (
|
||||
<Badge color="info" size="sm" variant="light">
|
||||
{structureLabels[value] || value || '-'}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...statusColumn,
|
||||
@@ -362,39 +329,31 @@ export const createTasksPageConfig = (
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_structure',
|
||||
label: 'Structure',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Structures' },
|
||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
||||
{ value: 'landing_page', label: 'Landing Page' },
|
||||
{ value: 'pillar_page', label: 'Pillar Page' },
|
||||
{ value: 'supporting_page', label: 'Supporting Page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_type',
|
||||
label: 'Type',
|
||||
label: 'Content Type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Types' },
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'tutorial', label: 'Tutorial' },
|
||||
{ value: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'category', label: 'Category' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: 'Source',
|
||||
key: 'content_structure',
|
||||
label: 'Content Structure',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Sources' },
|
||||
{ value: 'site_builder', label: 'Site Builder' },
|
||||
{ value: 'ideas', label: 'Ideas' },
|
||||
{ value: 'manual', label: 'Manual' },
|
||||
{ value: '', label: 'All Structures' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'listicle', label: 'Listicle' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'comparison', label: 'Comparison' },
|
||||
{ value: 'product_page', label: 'Product Page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -409,21 +368,6 @@ export const createTasksPageConfig = (
|
||||
})(),
|
||||
dynamicOptions: 'clusters',
|
||||
},
|
||||
// Stage 3: Entity type filter
|
||||
{
|
||||
key: 'entity_type',
|
||||
label: 'Entity Type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Entity Types' },
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'taxonomy', label: 'Taxonomy' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
],
|
||||
},
|
||||
],
|
||||
formFields: (clusters: Array<{ id: number; name: string }>) => [
|
||||
{
|
||||
@@ -473,28 +417,31 @@ export const createTasksPageConfig = (
|
||||
key: 'content_structure',
|
||||
label: 'Content Structure',
|
||||
type: 'select',
|
||||
value: handlers.formData.content_structure || 'blog_post',
|
||||
value: handlers.formData.content_structure || 'article',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, content_structure: value }),
|
||||
options: [
|
||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
||||
{ value: 'landing_page', label: 'Landing Page' },
|
||||
{ value: 'pillar_page', label: 'Pillar Page' },
|
||||
{ value: 'supporting_page', label: 'Supporting Page' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'listicle', label: 'Listicle' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'comparison', label: 'Comparison' },
|
||||
{ value: 'product_page', label: 'Product Page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_type',
|
||||
label: 'Content Type',
|
||||
type: 'select',
|
||||
value: handlers.formData.content_type || 'blog_post',
|
||||
value: handlers.formData.content_type || 'post',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, content_type: value }),
|
||||
options: [
|
||||
{ value: 'blog_post', label: 'Blog Post' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'guide', label: 'Guide' },
|
||||
{ value: 'tutorial', label: 'Tutorial' },
|
||||
{ value: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'category', label: 'Category' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user