asd
This commit is contained in:
@@ -321,12 +321,29 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
try:
|
try:
|
||||||
# Validate industry/sector match
|
# Validate industry/sector match
|
||||||
if site.industry != seed_keyword.industry:
|
if site.industry != seed_keyword.industry:
|
||||||
errors.append(f"SeedKeyword '{seed_keyword.keyword}' industry mismatch")
|
errors.append(
|
||||||
|
f"Keyword '{seed_keyword.keyword}': industry mismatch "
|
||||||
|
f"(site={site.industry.name if site.industry else 'None'}, "
|
||||||
|
f"seed={seed_keyword.industry.name if seed_keyword.industry else 'None'})"
|
||||||
|
)
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if sector has industry_sector set
|
||||||
|
if not sector.industry_sector:
|
||||||
|
errors.append(
|
||||||
|
f"Keyword '{seed_keyword.keyword}': sector '{sector.name}' has no industry_sector set. "
|
||||||
|
f"Please update the sector to reference an industry sector."
|
||||||
|
)
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if sector.industry_sector != seed_keyword.sector:
|
if sector.industry_sector != seed_keyword.sector:
|
||||||
errors.append(f"SeedKeyword '{seed_keyword.keyword}' sector mismatch")
|
errors.append(
|
||||||
|
f"Keyword '{seed_keyword.keyword}': sector mismatch "
|
||||||
|
f"(sector={sector.industry_sector.name if sector.industry_sector else 'None'}, "
|
||||||
|
f"seed={seed_keyword.sector.name if seed_keyword.sector else 'None'})"
|
||||||
|
)
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -297,25 +297,43 @@ export default function KeywordOpportunities() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(`Successfully added ${result.created} keyword(s) to workflow`);
|
// Show success message with created count
|
||||||
|
if (result.created > 0) {
|
||||||
|
toast.success(`Successfully added ${result.created} keyword(s) to workflow`);
|
||||||
|
}
|
||||||
|
|
||||||
// Track these as recently added to preserve state during reload
|
// Show skipped count if any
|
||||||
seedKeywordIds.forEach(id => {
|
if (result.skipped > 0) {
|
||||||
recentlyAddedRef.current.add(id);
|
toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Show detailed errors if any
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
result.errors.forEach((error: string) => {
|
||||||
|
toast.error(error, { duration: 8000 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only track and mark as added if actually created
|
||||||
|
if (result.created > 0) {
|
||||||
|
// Track these as recently added to preserve state during reload
|
||||||
|
seedKeywordIds.forEach(id => {
|
||||||
|
recentlyAddedRef.current.add(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Immediately update state to mark keywords as added - this gives instant feedback
|
||||||
|
setSeedKeywords(prevKeywords =>
|
||||||
|
prevKeywords.map(kw =>
|
||||||
|
seedKeywordIds.includes(kw.id)
|
||||||
|
? { ...kw, isAdded: true }
|
||||||
|
: kw
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear selection
|
// Clear selection
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
|
|
||||||
// Immediately update state to mark keywords as added - this gives instant feedback
|
|
||||||
setSeedKeywords(prevKeywords =>
|
|
||||||
prevKeywords.map(kw =>
|
|
||||||
seedKeywordIds.includes(kw.id)
|
|
||||||
? { ...kw, isAdded: true }
|
|
||||||
: kw
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Don't reload immediately - the state is already updated
|
// Don't reload immediately - the state is already updated
|
||||||
// The recentlyAddedRef will ensure they stay marked as added
|
// The recentlyAddedRef will ensure they stay marked as added
|
||||||
// Only reload if user changes filters/pagination
|
// Only reload if user changes filters/pagination
|
||||||
@@ -546,6 +564,13 @@ export default function KeywordOpportunities() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
bulkActions: !activeSector ? [] : [
|
||||||
|
{
|
||||||
|
key: 'add_selected_to_workflow',
|
||||||
|
label: 'Add Selected to Workflow',
|
||||||
|
variant: 'primary' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}, [activeSector]);
|
}, [activeSector]);
|
||||||
|
|
||||||
@@ -555,6 +580,30 @@ export default function KeywordOpportunities() {
|
|||||||
title="Keyword Opportunities"
|
title="Keyword Opportunities"
|
||||||
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Show info banner when no sector is selected */}
|
||||||
|
{!activeSector && activeSite && (
|
||||||
|
<div className="mx-6 mt-6 mb-4">
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-200">
|
||||||
|
Select a Sector to Add Keywords
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Please select a sector from the dropdown above to enable adding keywords to your workflow. Keywords must be added to a specific sector.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<TablePageTemplate
|
<TablePageTemplate
|
||||||
columns={pageConfig.columns}
|
columns={pageConfig.columns}
|
||||||
data={seedKeywords}
|
data={seedKeywords}
|
||||||
@@ -581,6 +630,11 @@ export default function KeywordOpportunities() {
|
|||||||
}}
|
}}
|
||||||
onRowAction={async (actionKey: string, row: SeedKeyword & { isAdded?: boolean }) => {
|
onRowAction={async (actionKey: string, row: SeedKeyword & { isAdded?: boolean }) => {
|
||||||
if (actionKey === 'add_to_workflow') {
|
if (actionKey === 'add_to_workflow') {
|
||||||
|
// Check if sector is selected
|
||||||
|
if (!activeSector) {
|
||||||
|
toast.error('Please select a sector first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Don't allow adding already-added keywords
|
// Don't allow adding already-added keywords
|
||||||
if (row.isAdded) {
|
if (row.isAdded) {
|
||||||
toast.info('This keyword is already added to workflow');
|
toast.info('This keyword is already added to workflow');
|
||||||
@@ -589,12 +643,17 @@ export default function KeywordOpportunities() {
|
|||||||
await handleAddToWorkflow([row.id]);
|
await handleAddToWorkflow([row.id]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
bulkActions={pageConfig.bulkActions}
|
||||||
onBulkAction={async (actionKey: string, ids: string[]) => {
|
onBulkAction={async (actionKey: string, ids: string[]) => {
|
||||||
if (actionKey === 'add_selected_to_workflow') {
|
if (actionKey === 'add_selected_to_workflow') {
|
||||||
|
if (!activeSector) {
|
||||||
|
toast.error('Please select a sector first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await handleBulkAddSelected(ids);
|
await handleBulkAddSelected(ids);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCreate={handleAddAll}
|
onCreate={activeSector ? handleAddAll : undefined}
|
||||||
createLabel="Add All to Workflow"
|
createLabel="Add All to Workflow"
|
||||||
onCreateIcon={<PlusIcon />}
|
onCreateIcon={<PlusIcon />}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -317,24 +317,42 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(`Successfully added ${result.created} keyword(s) to workflow`);
|
// Show success message with created count
|
||||||
|
if (result.created > 0) {
|
||||||
|
toast.success(`Successfully added ${result.created} keyword(s) to workflow`);
|
||||||
|
}
|
||||||
|
|
||||||
// Track as recently added
|
// Show skipped count if any
|
||||||
seedKeywordIds.forEach(id => {
|
if (result.skipped > 0) {
|
||||||
recentlyAddedRef.current.add(id);
|
toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Show detailed errors if any
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
result.errors.forEach((error: string) => {
|
||||||
|
toast.error(error, { duration: 8000 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only track and mark as added if actually created
|
||||||
|
if (result.created > 0) {
|
||||||
|
// Track as recently added
|
||||||
|
seedKeywordIds.forEach(id => {
|
||||||
|
recentlyAddedRef.current.add(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update state - mark as added
|
||||||
|
setSeedKeywords(prevKeywords =>
|
||||||
|
prevKeywords.map(kw =>
|
||||||
|
seedKeywordIds.includes(kw.id)
|
||||||
|
? { ...kw, isAdded: true }
|
||||||
|
: kw
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear selection
|
// Clear selection
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
|
|
||||||
// Update state immediately
|
|
||||||
setSeedKeywords(prevKeywords =>
|
|
||||||
prevKeywords.map(kw =>
|
|
||||||
seedKeywordIds.includes(kw.id)
|
|
||||||
? { ...kw, isAdded: true }
|
|
||||||
: kw
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Failed to add keywords: ${result.errors?.join(', ') || 'Unknown error'}`);
|
toast.error(`Failed to add keywords: ${result.errors?.join(', ') || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
@@ -531,20 +549,28 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
label: '',
|
label: '',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
align: 'right' as const,
|
align: 'right' as const,
|
||||||
render: (_value: any, row: SeedKeyword & { isAdded?: boolean }) => (
|
render: (_value: any, row: SeedKeyword & { isAdded?: boolean }) => {
|
||||||
<Button
|
const isDisabled = !activeSector || row.isAdded;
|
||||||
size="sm"
|
const buttonText = row.isAdded ? 'Added' : 'Add to Workflow';
|
||||||
variant={row.isAdded ? 'ghost' : 'primary'}
|
|
||||||
disabled={row.isAdded}
|
return (
|
||||||
onClick={() => {
|
<div className="flex items-center justify-end gap-2">
|
||||||
if (!row.isAdded) {
|
<Button
|
||||||
handleAddToWorkflow([row.id]);
|
size="sm"
|
||||||
}
|
variant={row.isAdded ? 'ghost' : 'primary'}
|
||||||
}}
|
disabled={isDisabled}
|
||||||
>
|
onClick={() => {
|
||||||
{row.isAdded ? 'Added' : 'Add to Workflow'}
|
if (!row.isAdded && activeSector) {
|
||||||
</Button>
|
handleAddToWorkflow([row.id]);
|
||||||
),
|
}
|
||||||
|
}}
|
||||||
|
title={!activeSector ? 'Please select a sector first' : ''}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
@@ -580,7 +606,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bulkActions: [
|
bulkActions: !activeSector ? [] : [
|
||||||
{
|
{
|
||||||
key: 'add_selected_to_workflow',
|
key: 'add_selected_to_workflow',
|
||||||
label: 'Add Selected to Workflow',
|
label: 'Add Selected to Workflow',
|
||||||
@@ -631,6 +657,29 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
title="Add Keywords"
|
title="Add Keywords"
|
||||||
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
badge={{ icon: <BoltIcon />, color: 'blue' }}
|
||||||
/>
|
/>
|
||||||
|
{/* Show info banner when no sector is selected */}
|
||||||
|
{!activeSector && activeSite && (
|
||||||
|
<div className="mx-6 mt-6 mb-4">
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-200">
|
||||||
|
Select a Sector to Add Keywords
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Please select a sector from the dropdown above to enable adding keywords to your workflow. Keywords must be added to a specific sector.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<TablePageTemplate
|
<TablePageTemplate
|
||||||
columns={pageConfig.columns}
|
columns={pageConfig.columns}
|
||||||
data={seedKeywords}
|
data={seedKeywords}
|
||||||
|
|||||||
@@ -1906,10 +1906,10 @@ export async function fetchSeedKeywords(filters?: {
|
|||||||
/**
|
/**
|
||||||
* Add SeedKeywords to workflow (create Keywords records)
|
* Add SeedKeywords to workflow (create Keywords records)
|
||||||
*/
|
*/
|
||||||
export async function addSeedKeywordsToWorkflow(seedKeywordIds: number[], siteId: number, sectorId: number): Promise<{ success: boolean; created: number; errors?: string[] }> {
|
export async function addSeedKeywordsToWorkflow(seedKeywordIds: number[], siteId: number, sectorId: number): Promise<{ success: boolean; created: number; skipped?: number; errors?: string[] }> {
|
||||||
try {
|
try {
|
||||||
// fetchAPI extracts data from unified format {success: true, data: {...}}
|
// fetchAPI extracts data from unified format {success: true, data: {...}}
|
||||||
// So response is already the data object: {created: X, ...}
|
// So response is already the data object: {created: X, skipped: X, errors: [...]}
|
||||||
const response = await fetchAPI('/v1/planner/keywords/bulk_add_from_seed/', {
|
const response = await fetchAPI('/v1/planner/keywords/bulk_add_from_seed/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user