From e9e0de40d01f1b5d231f9bb1f18a12e57b644818 Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Sat, 29 Nov 2025 11:23:42 +0500 Subject: [PATCH] test if works or revert --- frontend/src/App.tsx | 4 +- .../components/common/ContentImageCell.tsx | 21 -- frontend/src/config/pages/content.config.tsx | 8 +- frontend/src/config/pages/review.config.tsx | 94 +++++---- frontend/src/hooks/useFeatureFlag.ts | 24 +++ frontend/src/layout/AppSidebar.tsx | 2 +- frontend/src/pages/Writer/Content.tsx | 23 +-- .../src/templates/ContentViewTemplate.tsx | 88 +++++++- master-docs/CONTENT-MANAGER-REMOVAL-PLAN.md | 190 ++++++++++++++++++ master-docs/CONTENT-MANAGER-REMOVAL-TESTS.md | 53 +++++ 10 files changed, 422 insertions(+), 85 deletions(-) create mode 100644 frontend/src/hooks/useFeatureFlag.ts create mode 100644 master-docs/CONTENT-MANAGER-REMOVAL-PLAN.md create mode 100644 master-docs/CONTENT-MANAGER-REMOVAL-TESTS.md diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3982f674..d5cbfe25 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -210,8 +210,8 @@ export default function App() { } /> - {/* Writer Module - Redirect dashboard to content */} - } /> + {/* Writer Module - Redirect dashboard to tasks */} + } /> diff --git a/frontend/src/components/common/ContentImageCell.tsx b/frontend/src/components/common/ContentImageCell.tsx index 85027bcd..81302f29 100644 --- a/frontend/src/components/common/ContentImageCell.tsx +++ b/frontend/src/components/common/ContentImageCell.tsx @@ -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 (
- {/* Prompt Text */} - {prompt && ( -
-

- {displayPrompt} - {shouldTruncate && ( - - )} -

-
- )} - {/* Image Display */}
{image.status === 'pending' && ( diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index ec60b83f..a4f5f480 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -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 = { @@ -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) => (
{handlers.onViewContent ? ( diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx index 658d9837..e801a228 100644 --- a/frontend/src/config/pages/review.config.tsx +++ b/frontend/src/config/pages/review.config.tsx @@ -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 -; - return ( -
- {categories.map((cat: any) => ( - {cat.name} - ))} -
- ); - }, - 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 -; - return ( -
- {tags.map((tag: any) => ( - {tag.name} - ))} -
- ); - }, - 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) => ( +
+ + {value || `Content #${row.id}`} + +
+ ), + }, + { + 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 -; + return ( +
+ {categories.map((cat: any) => ( + {cat.name} + ))} +
+ ); + }, + 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 -; + return ( +
+ {tags.map((tag: any) => ( + {tag.name} + ))} +
+ ); + }, + toggleable: false, + defaultVisible: true, + }, { key: 'title', label: 'Title', diff --git a/frontend/src/hooks/useFeatureFlag.ts b/frontend/src/hooks/useFeatureFlag.ts new file mode 100644 index 00000000..20e1dd98 --- /dev/null +++ b/frontend/src/hooks/useFeatureFlag.ts @@ -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; +} + + diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 7869013a..3363c263 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -130,7 +130,7 @@ const AppSidebar: React.FC = () => { workflowItems.push({ icon: , name: "Writer", - path: "/writer/content", // Default to content, submenus shown as in-page navigation + path: "/writer/tasks", // Default to tasks (changed from content) }); } diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index dda9f989..133fe7a4 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -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(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 */} - { - 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 */} { + 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 (
@@ -677,7 +733,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
{/* Header Section */}
-
+
@@ -694,6 +750,34 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten

)}
+
+ {featureEnabled && (content.status === 'draft' || content.status === 'review') && ( + + )} + {featureEnabled && content.status === 'draft' && ( + + )} + {featureEnabled && content.status === 'review' && ( + + )} +
diff --git a/master-docs/CONTENT-MANAGER-REMOVAL-PLAN.md b/master-docs/CONTENT-MANAGER-REMOVAL-PLAN.md new file mode 100644 index 00000000..799471e0 --- /dev/null +++ b/master-docs/CONTENT-MANAGER-REMOVAL-PLAN.md @@ -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 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. diff --git a/master-docs/CONTENT-MANAGER-REMOVAL-TESTS.md b/master-docs/CONTENT-MANAGER-REMOVAL-TESTS.md new file mode 100644 index 00000000..2665b373 --- /dev/null +++ b/master-docs/CONTENT-MANAGER-REMOVAL-TESTS.md @@ -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. + +