This commit is contained in:
IGNY8 VPS (Salman)
2025-11-30 00:21:00 +00:00
parent d2f3f3ef97
commit 550a8f26a2
5 changed files with 144 additions and 33 deletions

View File

@@ -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[]) => {

View File

@@ -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">

View File

@@ -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
View 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
1 keyword volume difficulty intent status
2 keyword 1 700 52 informational pending
3 keyword 2 950 92 commercial pending
4 keyword 3 850 76 commercial pending

4
seedKeywords_fixed.csv Normal file
View 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
1 keyword volume difficulty intent industry_name sector_name
2 keyword 1 550 28 commercial Home & Garden Home Decor
3 keyword 2 700 52 informational Home & Garden Home Decor
4 keyword 3 950 92 commercial Home & Garden Home Decor