fixing issues of integration with wordpress plugin
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user