@@ -210,8 +210,8 @@ export default function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
{/* Writer Module - Redirect dashboard to tasks */}
|
{/* Writer Module - Redirect dashboard to content */}
|
||||||
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
|
<Route path="/writer" element={<Navigate to="/writer/content" replace />} />
|
||||||
<Route path="/writer/tasks" element={
|
<Route path="/writer/tasks" element={
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ModuleGuard module="writer">
|
<ModuleGuard module="writer">
|
||||||
|
|||||||
@@ -59,8 +59,29 @@ export default function ContentImageCell({ image, maxPromptLength = 100 }: Conte
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prompt = image.prompt || '';
|
||||||
|
const shouldTruncate = prompt.length > maxPromptLength;
|
||||||
|
const displayPrompt = showFullPrompt || !shouldTruncate ? prompt : `${prompt.substring(0, maxPromptLength)}...`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
{/* Prompt Text */}
|
||||||
|
{prompt && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
|
{displayPrompt}
|
||||||
|
{shouldTruncate && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFullPrompt(!showFullPrompt)}
|
||||||
|
className="ml-1 text-brand-500 hover:text-brand-600 text-xs"
|
||||||
|
>
|
||||||
|
{showFullPrompt ? 'Show less' : 'Show more'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Image Display */}
|
{/* Image Display */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{image.status === 'pending' && (
|
{image.status === 'pending' && (
|
||||||
|
|||||||
@@ -83,10 +83,8 @@ export const createContentPageConfig = (
|
|||||||
setStatusFilter: (value: string) => void;
|
setStatusFilter: (value: string) => void;
|
||||||
setCurrentPage: (page: number) => void;
|
setCurrentPage: (page: number) => void;
|
||||||
onViewContent?: (row: Content) => void;
|
onViewContent?: (row: Content) => void;
|
||||||
enableToggleContent?: boolean; // If false, do not add toggleable content column behavior
|
|
||||||
}
|
}
|
||||||
): ContentPageConfig => {
|
): ContentPageConfig => {
|
||||||
const enableToggle = handlers.enableToggleContent !== false;
|
|
||||||
const showSectorColumn = !handlers.activeSector;
|
const showSectorColumn = !handlers.activeSector;
|
||||||
|
|
||||||
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
|
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
|
||||||
@@ -101,9 +99,9 @@ export const createContentPageConfig = (
|
|||||||
...titleColumn,
|
...titleColumn,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
sortField: 'title',
|
sortField: 'title',
|
||||||
toggleable: enableToggle,
|
toggleable: true,
|
||||||
toggleContentKey: enableToggle ? 'content_html' : undefined,
|
toggleContentKey: 'content_html',
|
||||||
toggleContentLabel: enableToggle ? 'Generated Content' : undefined,
|
toggleContentLabel: 'Generated Content',
|
||||||
render: (value: string, row: Content) => (
|
render: (value: string, row: Content) => (
|
||||||
<div>
|
<div>
|
||||||
{handlers.onViewContent ? (
|
{handlers.onViewContent ? (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Content } from '../../services/api';
|
import { Content } from '../../services/api';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import { formatRelativeDate } from '../../utils/date';
|
import { formatRelativeDate } from '../../utils/date';
|
||||||
import { CheckCircleIcon } from '../../icons';
|
import { CheckCircleIcon } from '../../icons';
|
||||||
@@ -57,61 +56,44 @@ export function createReviewPageConfig(params: {
|
|||||||
const showSectorColumn = !params.activeSector;
|
const showSectorColumn = !params.activeSector;
|
||||||
|
|
||||||
const columns: ColumnConfig[] = [
|
const columns: ColumnConfig[] = [
|
||||||
// Title first, then categories and tags (moved after title per change request)
|
{
|
||||||
{
|
key: 'categories',
|
||||||
key: 'title',
|
label: 'Categories',
|
||||||
label: 'Title',
|
sortable: false,
|
||||||
sortable: true,
|
width: '180px',
|
||||||
sortField: 'title',
|
render: (_value: any, row: Content) => {
|
||||||
toggleable: true,
|
const categories = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'category') || [];
|
||||||
toggleContentKey: 'content_html',
|
if (!categories.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||||
toggleContentLabel: 'Generated Content',
|
return (
|
||||||
render: (value: string, row: Content) => (
|
<div className="flex flex-wrap gap-1">
|
||||||
<div className="flex items-center gap-2">
|
{categories.map((cat: any) => (
|
||||||
<Link to={`/writer/content/${row.id}`} className="font-medium text-gray-900 dark:text-white hover:underline">
|
<span key={cat.id} className="px-2 py-0.5 bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 rounded-full text-xs font-medium">{cat.name}</span>
|
||||||
{value || `Content #${row.id}`}
|
))}
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
);
|
||||||
),
|
},
|
||||||
},
|
toggleable: true,
|
||||||
{
|
defaultVisible: false,
|
||||||
key: 'categories',
|
},
|
||||||
label: 'Categories',
|
{
|
||||||
sortable: false,
|
key: 'tags',
|
||||||
width: '180px',
|
label: 'Tags',
|
||||||
render: (_value: any, row: Content) => {
|
sortable: false,
|
||||||
const categories = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'category') || [];
|
width: '180px',
|
||||||
if (!categories.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
render: (_value: any, row: Content) => {
|
||||||
return (
|
const tags = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'tag') || [];
|
||||||
<div className="flex flex-wrap gap-1">
|
if (!tags.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
||||||
{categories.map((cat: any) => (
|
return (
|
||||||
<span key={cat.id} className="px-2 py-0.5 bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 rounded-full text-xs font-medium">{cat.name}</span>
|
<div className="flex flex-wrap gap-1">
|
||||||
))}
|
{tags.map((tag: any) => (
|
||||||
</div>
|
<span key={tag.id} className="px-2 py-0.5 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 rounded-full text-xs font-medium">{tag.name}</span>
|
||||||
);
|
))}
|
||||||
},
|
</div>
|
||||||
toggleable: false,
|
);
|
||||||
defaultVisible: true,
|
},
|
||||||
},
|
toggleable: true,
|
||||||
{
|
defaultVisible: false,
|
||||||
key: 'tags',
|
},
|
||||||
label: 'Tags',
|
|
||||||
sortable: false,
|
|
||||||
width: '180px',
|
|
||||||
render: (_value: any, row: Content) => {
|
|
||||||
const tags = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'tag') || [];
|
|
||||||
if (!tags.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{tags.map((tag: any) => (
|
|
||||||
<span key={tag.id} className="px-2 py-0.5 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 rounded-full text-xs font-medium">{tag.name}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
toggleable: false,
|
|
||||||
defaultVisible: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
label: 'Title',
|
label: 'Title',
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useSettingsStore } from '../store/settingsStore';
|
|
||||||
|
|
||||||
// Simple client-side feature flag hook backed by accountSettings
|
|
||||||
export function useFeatureFlag(key: string): boolean {
|
|
||||||
const accountSettings = useSettingsStore((s) => s.accountSettings);
|
|
||||||
const setting = accountSettings?.[key];
|
|
||||||
const enabled = useMemo(() => {
|
|
||||||
if (!setting || !setting.config) return false;
|
|
||||||
try {
|
|
||||||
// Expect config to be boolean or { enabled: boolean }
|
|
||||||
if (typeof setting.config === 'boolean') return Boolean(setting.config);
|
|
||||||
if (typeof setting.config === 'object' && setting.config !== null && 'enabled' in setting.config) {
|
|
||||||
return Boolean((setting.config as any).enabled);
|
|
||||||
}
|
|
||||||
return Boolean(setting.config);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [setting]);
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <TaskIcon />,
|
icon: <TaskIcon />,
|
||||||
name: "Writer",
|
name: "Writer",
|
||||||
path: "/writer/tasks", // Default to tasks (changed from content)
|
path: "/writer/content", // Default to content, submenus shown as in-page navigation
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useSectorStore } from '../../store/sectorStore';
|
|||||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
import ProgressModal from '../../components/common/ProgressModal';
|
import ProgressModal from '../../components/common/ProgressModal';
|
||||||
import { useProgressModal } from '../../hooks/useProgressModal';
|
import { useProgressModal } from '../../hooks/useProgressModal';
|
||||||
|
import ContentViewerModal from '../../components/common/ContentViewerModal';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||||
@@ -49,6 +50,9 @@ export default function Content() {
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
|
// Content viewer modal state
|
||||||
|
const [isViewerModalOpen, setIsViewerModalOpen] = useState(false);
|
||||||
|
const [viewerContent, setViewerContent] = useState<ContentType | null>(null);
|
||||||
|
|
||||||
// Progress modal for AI functions
|
// Progress modal for AI functions
|
||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
@@ -134,10 +138,11 @@ export default function Content() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle view content - navigate to content view instead of opening modal
|
// Handle view content
|
||||||
const handleViewContent = useCallback((row: ContentType) => {
|
const handleViewContent = useCallback((row: ContentType) => {
|
||||||
navigate(`/writer/content/${row.id}`);
|
setViewerContent(row);
|
||||||
}, [navigate]);
|
setIsViewerModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Create page config
|
// Create page config
|
||||||
const pageConfig = useMemo(() => {
|
const pageConfig = useMemo(() => {
|
||||||
@@ -149,7 +154,6 @@ export default function Content() {
|
|||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
onViewContent: handleViewContent,
|
onViewContent: handleViewContent,
|
||||||
enableToggleContent: false, // Disable dropdown toggle on this page; open full view instead
|
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
activeSector,
|
activeSector,
|
||||||
@@ -296,7 +300,16 @@ export default function Content() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content view opens in its own route; modal removed */}
|
{/* Content Viewer Modal */}
|
||||||
|
<ContentViewerModal
|
||||||
|
isOpen={isViewerModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsViewerModalOpen(false);
|
||||||
|
setViewerContent(null);
|
||||||
|
}}
|
||||||
|
title={viewerContent?.title || 'Content'}
|
||||||
|
contentHtml={viewerContent?.content_html || ''}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Progress Modal for AI Functions */}
|
{/* Progress Modal for AI Functions */}
|
||||||
<ProgressModal
|
<ProgressModal
|
||||||
|
|||||||
@@ -16,10 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Content, fetchImages, ImageRecord } from '../services/api';
|
||||||
import { Content, fetchImages, ImageRecord, publishContent, generateImagePrompts } from '../services/api';
|
|
||||||
import { useToast } from '../components/ui/toast/ToastContainer';
|
|
||||||
import { useFeatureFlag } from '../hooks/useFeatureFlag';
|
|
||||||
import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon } from '../icons';
|
import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon } from '../icons';
|
||||||
|
|
||||||
interface ContentViewTemplateProps {
|
interface ContentViewTemplateProps {
|
||||||
@@ -594,59 +591,6 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
|
|
||||||
const shouldShowFeaturedBlock = imagesLoading || Boolean(resolvedFeaturedImage);
|
const shouldShowFeaturedBlock = imagesLoading || Boolean(resolvedFeaturedImage);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const toast = useToast();
|
|
||||||
const [publishing, setPublishing] = useState(false);
|
|
||||||
const [generatingImages, setGeneratingImages] = useState(false);
|
|
||||||
const featureEnabled = useFeatureFlag('feature.content_manager_refactor');
|
|
||||||
|
|
||||||
const handleEditContent = () => {
|
|
||||||
const siteId = content?.site ?? content?.site_id ?? null;
|
|
||||||
if (!siteId || !content?.id) {
|
|
||||||
toast.error('Site or content id missing');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate(`/sites/${siteId}/posts/${content.id}/edit`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerateImages = async () => {
|
|
||||||
if (!content?.id) return;
|
|
||||||
try {
|
|
||||||
setGeneratingImages(true);
|
|
||||||
const result = await generateImagePrompts([content.id]);
|
|
||||||
if (result && result.success) {
|
|
||||||
toast.success('Image generation started');
|
|
||||||
// If async task_id returned, open progress modal elsewhere; refresh images after short delay
|
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
|
||||||
} else {
|
|
||||||
toast.error(result?.error || 'Failed to start image generation');
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(`Failed to generate images: ${e?.message || e}`);
|
|
||||||
} finally {
|
|
||||||
setGeneratingImages(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePublish = async () => {
|
|
||||||
if (!content?.id) return;
|
|
||||||
try {
|
|
||||||
setPublishing(true);
|
|
||||||
const result = await publishContent(content.id);
|
|
||||||
if (result && (result.external_url || result.external_id)) {
|
|
||||||
toast.success('Content published successfully');
|
|
||||||
// Reload to show updated external_id/status
|
|
||||||
setTimeout(() => window.location.reload(), 800);
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to publish content');
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(`Publish failed: ${e?.message || e}`);
|
|
||||||
} finally {
|
|
||||||
setPublishing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
@@ -733,7 +677,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="bg-gradient-to-r from-brand-500 to-brand-600 px-8 py-6 text-white">
|
<div className="bg-gradient-to-r from-brand-500 to-brand-600 px-8 py-6 text-white">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<FileTextIcon className="w-6 h-6" />
|
<FileTextIcon className="w-6 h-6" />
|
||||||
@@ -750,34 +694,6 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 flex items-center gap-2">
|
|
||||||
{featureEnabled && (content.status === 'draft' || content.status === 'review') && (
|
|
||||||
<button
|
|
||||||
onClick={handleEditContent}
|
|
||||||
className="inline-flex items-center gap-2 px-3 py-2 bg-white text-gray-700 rounded-lg border border-gray-200 hover:bg-gray-50 transition"
|
|
||||||
>
|
|
||||||
Edit content
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{featureEnabled && content.status === 'draft' && (
|
|
||||||
<button
|
|
||||||
onClick={handleGenerateImages}
|
|
||||||
disabled={generatingImages}
|
|
||||||
className="inline-flex items-center gap-2 px-3 py-2 bg-white text-gray-700 rounded-lg border border-gray-200 hover:bg-gray-50 transition"
|
|
||||||
>
|
|
||||||
{generatingImages ? 'Generating...' : 'Generate images'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{featureEnabled && content.status === 'review' && (
|
|
||||||
<button
|
|
||||||
onClick={handlePublish}
|
|
||||||
disabled={publishing}
|
|
||||||
className="inline-flex items-center gap-2 px-3 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition"
|
|
||||||
>
|
|
||||||
{publishing ? 'Publishing...' : 'Publish'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
# Content Manager Removal & Refactor Plan
|
|
||||||
|
|
||||||
**Purpose**: Provide a safe, staged, reversible plan to remove the Content Manager UI and perform the requested content-manager refactor while preserving Writer pages and integrations. This file is a single-source plan for developers and QA.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Objectives
|
|
||||||
|
|
||||||
- Safely remove/hide the Content Manager dashboard UI and related code paths without breaking Writer flows: Tasks, Content (detail), Images, Published, Sites, Site integration, and WordPress sync.
|
|
||||||
- Implement requested UI changes (three buttons, metadata block, table column changes) behind a feature flag.
|
|
||||||
- Ensure background syncs, plugin webhooks, and publish adapters continue to function.
|
|
||||||
- Do not perform destructive DB schema removals during initial rollout; deprecate first, remove later after validations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## High-level Phases
|
|
||||||
|
|
||||||
1. Preparation & discovery (completed)
|
|
||||||
2. Feature-flag gating & compatibility proxies
|
|
||||||
3. Frontend hide + UI refactor (non-destructive)
|
|
||||||
4. Backend normalization & compatibility layer
|
|
||||||
5. Staged cleanup & code removal
|
|
||||||
6. Tests, rollout, monitoring, and rollback
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature flag and admin control
|
|
||||||
|
|
||||||
- Add server-side feature flag `feature.content_manager_refactor` (default OFF in production).
|
|
||||||
- Add admin option `enable_content_manager_removal_mode` to WP plugin and backend config for controlled toggles.
|
|
||||||
- All major UI changes and route behavior changes must be gated behind the feature flag.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Per-file action plan (first sprint - non-destructive)
|
|
||||||
|
|
||||||
- frontend/src/layout/AppSidebar.tsx
|
|
||||||
- Change Writer menu link default from `/writer/content` to `/writer/tasks` (behind flag).
|
|
||||||
|
|
||||||
- frontend/src/config/routes.config.ts
|
|
||||||
- Ensure `/writer` redirects to `/writer/tasks` when flag enabled.
|
|
||||||
|
|
||||||
- frontend/src/pages/Writer/Content.tsx
|
|
||||||
- Hide inline body toggle and modal behavior behind flag; ensure title click navigates to content detail.
|
|
||||||
|
|
||||||
- frontend/src/components/common/ToggleTableRow.tsx
|
|
||||||
- Remove usage on Content list (do not delete component globally).
|
|
||||||
|
|
||||||
- frontend/src/pages/Writer/ContentView.tsx (or ContentViewTemplate)
|
|
||||||
- Add metadata block (Cluster, Sector, Categories, Tags).
|
|
||||||
- Add three buttons with conditional visibility (Edit content, Generate images, Publish).
|
|
||||||
|
|
||||||
- frontend/src/pages/Writer/Review.tsx
|
|
||||||
- Add Categories and Tags columns after Title; make Title clickable to open ContentView.
|
|
||||||
|
|
||||||
- frontend/src/pages/Writer/Images.tsx
|
|
||||||
- Remove image prompt column; render image and status badge stacked.
|
|
||||||
|
|
||||||
- frontend/src/config/pages/content.config.tsx and table-actions.config.tsx
|
|
||||||
- Add/adjust columns and action visibility rules to support the UI changes.
|
|
||||||
|
|
||||||
- frontend/src/templates/TablePageTemplate.tsx
|
|
||||||
- Persist column selector state per-page using localStorage key `table-columns:${routePath}`.
|
|
||||||
|
|
||||||
- frontend/src/services/api.ts
|
|
||||||
- Keep `publishContent`, `unpublishContent`, `generateImagePrompts`, `fetchContentById` unchanged; add wrappers only if needed for compatibility.
|
|
||||||
|
|
||||||
- backend/igny8_core/modules/writer/views.py
|
|
||||||
- Keep `ContentViewSet` endpoints; ensure `publish()` writes `external_id` & `external_url` and has robust validation.
|
|
||||||
|
|
||||||
- backend/igny8_core/business/integration/services/content_sync_service.py
|
|
||||||
- Normalize status usage (`published` not `publish`) and set canonical `content.external_id` on successful publish.
|
|
||||||
|
|
||||||
- backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py
|
|
||||||
- Confirm adapter returns `external_id` and `url` reliably and handles media errors gracefully.
|
|
||||||
|
|
||||||
- igny8-wp-integration/sync/igny8-to-wp.php
|
|
||||||
- Add fallback: if `task['content_id']` is present, fetch `GET /v1/writer/content/{content_id}/` and use `content_html` for post body; fallback to legacy `task['content']` if content fetch fails.
|
|
||||||
|
|
||||||
- igny8-wp-integration/includes/functions.php
|
|
||||||
- Ensure cron jobs and connection toggles can be disabled via admin option to stop sync during rollback.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UI Requirements & Exact Behavior
|
|
||||||
|
|
||||||
1. Content detail `/writer/content/:id` (ContentView)
|
|
||||||
- Metadata block (top-right or top section):
|
|
||||||
- Cluster: label + cluster name (link to cluster detail if clicked)
|
|
||||||
- Sector: label + value
|
|
||||||
- Categories and Tags: chips below Sector
|
|
||||||
- Buttons (top toolbar):
|
|
||||||
- Edit content
|
|
||||||
- Visible when `status` ∈ {`draft`, `review`}.
|
|
||||||
- Action: navigate to PostEditor (use existing PostEditor route, pass `content_id`).
|
|
||||||
- Generate images
|
|
||||||
- Visible when `status` == `draft`.
|
|
||||||
- Action: call `POST /v1/writer/content/{id}/generate_image_prompts/`; show progress modal if `task_id` returned.
|
|
||||||
- Publish
|
|
||||||
- Visible when `status` == `review`.
|
|
||||||
- Action: call `POST /v1/writer/content/{id}/publish/`; on success display toast with `external_url` and refresh.
|
|
||||||
|
|
||||||
2. `/writer/content` list
|
|
||||||
- Remove dropdown row-toggle that shows body.
|
|
||||||
- Clicking Title navigates to `/writer/content/{id}` (no modal).
|
|
||||||
|
|
||||||
3. `/writer/review`
|
|
||||||
- Table columns: [Select] [Title (clickable)] [Categories] [Tags] [Cluster] [Status] [Actions].
|
|
||||||
|
|
||||||
4. `/writer/images`
|
|
||||||
- Remove 'image prompt' column.
|
|
||||||
- Image column shows: thumbnail (top) and status badge (below) as vertical stack in a single column.
|
|
||||||
|
|
||||||
5. Table column persistence
|
|
||||||
- Store selected columns per route in `localStorage` key `table-columns:${routePath}`.
|
|
||||||
- On mount, load and apply selection.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data flow & canonical fields (reference)
|
|
||||||
|
|
||||||
- Task (lightweight): id, title, cluster_id, content_type, content_structure, taxonomy_term_id, keywords (M2M), status (`queued`|`completed`).
|
|
||||||
- Content (canonical body): id, title, content_html, ai_raw/json (if present), content_type, content_structure, cluster_id, taxonomy_terms (M2M), source (`igny8`|`wordpress`), status (`draft`|`published`), external_id, external_url.
|
|
||||||
- WordPress: expect adapter to return `external_id` and `url`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend normalization & compatibility
|
|
||||||
|
|
||||||
- Replace any uses of `status == 'publish'` with `status == 'published'` (e.g., in `ContentSyncService`).
|
|
||||||
- Standardize storing WP post id in `content.external_id` (not only in `content.metadata['wordpress_id']`).
|
|
||||||
- Keep old metadata keys supported for one release by copying `metadata['wordpress_id']` → `external_id` (migration or compatibility write).
|
|
||||||
- Plugin fallback: if webhook provides `task.content_id`, plugin must fetch content detail for `content_html`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests & verification
|
|
||||||
|
|
||||||
- Unit tests (backend)
|
|
||||||
- `ContentViewSet.publish()` updates `external_id` & `external_url` and content status.
|
|
||||||
- `ContentSyncService` picks up `published` content and updates `external_id`.
|
|
||||||
- Frontend unit tests
|
|
||||||
- ContentView button visibility by status.
|
|
||||||
- Review table shows categories/tags and clickable title.
|
|
||||||
- Integration/E2E tests
|
|
||||||
- End-to-end flow: Idea → Task → Generate content → verify Content saved → generate images → publish → WP post created → `content.external_id` present.
|
|
||||||
- Plugin webhook simulation
|
|
||||||
- Simulate `task_published` with `content_id` present; verify WP post has full body from `content_html` and metadata keys set.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback & monitoring
|
|
||||||
|
|
||||||
- Quick rollback: flip `feature.content_manager_refactor` OFF.
|
|
||||||
- If publish failures spike: set WP plugin option `igny8_connection_enabled` = 0 to stop cron and inbound sync.
|
|
||||||
- Instrument logs for publish path and plugin webhook calls; monitor error rates, failed publishes, missing credentials.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timeline & sprint breakdown (first sprint - non-destructive)
|
|
||||||
|
|
||||||
- Day 0.5: Feature flag & admin option + small plugin fallback change.
|
|
||||||
- Day 1–2: Frontend changes (hide old UI, implement ContentView metadata + buttons behind flag, review/images updates, column persistence).
|
|
||||||
- Day 0.5: Backend normalization: `ContentSyncService` status normalization and `external_id` writes; confirm `publish()` stable.
|
|
||||||
- Day 1: Tests, staging deploy, integration E2E.
|
|
||||||
- Day 0.5: Staged canary release and monitoring.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deliverables for first sprint
|
|
||||||
|
|
||||||
- `master-docs/CONTENT-MANAGER-REMOVAL-PLAN.md` (this file)
|
|
||||||
- Feature flag implemented and documented
|
|
||||||
- Frontend UI changes behind flag: ContentView metadata/buttons, Review table updates, Images table updates, Column persistence
|
|
||||||
- Backend fixes: status normalization, external_id unification
|
|
||||||
- WP plugin fallback to fetch `content_html` when `content_id` present
|
|
||||||
- Tests and staging validation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next steps I will take if you approve
|
|
||||||
|
|
||||||
1. Produce exact per-file diffs for the first sprint (safe edits).
|
|
||||||
2. Optionally implement the edits and run the test suite in staging.
|
|
||||||
|
|
||||||
If you want me to generate the diffs now, say "generate diffs" and I will produce the changes for review.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
End of plan.
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Content Manager Removal — Tests, E2E Flows & Rollback
|
|
||||||
|
|
||||||
Purpose: a concise, actionable test plan and rollback instructions to validate the first non-destructive sprint before full cleanup.
|
|
||||||
|
|
||||||
1) Smoke tests (manual or scriptable)
|
|
||||||
- Create content from Planner → verify Content record exists with `content_html` populated.
|
|
||||||
- Generate image prompts for a Content item (draft) → verify image prompt records created and UI shows "Image Prompts Generated".
|
|
||||||
- Generate images → verify images tasks start and images appear in `/writer/images`.
|
|
||||||
- Move Content to `review` status → call Publish → verify backend returns `external_id` and `external_url` set on Content and WP plugin creates post.
|
|
||||||
- Trigger plugin webhook simulation with `task` payload that includes `content_id` → verify WP post body uses `content_html`.
|
|
||||||
|
|
||||||
2) Unit tests (backend)
|
|
||||||
- `ContentViewSet.publish()`:
|
|
||||||
- publishes and sets `external_id` and `external_url`.
|
|
||||||
- rejects publish when site credentials missing.
|
|
||||||
- `ContentSyncService._sync_to_wordpress`:
|
|
||||||
- queries `status='published'` (not 'publish').
|
|
||||||
- writes `content.external_id` on success.
|
|
||||||
|
|
||||||
3) Frontend unit tests
|
|
||||||
- `ContentViewTemplate`:
|
|
||||||
- buttons visibility by `status` (`draft`: Edit + Generate; `review`: Edit + Publish).
|
|
||||||
- Edit button navigates to `/sites/{siteId}/posts/{contentId}/edit`.
|
|
||||||
- `Review` page:
|
|
||||||
- Title renders as link to `/writer/content/{id}`.
|
|
||||||
- Categories and Tags columns appear and persist via ColumnSelector.
|
|
||||||
- `Images` page:
|
|
||||||
- Content image cells do not show prompt text, only image and status badge.
|
|
||||||
|
|
||||||
4) E2E test (recommended - scriptable)
|
|
||||||
- Flow:
|
|
||||||
1. Create Idea → create Task → generate content (AI) → assert Content record created.
|
|
||||||
2. For created Content (status draft) call generate image prompts → start generation → wait for images generated.
|
|
||||||
3. Change status to review → call publish endpoint → assert WP post exists via plugin test endpoint and `external_id` present.
|
|
||||||
|
|
||||||
5) Monitoring & metrics to watch in staging
|
|
||||||
- Publish success rate (per-minute/hour).
|
|
||||||
- WP plugin webhook failures and missing credentials.
|
|
||||||
- Content with `content_id` but empty `content_html`.
|
|
||||||
|
|
||||||
6) Rollback steps (fast)
|
|
||||||
- Flip account setting `feature.content_manager_refactor` OFF (server-side account setting) — this hides/refuses new UI.
|
|
||||||
- If publish failures spike, set WP plugin option `igny8_connection_enabled` = 0 to stop outbound syncs and webhooks.
|
|
||||||
- Revert UI commits in the release branch and redeploy.
|
|
||||||
|
|
||||||
7) Test artifacts
|
|
||||||
- Store E2E run logs and failing request/response pairs in `staging/e2e-runs/{timestamp}/`.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
- Automate the E2E with Cypress or Playwright; prefer Playwright for headless CI runs.
|
|
||||||
- Use test WP site with test credentials; do not use production credentials for staging tests.
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user