test if works or revert

This commit is contained in:
alorig
2025-11-29 11:23:42 +05:00
parent 0b3830c891
commit e9e0de40d0
10 changed files with 422 additions and 85 deletions

View File

@@ -210,8 +210,8 @@ export default function App() {
</Suspense>
} />
{/* Writer Module - Redirect dashboard to content */}
<Route path="/writer" element={<Navigate to="/writer/content" replace />} />
{/* Writer Module - Redirect dashboard to tasks */}
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
<Route path="/writer/tasks" element={
<Suspense fallback={null}>
<ModuleGuard module="writer">

View File

@@ -59,29 +59,8 @@ 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 (
<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 */}
<div className="relative">
{image.status === 'pending' && (

View File

@@ -83,8 +83,10 @@ export const createContentPageConfig = (
setStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void;
onViewContent?: (row: Content) => void;
enableToggleContent?: boolean; // If false, do not add toggleable content column behavior
}
): ContentPageConfig => {
const enableToggle = handlers.enableToggleContent !== false;
const showSectorColumn = !handlers.activeSector;
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
@@ -99,9 +101,9 @@ export const createContentPageConfig = (
...titleColumn,
sortable: true,
sortField: 'title',
toggleable: true,
toggleContentKey: 'content_html',
toggleContentLabel: 'Generated Content',
toggleable: enableToggle,
toggleContentKey: enableToggle ? 'content_html' : undefined,
toggleContentLabel: enableToggle ? 'Generated Content' : undefined,
render: (value: string, row: Content) => (
<div>
{handlers.onViewContent ? (

View File

@@ -4,6 +4,7 @@
*/
import { Content } from '../../services/api';
import { Link } from 'react-router-dom';
import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date';
import { CheckCircleIcon } from '../../icons';
@@ -56,44 +57,61 @@ export function createReviewPageConfig(params: {
const showSectorColumn = !params.activeSector;
const columns: ColumnConfig[] = [
{
key: 'categories',
label: 'Categories',
sortable: false,
width: '180px',
render: (_value: any, row: Content) => {
const categories = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'category') || [];
if (!categories.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
return (
<div className="flex flex-wrap gap-1">
{categories.map((cat: any) => (
<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>
);
},
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: true,
defaultVisible: false,
},
// Title first, then categories and tags (moved after title per change request)
{
key: 'title',
label: 'Title',
sortable: true,
sortField: 'title',
toggleable: true,
toggleContentKey: 'content_html',
toggleContentLabel: 'Generated Content',
render: (value: string, row: Content) => (
<div className="flex items-center gap-2">
<Link to={`/writer/content/${row.id}`} className="font-medium text-gray-900 dark:text-white hover:underline">
{value || `Content #${row.id}`}
</Link>
</div>
),
},
{
key: 'categories',
label: 'Categories',
sortable: false,
width: '180px',
render: (_value: any, row: Content) => {
const categories = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'category') || [];
if (!categories.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
return (
<div className="flex flex-wrap gap-1">
{categories.map((cat: any) => (
<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>
);
},
toggleable: false,
defaultVisible: true,
},
{
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',
label: 'Title',

View File

@@ -0,0 +1,24 @@
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;
}

View File

@@ -130,7 +130,7 @@ const AppSidebar: React.FC = () => {
workflowItems.push({
icon: <TaskIcon />,
name: "Writer",
path: "/writer/content", // Default to content, submenus shown as in-page navigation
path: "/writer/tasks", // Default to tasks (changed from content)
});
}

View File

@@ -20,7 +20,6 @@ import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import ProgressModal from '../../components/common/ProgressModal';
import { useProgressModal } from '../../hooks/useProgressModal';
import ContentViewerModal from '../../components/common/ContentViewerModal';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
@@ -50,9 +49,6 @@ export default function Content() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
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
const progressModal = useProgressModal();
@@ -138,11 +134,10 @@ export default function Content() {
setCurrentPage(1);
};
// Handle view content
// Handle view content - navigate to content view instead of opening modal
const handleViewContent = useCallback((row: ContentType) => {
setViewerContent(row);
setIsViewerModalOpen(true);
}, []);
navigate(`/writer/content/${row.id}`);
}, [navigate]);
// Create page config
const pageConfig = useMemo(() => {
@@ -154,6 +149,7 @@ export default function Content() {
setStatusFilter,
setCurrentPage,
onViewContent: handleViewContent,
enableToggleContent: false, // Disable dropdown toggle on this page; open full view instead
});
}, [
activeSector,
@@ -300,16 +296,7 @@ export default function Content() {
}}
/>
{/* Content Viewer Modal */}
<ContentViewerModal
isOpen={isViewerModalOpen}
onClose={() => {
setIsViewerModalOpen(false);
setViewerContent(null);
}}
title={viewerContent?.title || 'Content'}
contentHtml={viewerContent?.content_html || ''}
/>
{/* Content view opens in its own route; modal removed */}
{/* Progress Modal for AI Functions */}
<ProgressModal

View File

@@ -16,7 +16,10 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Content, fetchImages, ImageRecord } from '../services/api';
import { useNavigate } from 'react-router-dom';
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';
interface ContentViewTemplateProps {
@@ -591,6 +594,59 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
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) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
@@ -677,7 +733,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">
{/* Header Section */}
<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 items-center gap-3 mb-3">
<FileTextIcon className="w-6 h-6" />
@@ -694,6 +750,34 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
</p>
)}
</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>

View File

@@ -0,0 +1,190 @@
# 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 12: 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.

View File

@@ -0,0 +1,53 @@
# 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.