fixing issues of integration with wordpress plugin

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 23:25:47 +00:00
parent ad828a9fcd
commit 5c3aa90e91
18 changed files with 1414 additions and 427 deletions

View File

@@ -1,6 +1,6 @@
/**
* WordPress Integration Form Component
* Inline form for WordPress integration with API key generation and plugin download
* Simplified - uses only Site.wp_api_key, no SiteIntegration model needed
*/
import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
@@ -11,7 +11,6 @@ import Input from '../form/input/InputField';
import Checkbox from '../form/input/Checkbox';
import Switch from '../form/switch/Switch';
import { useToast } from '../ui/toast/ToastContainer';
import { integrationApi, SiteIntegration } from '../../services/integration.api';
import { fetchAPI, API_BASE_URL } from '../../services/api';
import {
CheckCircleIcon,
@@ -28,18 +27,18 @@ import {
interface WordPressIntegrationFormProps {
siteId: number;
integration: SiteIntegration | null;
siteName?: string;
siteUrl?: string;
onIntegrationUpdate?: (integration: SiteIntegration) => void;
wpApiKey?: string; // API key from Site.wp_api_key
onApiKeyUpdate?: (apiKey: string | null) => void;
}
export default function WordPressIntegrationForm({
siteId,
integration,
siteName,
siteUrl,
onIntegrationUpdate,
wpApiKey,
onApiKeyUpdate,
}: WordPressIntegrationFormProps) {
const toast = useToast();
const [loading, setLoading] = useState(false);
@@ -48,15 +47,20 @@ export default function WordPressIntegrationForm({
const [apiKeyVisible, setApiKeyVisible] = useState(false);
const [pluginInfo, setPluginInfo] = useState<any>(null);
const [loadingPlugin, setLoadingPlugin] = useState(false);
// Connection status state
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'testing' | 'connected' | 'api_key_pending' | 'plugin_missing' | 'error'>('unknown');
const [connectionMessage, setConnectionMessage] = useState<string>('');
const [testingConnection, setTestingConnection] = useState(false);
// Load API key from integration on mount or when integration changes
// Load API key from wpApiKey prop (from Site.wp_api_key) on mount or when it changes
useEffect(() => {
if (integration?.api_key) {
setApiKey(integration.api_key);
if (wpApiKey) {
setApiKey(wpApiKey);
} else {
setApiKey('');
}
}, [integration]);
}, [wpApiKey]);
// Fetch plugin information
useEffect(() => {
@@ -75,11 +79,84 @@ export default function WordPressIntegrationForm({
fetchPluginInfo();
}, []);
// Test connection when API key exists
const testConnection = async () => {
if (!apiKey || !siteUrl) {
setConnectionStatus('unknown');
setConnectionMessage('API key or site URL missing');
return;
}
try {
setTestingConnection(true);
setConnectionStatus('testing');
setConnectionMessage('Testing connection...');
// Call backend to test connection to WordPress
// Backend reads API key from Site.wp_api_key (single source of truth)
const response = await fetchAPI('/v1/integration/integrations/test-connection/', {
method: 'POST',
body: JSON.stringify({
site_id: siteId,
}),
});
if (response.success) {
// Check the health checks from response
const healthChecks = response.health_checks || {};
// CRITICAL: api_key_verified confirms WordPress accepts our API key
if (healthChecks.api_key_verified) {
setConnectionStatus('connected');
setConnectionMessage('WordPress is connected and API key verified');
toast.success('WordPress connection verified!');
} else if (healthChecks.plugin_has_api_key && !healthChecks.api_key_verified) {
// WordPress has A key, but it's NOT the same as IGNY8's key
setConnectionStatus('api_key_pending');
setConnectionMessage('API key mismatch - copy the key from IGNY8 to WordPress plugin');
toast.warning('WordPress has different API key. Please update WordPress with the key shown above.');
} else if (healthChecks.plugin_installed && !healthChecks.plugin_has_api_key) {
setConnectionStatus('api_key_pending');
setConnectionMessage('Plugin installed - please add API key in WordPress');
toast.warning('Plugin found but API key not configured in WordPress');
} else if (!healthChecks.plugin_installed) {
setConnectionStatus('plugin_missing');
setConnectionMessage('IGNY8 plugin not installed on WordPress site');
toast.warning('WordPress site reachable but plugin not found');
} else {
setConnectionStatus('error');
setConnectionMessage(response.message || 'Connection verification incomplete');
toast.error(response.message || 'Connection test incomplete');
}
} else {
setConnectionStatus('error');
setConnectionMessage(response.message || 'Connection test failed');
toast.error(response.message || 'Connection test failed');
}
} catch (error: any) {
setConnectionStatus('error');
setConnectionMessage(error.message || 'Connection test failed');
toast.error(`Connection test failed: ${error.message}`);
} finally {
setTestingConnection(false);
}
};
// Auto-test connection when API key changes
useEffect(() => {
if (apiKey && siteUrl) {
testConnection();
} else {
setConnectionStatus('unknown');
setConnectionMessage('');
}
}, [apiKey, siteUrl]);
const handleGenerateApiKey = async () => {
try {
setGeneratingKey(true);
// Call the new generate-api-key endpoint
// Call the simplified generate-api-key endpoint
const response = await fetchAPI('/v1/integration/integrations/generate-api-key/', {
method: 'POST',
body: JSON.stringify({ site_id: siteId }),
@@ -89,9 +166,9 @@ export default function WordPressIntegrationForm({
setApiKey(newKey);
setApiKeyVisible(true);
// Trigger integration update
if (onIntegrationUpdate && response.integration) {
onIntegrationUpdate(response.integration);
// Notify parent component
if (onApiKeyUpdate) {
onApiKeyUpdate(newKey);
}
toast.success('API key generated successfully');
@@ -119,9 +196,9 @@ export default function WordPressIntegrationForm({
setApiKey(newKey);
setApiKeyVisible(true);
// Trigger integration update
if (onIntegrationUpdate && response.integration) {
onIntegrationUpdate(response.integration);
// Notify parent component
if (onApiKeyUpdate) {
onApiKeyUpdate(newKey);
}
toast.success('API key regenerated successfully');
@@ -139,20 +216,20 @@ export default function WordPressIntegrationForm({
try {
setGeneratingKey(true);
if (!integration) {
toast.error('No integration found');
return;
}
// Delete the integration to revoke the API key
await integrationApi.deleteIntegration(integration.id);
// Revoke API key via dedicated endpoint (single source of truth: Site.wp_api_key)
await fetchAPI('/v1/integration/integrations/revoke-api-key/', {
method: 'POST',
body: JSON.stringify({ site_id: siteId }),
});
setApiKey('');
setApiKeyVisible(false);
setConnectionStatus('unknown');
setConnectionMessage('');
// Trigger integration update
if (onIntegrationUpdate) {
onIntegrationUpdate(null as any);
// Notify parent component
if (onApiKeyUpdate) {
onApiKeyUpdate(null);
}
toast.success('API key revoked successfully');
@@ -183,47 +260,9 @@ export default function WordPressIntegrationForm({
return key.substring(0, 8) + '**********' + key.substring(key.length - 4);
};
// Toggle integration sync enabled status (not creation - that happens automatically)
const [integrationEnabled, setIntegrationEnabled] = useState(integration?.sync_enabled ?? false);
const handleToggleIntegration = async (enabled: boolean) => {
try {
setIntegrationEnabled(enabled);
if (integration) {
// Update existing integration - only toggle sync_enabled, not creation
await integrationApi.updateIntegration(integration.id, {
sync_enabled: enabled,
} as any);
toast.success(enabled ? 'Sync enabled' : 'Sync disabled');
// Reload integration
const updated = await integrationApi.getWordPressIntegration(siteId);
if (onIntegrationUpdate && updated) {
onIntegrationUpdate(updated);
}
} else {
// Integration doesn't exist - it should be created automatically by plugin
// when user connects from WordPress side
toast.info('Integration will be created automatically when you connect from WordPress plugin. Please connect from the plugin first.');
setIntegrationEnabled(false);
}
} catch (error: any) {
toast.error(`Failed to update integration: ${error.message}`);
// Revert on error
setIntegrationEnabled(!enabled);
}
};
useEffect(() => {
if (integration) {
setIntegrationEnabled(integration.sync_enabled ?? false);
}
}, [integration]);
return (
<div className="space-y-6">
{/* Header with Toggle */}
{/* Header */}
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
@@ -239,13 +278,60 @@ export default function WordPressIntegrationForm({
</div>
</div>
{/* Toggle Switch */}
{/* Connection Status */}
{apiKey && (
<Switch
label={integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
checked={integrationEnabled}
onChange={(checked) => handleToggleIntegration(checked)}
/>
<div className="flex items-center gap-2">
{/* Status Badge */}
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border ${
connectionStatus === 'connected'
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: connectionStatus === 'testing'
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
: connectionStatus === 'api_key_pending'
? 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800'
: connectionStatus === 'plugin_missing'
? 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800'
: connectionStatus === 'error'
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
: 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
}`}>
{connectionStatus === 'connected' && (
<><CheckCircleIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-green-700 dark:text-green-300">Connected</span></>
)}
{connectionStatus === 'testing' && (
<><RefreshCwIcon className="w-4 h-4 text-blue-600 dark:text-blue-400 animate-spin" />
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Testing...</span></>
)}
{connectionStatus === 'api_key_pending' && (
<><AlertIcon className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm font-medium text-amber-700 dark:text-amber-300">Pending Setup</span></>
)}
{connectionStatus === 'plugin_missing' && (
<><AlertIcon className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm font-medium text-amber-700 dark:text-amber-300">Plugin Missing</span></>
)}
{connectionStatus === 'error' && (
<><AlertIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
<span className="text-sm font-medium text-red-700 dark:text-red-300">Error</span></>
)}
{connectionStatus === 'unknown' && (
<><InfoIcon className="w-4 h-4 text-gray-500 dark:text-gray-400" />
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Not Tested</span></>
)}
</div>
{/* Test Connection Button */}
<Button
onClick={testConnection}
variant="outline"
size="sm"
disabled={testingConnection || !apiKey}
startIcon={<RefreshCwIcon className={`w-4 h-4 ${testingConnection ? 'animate-spin' : ''}`} />}
>
Test
</Button>
</div>
)}
</div>
@@ -264,18 +350,9 @@ export default function WordPressIntegrationForm({
onClick={handleGenerateApiKey}
variant="solid"
disabled={generatingKey}
startIcon={generatingKey ? <RefreshCwIcon className="w-4 h-4 animate-spin" /> : <PlusIcon className="w-4 h-4" />}
>
{generatingKey ? (
<>
<RefreshCwIcon className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<PlusIcon className="w-4 h-4 mr-2" />
Add API Key
</>
)}
{generatingKey ? 'Generating...' : 'Add API Key'}
</Button>
</div>
)}
@@ -306,6 +383,7 @@ export default function WordPressIntegrationForm({
readOnly
type={apiKeyVisible ? 'text' : 'password'}
value={apiKeyVisible ? apiKey : maskApiKey(apiKey)}
onChange={() => {}} // No-op to satisfy React
/>
<IconButton
onClick={handleCopyApiKey}

View File

@@ -7,7 +7,7 @@ import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date';
import { CheckCircleIcon, ArrowRightIcon } from '../../icons';
import { STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
import { STRUCTURE_LABELS, TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping';
export interface ColumnConfig {
key: string;
@@ -48,8 +48,10 @@ export interface ApprovedPageConfig {
export function createApprovedPageConfig(params: {
searchTerm: string;
setSearchTerm: (value: string) => void;
publishStatusFilter: string;
setPublishStatusFilter: (value: string) => void;
statusFilter: string;
setStatusFilter: (value: string) => void;
siteStatusFilter: string;
setSiteStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void;
activeSector: { id: number; name: string } | null;
onRowClick?: (row: Content) => void;
@@ -97,10 +99,12 @@ export function createApprovedPageConfig(params: {
sortable: true,
sortField: 'status',
render: (value: string, row: Content) => {
// Map internal status to user-friendly labels
// Map internal status to standard labels
const statusConfig: Record<string, { color: 'success' | 'blue' | 'amber' | 'gray'; label: string }> = {
'approved': { color: 'blue', label: 'Ready to Publish' },
'published': { color: 'success', label: row.external_id ? 'On Site' : 'Approved' },
'draft': { color: 'gray', label: 'Draft' },
'review': { color: 'amber', label: 'Review' },
'approved': { color: 'blue', label: 'Approved' },
'published': { color: 'success', label: 'Published' },
};
const config = statusConfig[value] || { color: 'gray' as const, label: value || '-' };
@@ -112,31 +116,21 @@ export function createApprovedPageConfig(params: {
},
},
{
key: 'wordpress_status',
label: 'Site Content Status',
sortable: false,
width: '120px',
render: (_value: any, row: Content) => {
// Check if content has been published to WordPress
if (!row.external_id) {
return (
<Badge color="amber" size="xs" variant="soft">
<span className="text-[11px] font-normal">Not Published</span>
</Badge>
);
}
// WordPress status badge - use external_status if available, otherwise show 'Published'
const wpStatus = (row as any).wordpress_status || 'publish';
key: 'site_status',
label: 'Site Status',
sortable: true,
sortField: 'site_status',
width: '130px',
render: (value: string, row: Content) => {
// Show actual site_status field
const statusConfig: Record<string, { color: 'success' | 'amber' | 'blue' | 'gray' | 'red'; label: string }> = {
publish: { color: 'success', label: 'Published' },
draft: { color: 'gray', label: 'Draft' },
pending: { color: 'amber', label: 'Pending' },
future: { color: 'blue', label: 'Scheduled' },
private: { color: 'amber', label: 'Private' },
trash: { color: 'red', label: 'Trashed' },
'not_published': { color: 'gray', label: 'Not Published' },
'scheduled': { color: 'amber', label: 'Scheduled' },
'publishing': { color: 'amber', label: 'Publishing' },
'published': { color: 'success', label: 'Published' },
'failed': { color: 'red', label: 'Failed' },
};
const config = statusConfig[wpStatus] || { color: 'success' as const, label: 'Published' };
const config = statusConfig[value] || { color: 'gray' as const, label: value || 'Not Published' };
return (
<Badge color={config.color} size="xs" variant="soft">
@@ -145,6 +139,28 @@ export function createApprovedPageConfig(params: {
);
},
},
{
key: 'scheduled_publish_at',
label: 'Publish Date',
sortable: true,
sortField: 'scheduled_publish_at',
date: true,
width: '150px',
render: (value: string, row: Content) => {
if (!value) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">Not scheduled</span>;
}
const publishDate = new Date(value);
const now = new Date();
const isFuture = publishDate > now;
return (
<span className={isFuture ? "text-blue-600 dark:text-blue-400 font-medium" : "text-amber-600 dark:text-amber-400 font-medium"}>
{formatRelativeDate(value)}
</span>
);
},
},
{
key: 'content_type',
@@ -283,13 +299,46 @@ export function createApprovedPageConfig(params: {
placeholder: 'Search approved content...',
},
{
key: 'publishStatus',
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: '', label: 'All' },
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' },
{ value: 'approved', label: 'Approved' },
{ value: 'published', label: 'Published' },
],
},
{
key: 'site_status',
label: 'Site Status',
type: 'select',
options: [
{ value: '', label: 'All' },
{ value: 'published', label: 'Published to Site' },
{ value: 'not_published', label: 'Not Published' },
{ value: 'scheduled', label: 'Scheduled' },
{ value: 'publishing', label: 'Publishing' },
{ value: 'published', label: 'Published' },
{ value: 'failed', label: 'Failed' },
],
},
{
key: 'content_type',
label: 'Type',
type: 'select',
options: [
{ value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS,
],
},
{
key: 'content_structure',
label: 'Structure',
type: 'select',
options: [
{ value: '', label: 'All Structures' },
...ALL_CONTENT_STRUCTURES,
],
},
];

View File

@@ -188,9 +188,21 @@ export const createImagesPageConfig = (
type: 'text',
placeholder: 'Search by content title...',
},
{
key: 'content_status',
label: 'Content Status',
type: 'select',
options: [
{ value: '', label: 'All' },
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' },
{ value: 'approved', label: 'Approved' },
{ value: 'published', label: 'Published' },
],
},
{
key: 'status',
label: 'Status',
label: 'Image Status',
type: 'select',
options: [
{ value: '', label: 'All Status' },

View File

@@ -7,7 +7,7 @@ import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date';
import { CheckCircleIcon } from '../../icons';
import { STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
import { STRUCTURE_LABELS, TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping';
export interface ColumnConfig {
key: string;
@@ -256,6 +256,49 @@ export function createReviewPageConfig(params: {
type: 'text',
placeholder: 'Search content...',
},
{
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: '', label: 'All' },
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' },
{ value: 'approved', label: 'Approved' },
{ value: 'published', label: 'Published' },
],
},
{
key: 'site_status',
label: 'Site Status',
type: 'select',
options: [
{ value: '', label: 'All' },
{ value: 'not_published', label: 'Not Published' },
{ value: 'scheduled', label: 'Scheduled' },
{ value: 'publishing', label: 'Publishing' },
{ value: 'published', label: 'Published' },
{ value: 'failed', label: 'Failed' },
],
},
{
key: 'content_type',
label: 'Type',
type: 'select',
options: [
{ value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS,
],
},
{
key: 'content_structure',
label: 'Structure',
type: 'select',
options: [
{ value: '', label: 'All Structures' },
...ALL_CONTENT_STRUCTURES,
],
},
],
headerMetrics: [
{

View File

@@ -29,7 +29,6 @@ import {
} from '../../services/api';
import { useSiteStore } from '../../store/siteStore';
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
import { integrationApi, SiteIntegration } from '../../services/integration.api';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon, ArrowRightIcon, SettingsIcon, GlobeIcon, LayersIcon, CheckCircleIcon, CalendarIcon, InfoIcon } from '../../icons';
import Badge from '../../components/ui/badge/Badge';
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
@@ -45,8 +44,6 @@ export default function SiteSettings() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [site, setSite] = useState<any>(null);
const [wordPressIntegration, setWordPressIntegration] = useState<SiteIntegration | null>(null);
const [integrationLoading, setIntegrationLoading] = useState(false);
// Site selector state
const [sites, setSites] = useState<Site[]>([]);
@@ -134,12 +131,10 @@ export default function SiteSettings() {
useEffect(() => {
if (siteId) {
// Clear state when site changes
setWordPressIntegration(null);
setSite(null);
// Load new site data
loadSite();
loadIntegrations();
loadIndustries();
}
}, [siteId]);
@@ -248,17 +243,10 @@ export default function SiteSettings() {
}
};
const loadIntegrations = async () => {
if (!siteId) return;
try {
setIntegrationLoading(true);
const integration = await integrationApi.getWordPressIntegration(Number(siteId));
setWordPressIntegration(integration);
} catch (error: any) {
// Integration might not exist, that's okay
setWordPressIntegration(null);
} finally {
setIntegrationLoading(false);
const handleApiKeyUpdate = (newApiKey: string | null) => {
// Update site state with new API key
if (site) {
setSite({ ...site, wp_api_key: newApiKey });
}
};
@@ -495,11 +483,6 @@ export default function SiteSettings() {
}
};
const handleIntegrationUpdate = async (integration: SiteIntegration) => {
setWordPressIntegration(integration);
await loadIntegrations();
};
const formatRelativeTime = (iso: string | null) => {
if (!iso) return '-';
const then = new Date(iso).getTime();
@@ -516,83 +499,56 @@ export default function SiteSettings() {
return `${months}mo ago`;
};
// Integration status with authentication check
// Integration status - tracks actual connection state
const [integrationStatus, setIntegrationStatus] = useState<'connected' | 'configured' | 'not_configured'>('not_configured');
const [testingAuth, setTestingAuth] = useState(false);
// Check basic configuration - integration must exist in DB and have sync_enabled
// Check integration status based on API key presence (will be updated by WordPressIntegrationForm)
useEffect(() => {
const checkStatus = async () => {
// Integration must exist in database and have sync_enabled = true
if (wordPressIntegration && wordPressIntegration.id && wordPressIntegration.sync_enabled) {
setIntegrationStatus('configured');
// Test authentication
testAuthentication();
} else {
setIntegrationStatus('not_configured');
}
};
checkStatus();
}, [wordPressIntegration, site]);
// Auto-refresh integration list periodically to detect plugin-created integrations
useEffect(() => {
const interval = setInterval(() => {
if (!wordPressIntegration) {
loadIntegrations();
}
}, 5000); // Check every 5 seconds if integration doesn't exist
return () => clearInterval(interval);
}, [wordPressIntegration]);
// Test authentication with WordPress API
const testAuthentication = async () => {
if (testingAuth || !wordPressIntegration?.id) return;
try {
setTestingAuth(true);
const resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, {
method: 'POST',
body: {}
});
if (resp && resp.success) {
setIntegrationStatus('connected');
} else {
// Keep as 'configured' if auth fails
setIntegrationStatus('configured');
}
} catch (err) {
// Keep as 'configured' if auth test fails
if (site?.wp_api_key) {
// API key exists - mark as configured (actual connection tested in WordPressIntegrationForm)
setIntegrationStatus('configured');
} finally {
setTestingAuth(false);
} else {
setIntegrationStatus('not_configured');
}
};
}, [site?.wp_api_key]);
// Sync Now handler extracted
// Sync Now handler - tests actual WordPress connection
const [syncLoading, setSyncLoading] = useState(false);
const [lastSyncTime, setLastSyncTime] = useState<string | null>(null);
const handleManualSync = async () => {
if (!site?.wp_api_key) {
toast.error('WordPress API key not configured. Please generate an API key first.');
return;
}
setSyncLoading(true);
try {
if (wordPressIntegration && wordPressIntegration.id) {
const res = await integrationApi.syncIntegration(wordPressIntegration.id, 'metadata');
if (res && res.success) {
toast.success('WordPress structure synced successfully');
if (res.last_sync_at) {
setLastSyncTime(res.last_sync_at);
}
setTimeout(() => loadContentTypes(), 1500);
// Test connection to WordPress using backend test endpoint
// Backend reads API key from Site.wp_api_key (single source of truth)
const res = await fetchAPI('/v1/integration/integrations/test-connection/', {
method: 'POST',
body: JSON.stringify({
site_id: siteId,
}),
});
if (res && res.success) {
// Check health checks
const healthChecks = res.health_checks || {};
if (healthChecks.plugin_has_api_key) {
setIntegrationStatus('connected');
toast.success('WordPress connection verified - fully connected!');
} else if (healthChecks.plugin_installed) {
setIntegrationStatus('configured');
toast.warning('Plugin found but API key not configured in WordPress');
} else {
toast.error(res?.message || 'Sync failed to start');
toast.warning('WordPress reachable but IGNY8 plugin not installed');
}
setLastSyncTime(new Date().toISOString());
} else {
toast.error('No integration configured. Please configure WordPress integration first.');
toast.error(res?.message || 'Connection test failed');
}
} catch (err: any) {
toast.error(`Sync failed: ${err?.message || String(err)}`);
toast.error(`Connection test failed: ${err?.message || String(err)}`);
} finally {
setSyncLoading(false);
}
@@ -739,7 +695,7 @@ export default function SiteSettings() {
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
{integrationStatus === 'connected' && 'Connected'}
{integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')}
{integrationStatus === 'configured' && 'Configured'}
{integrationStatus === 'not_configured' && 'Not Configured'}
</span>
</div>
@@ -1874,10 +1830,10 @@ export default function SiteSettings() {
{activeTab === 'integrations' && siteId && (
<WordPressIntegrationForm
siteId={Number(siteId)}
integration={wordPressIntegration}
siteName={site?.name}
siteUrl={site?.domain || site?.wp_url}
onIntegrationUpdate={handleIntegrationUpdate}
wpApiKey={site?.wp_api_key}
onApiKeyUpdate={handleApiKeyUpdate}
/>
)}
</div>

View File

@@ -13,7 +13,6 @@ import {
ContentListResponse,
ContentFilters,
fetchAPI,
fetchWordPressStatus,
deleteContent,
bulkDeleteContent,
} from '../../services/api';
@@ -46,9 +45,12 @@ export default function Approved() {
const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state - default to approved status
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [publishStatusFilter, setPublishStatusFilter] = useState('');
const [statusFilter, setStatusFilter] = useState(''); // Status filter (draft/review/approved/published)
const [siteStatusFilter, setSiteStatusFilter] = useState(''); // Site status filter (not_published/scheduled/published/failed)
const [contentTypeFilter, setContentTypeFilter] = useState(''); // Content type filter (post/page/product/taxonomy)
const [contentStructureFilter, setContentStructureFilter] = useState(''); // Content structure filter
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
@@ -99,7 +101,10 @@ export default function Approved() {
const filters: ContentFilters = {
...(searchTerm && { search: searchTerm }),
status__in: 'approved,published', // Both approved and published content
// Default to approved+published if no status filter selected
...(statusFilter ? { status: statusFilter } : { status__in: 'approved,published' }),
...(contentTypeFilter && { content_type: contentTypeFilter }),
...(contentStructureFilter && { content_structure: contentStructureFilter }),
page: currentPage,
page_size: pageSize,
ordering,
@@ -107,34 +112,13 @@ export default function Approved() {
const data: ContentListResponse = await fetchContent(filters);
// Client-side filter for WordPress publish status if needed
// Client-side filter for site_status if needed (backend may not support this filter yet)
let filteredResults = data.results || [];
if (publishStatusFilter === 'published') {
filteredResults = filteredResults.filter(c => c.external_id);
} else if (publishStatusFilter === 'not_published') {
filteredResults = filteredResults.filter(c => !c.external_id);
if (siteStatusFilter) {
filteredResults = filteredResults.filter(c => c.site_status === siteStatusFilter);
}
// Fetch WordPress status for published content
const resultsWithWPStatus = await Promise.all(
filteredResults.map(async (content) => {
if (content.external_id) {
try {
const wpStatus = await fetchWordPressStatus(content.id);
return {
...content,
wordpress_status: wpStatus.wordpress_status,
};
} catch (error) {
console.warn(`Failed to fetch WP status for content ${content.id}:`, error);
return content;
}
}
return content;
})
);
setContent(resultsWithWPStatus);
setContent(filteredResults);
setTotalCount(data.count || 0);
setTotalPages(Math.ceil((data.count || 0) / pageSize));
@@ -148,7 +132,7 @@ export default function Approved() {
setShowContent(true);
setLoading(false);
}
}, [currentPage, publishStatusFilter, sortBy, sortDirection, searchTerm, pageSize, toast]);
}, [currentPage, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, sortBy, sortDirection, searchTerm, pageSize, toast]);
useEffect(() => {
loadContent();
@@ -326,15 +310,17 @@ export default function Approved() {
return createApprovedPageConfig({
searchTerm,
setSearchTerm,
publishStatusFilter,
setPublishStatusFilter,
statusFilter,
setStatusFilter,
siteStatusFilter,
setSiteStatusFilter,
setCurrentPage,
activeSector,
onRowClick: (row: Content) => {
navigate(`/writer/content/${row.id}`);
},
});
}, [searchTerm, publishStatusFilter, activeSector, navigate]);
}, [searchTerm, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, activeSector, navigate]);
// Calculate header metrics - use totals from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
@@ -392,7 +378,10 @@ export default function Approved() {
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
publishStatus: publishStatusFilter,
status: statusFilter,
site_status: siteStatusFilter,
content_type: contentTypeFilter,
content_structure: contentStructureFilter,
}}
primaryAction={{
label: 'Publish to Site',
@@ -403,8 +392,17 @@ export default function Approved() {
onFilterChange={(key: string, value: any) => {
if (key === 'search') {
setSearchTerm(value);
} else if (key === 'publishStatus') {
setPublishStatusFilter(value);
} else if (key === 'status') {
setStatusFilter(value);
setCurrentPage(1);
} else if (key === 'site_status') {
setSiteStatusFilter(value);
setCurrentPage(1);
} else if (key === 'content_type') {
setContentTypeFilter(value);
setCurrentPage(1);
} else if (key === 'content_structure') {
setContentStructureFilter(value);
setCurrentPage(1);
}
}}

View File

@@ -25,6 +25,33 @@ export function formatRelativeDate(dateString: string | Date): string {
const diffTime = today.getTime() - dateOnly.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
// Handle future dates (negative diffDays)
if (diffDays < 0) {
const futureDays = Math.abs(diffDays);
if (futureDays === 1) {
return 'Tomorrow';
} else if (futureDays < 30) {
return `in ${futureDays} days`;
} else if (futureDays < 365) {
const months = Math.floor(futureDays / 30);
const remainingDays = futureDays % 30;
if (remainingDays === 0) {
return `in ${months} month${months > 1 ? 's' : ''}`;
} else {
return `in ${months} month${months > 1 ? 's' : ''} ${remainingDays} day${remainingDays > 1 ? 's' : ''}`;
}
} else {
const years = Math.floor(futureDays / 365);
const remainingMonths = Math.floor((futureDays % 365) / 30);
if (remainingMonths === 0) {
return `in ${years} year${years > 1 ? 's' : ''}`;
} else {
return `in ${years} year${years > 1 ? 's' : ''} ${remainingMonths} month${remainingMonths > 1 ? 's' : ''}`;
}
}
}
// Handle past dates (positive diffDays)
if (diffDays === 0) {
return 'Today';
} else if (diffDays === 1) {