sad
This commit is contained in:
@@ -342,7 +342,7 @@ export default function Keywords() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Import/Export handlers
|
// Import/Export handlers
|
||||||
const { handleExport, handleImportClick, ImportModal } = useKeywordsImportExport(
|
const { handleExport, handleImportClick: baseHandleImportClick, ImportModal } = useKeywordsImportExport(
|
||||||
() => {
|
() => {
|
||||||
toast.success('Import successful', 'Keywords imported successfully.');
|
toast.success('Import successful', 'Keywords imported successfully.');
|
||||||
loadKeywords();
|
loadKeywords();
|
||||||
@@ -350,12 +350,34 @@ export default function Keywords() {
|
|||||||
(error) => {
|
(error) => {
|
||||||
toast.error('Import failed', error.message);
|
toast.error('Import failed', error.message);
|
||||||
},
|
},
|
||||||
// Pass active site_id and active sector_id for import
|
// Pass active site_id and active sector_id for import, but validate sector belongs to site
|
||||||
activeSite && activeSector
|
activeSite && activeSector && activeSector.site_id === activeSite.id
|
||||||
? { site_id: activeSite.id, sector_id: activeSector.id }
|
? { site_id: activeSite.id, sector_id: activeSector.id }
|
||||||
|
: activeSite // Only pass site_id if sector doesn't belong to site
|
||||||
|
? { site_id: activeSite.id }
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Custom import click handler with validation
|
||||||
|
const handleImportClick = () => {
|
||||||
|
// Validate that sector belongs to the selected site
|
||||||
|
if (activeSite && activeSector && activeSector.site_id !== activeSite.id) {
|
||||||
|
toast.error('Import failed', `Selected sector "${activeSector.name}" does not belong to site "${activeSite.name}". Please select a sector from the current site.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log('Import attempt:', {
|
||||||
|
site_id: activeSite?.id,
|
||||||
|
site_name: activeSite?.name,
|
||||||
|
sector_id: activeSector?.id,
|
||||||
|
sector_name: activeSector?.name,
|
||||||
|
sector_site_id: activeSector?.site_id
|
||||||
|
});
|
||||||
|
|
||||||
|
baseHandleImportClick();
|
||||||
|
};
|
||||||
|
|
||||||
// Handle bulk actions (delete, export, update_status are now handled by TablePageTemplate)
|
// Handle bulk actions (delete, export, update_status are now handled by TablePageTemplate)
|
||||||
// This is only for actions that don't have modals (like auto_cluster)
|
// This is only for actions that don't have modals (like auto_cluster)
|
||||||
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
|
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
|
||||||
|
|||||||
@@ -404,29 +404,84 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', importFile);
|
formData.append('file', importFile);
|
||||||
|
|
||||||
const response = await fetch('/api/v1/auth/seed-keywords/import_seed_keywords/', {
|
// Get token from auth store (consistent with other API calls)
|
||||||
method: 'POST',
|
const getAuthToken = () => {
|
||||||
headers: {
|
try {
|
||||||
'Authorization': `Token ${localStorage.getItem('authToken')}`,
|
const authStorage = localStorage.getItem('auth-storage');
|
||||||
},
|
if (authStorage) {
|
||||||
body: formData,
|
const parsed = JSON.parse(authStorage);
|
||||||
});
|
return parsed?.state?.token || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parsing errors
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.ok) {
|
const token = getAuthToken();
|
||||||
const errorData = await response.json();
|
const headers: HeadersInit = {};
|
||||||
throw new Error(errorData.error || 'Import failed');
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const response = await fetch('/api/v1/auth/seed-keywords/import_seed_keywords/', {
|
||||||
toast.success(`Successfully imported ${result.created || 0} keywords`);
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include', // Add credentials for consistency
|
||||||
|
});
|
||||||
|
|
||||||
// Reset and close modal
|
let errorMessage = 'Import failed';
|
||||||
setImportFile(null);
|
|
||||||
setIsImportModalOpen(false);
|
|
||||||
|
|
||||||
// Reload keywords
|
// Handle common HTTP status codes
|
||||||
if (activeSite) {
|
if (response.status === 404) {
|
||||||
loadSeedKeywords();
|
errorMessage = 'Import endpoint not found. Please check if the backend service is running.';
|
||||||
|
} else if (response.status === 429) {
|
||||||
|
errorMessage = 'Too many requests. Please wait a moment before trying again.';
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
errorMessage = 'Access denied. Admin privileges required for seed keyword import.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// Try to get error message from response (override status-based message if available)
|
||||||
|
if (result.error) {
|
||||||
|
errorMessage = result.error;
|
||||||
|
} else if (result.message) {
|
||||||
|
errorMessage = result.message;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success case
|
||||||
|
const importedCount = result.data?.imported || result.imported || 0;
|
||||||
|
const skippedCount = result.data?.skipped || result.skipped || 0;
|
||||||
|
const errors = result.data?.errors || result.errors || [];
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Successfully imported ${importedCount} keyword(s)${skippedCount > 0 ? `, ${skippedCount} skipped` : ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
toast.info(`Errors: ${errors.slice(0, 3).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset and close modal
|
||||||
|
setImportFile(null);
|
||||||
|
setIsImportModalOpen(false);
|
||||||
|
|
||||||
|
// Reload keywords
|
||||||
|
if (activeSite) {
|
||||||
|
loadSeedKeywords();
|
||||||
|
}
|
||||||
|
} catch (jsonError: any) {
|
||||||
|
// If JSON parsing fails, handle it gracefully
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(errorMessage || `Server error: ${response.status}`);
|
||||||
|
}
|
||||||
|
throw jsonError;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Import error:', error);
|
console.error('Import error:', error);
|
||||||
@@ -631,6 +686,21 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
title="Add Keywords"
|
title="Add Keywords"
|
||||||
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="px-6 pt-4 pb-2 flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
|
startIcon={<PlusIcon className="w-4 h-4" />}
|
||||||
|
onClick={handleImportClick}
|
||||||
|
>
|
||||||
|
Import Keywords
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<TablePageTemplate
|
<TablePageTemplate
|
||||||
columns={pageConfig.columns}
|
columns={pageConfig.columns}
|
||||||
data={seedKeywords}
|
data={seedKeywords}
|
||||||
@@ -661,18 +731,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
bulkActions={pageConfig.bulkActions}
|
bulkActions={pageConfig.bulkActions}
|
||||||
customActions={
|
customActions={undefined}
|
||||||
isAdmin ? (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleImportClick}
|
|
||||||
>
|
|
||||||
<PlusIcon className="w-4 h-4 mr-2" />
|
|
||||||
Import Keywords
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
@@ -702,6 +761,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
setImportFile(null);
|
setImportFile(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
className="max-w-md"
|
||||||
>
|
>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
|||||||
@@ -155,8 +155,29 @@ export const importTableData = async (
|
|||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`;
|
const url = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
|
// Get authentication token (consistent with other API calls)
|
||||||
|
const getAuthToken = () => {
|
||||||
|
try {
|
||||||
|
const authStorage = localStorage.getItem('auth-storage');
|
||||||
|
if (authStorage) {
|
||||||
|
const parsed = JSON.parse(authStorage);
|
||||||
|
return parsed?.state?.token || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parsing errors
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = getAuthToken();
|
||||||
|
const headers: HeadersInit = {};
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers,
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|||||||
4
keywordsplanner.csv
Normal file
4
keywordsplanner.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
keyword,volume,difficulty,intent,status
|
||||||
|
keyword 1,700,52,informational,pending
|
||||||
|
keyword 2,950,92,commercial,pending
|
||||||
|
keyword 3,850,76,commercial,pending
|
||||||
|
4
seedKeywords_fixed.csv
Normal file
4
seedKeywords_fixed.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
keyword,volume,difficulty,intent,industry_name,sector_name
|
||||||
|
keyword 1,550,28,commercial,Home & Garden,Home Decor
|
||||||
|
keyword 2,700,52,informational,Home & Garden,Home Decor
|
||||||
|
keyword 3,950,92,commercial,Home & Garden,Home Decor
|
||||||
|
Reference in New Issue
Block a user