filters fixed for all pages of planner writer
This commit is contained in:
@@ -988,18 +988,43 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
|
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
|
||||||
def filter_options(self, request):
|
def filter_options(self, request):
|
||||||
"""
|
"""
|
||||||
Get distinct filter values from current data.
|
Get distinct filter values from current data with cascading support.
|
||||||
Returns only values that exist in the current site's content.
|
Returns only values that exist based on other active filters.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
# Get distinct statuses
|
# Get filter parameters for cascading
|
||||||
statuses = list(set(queryset.values_list('status', flat=True)))
|
status_filter = request.query_params.get('status', '')
|
||||||
|
site_status_filter = request.query_params.get('site_status', '')
|
||||||
|
content_type_filter = request.query_params.get('content_type', '')
|
||||||
|
content_structure_filter = request.query_params.get('content_structure', '')
|
||||||
|
source_filter = request.query_params.get('source', '')
|
||||||
|
search = request.query_params.get('search', '')
|
||||||
|
|
||||||
|
# Apply search to base queryset
|
||||||
|
base_qs = queryset
|
||||||
|
if search:
|
||||||
|
base_qs = base_qs.filter(
|
||||||
|
Q(title__icontains=search) | Q(summary__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get statuses (filtered by other fields)
|
||||||
|
status_qs = base_qs
|
||||||
|
if site_status_filter:
|
||||||
|
status_qs = status_qs.filter(site_status=site_status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
status_qs = status_qs.filter(content_type=content_type_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
status_qs = status_qs.filter(content_structure=content_structure_filter)
|
||||||
|
if source_filter:
|
||||||
|
status_qs = status_qs.filter(source=source_filter)
|
||||||
|
statuses = list(set(status_qs.values_list('status', flat=True)))
|
||||||
statuses = sorted([s for s in statuses if s])
|
statuses = sorted([s for s in statuses if s])
|
||||||
status_labels = {
|
status_labels = {
|
||||||
'draft': 'Draft',
|
'draft': 'Draft',
|
||||||
@@ -1012,8 +1037,17 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct site_status
|
# Get site_statuses (filtered by other fields)
|
||||||
site_statuses = list(set(queryset.values_list('site_status', flat=True)))
|
site_status_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
site_status_qs = site_status_qs.filter(status=status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
site_status_qs = site_status_qs.filter(content_type=content_type_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
site_status_qs = site_status_qs.filter(content_structure=content_structure_filter)
|
||||||
|
if source_filter:
|
||||||
|
site_status_qs = site_status_qs.filter(source=source_filter)
|
||||||
|
site_statuses = list(set(site_status_qs.values_list('site_status', flat=True)))
|
||||||
site_statuses = sorted([s for s in site_statuses if s])
|
site_statuses = sorted([s for s in site_statuses if s])
|
||||||
site_status_labels = {
|
site_status_labels = {
|
||||||
'not_published': 'Not Published',
|
'not_published': 'Not Published',
|
||||||
@@ -1027,8 +1061,17 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
for s in site_statuses
|
for s in site_statuses
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct content_types
|
# Get content types (filtered by other fields)
|
||||||
content_types = list(set(queryset.values_list('content_type', flat=True)))
|
type_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
type_qs = type_qs.filter(status=status_filter)
|
||||||
|
if site_status_filter:
|
||||||
|
type_qs = type_qs.filter(site_status=site_status_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
type_qs = type_qs.filter(content_structure=content_structure_filter)
|
||||||
|
if source_filter:
|
||||||
|
type_qs = type_qs.filter(source=source_filter)
|
||||||
|
content_types = list(set(type_qs.values_list('content_type', flat=True)))
|
||||||
content_types = sorted([t for t in content_types if t])
|
content_types = sorted([t for t in content_types if t])
|
||||||
type_labels = {
|
type_labels = {
|
||||||
'post': 'Post',
|
'post': 'Post',
|
||||||
@@ -1041,8 +1084,17 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
for t in content_types
|
for t in content_types
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct content_structures
|
# Get content structures (filtered by other fields)
|
||||||
structures = list(set(queryset.values_list('content_structure', flat=True)))
|
structure_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
structure_qs = structure_qs.filter(status=status_filter)
|
||||||
|
if site_status_filter:
|
||||||
|
structure_qs = structure_qs.filter(site_status=site_status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
structure_qs = structure_qs.filter(content_type=content_type_filter)
|
||||||
|
if source_filter:
|
||||||
|
structure_qs = structure_qs.filter(source=source_filter)
|
||||||
|
structures = list(set(structure_qs.values_list('content_structure', flat=True)))
|
||||||
structures = sorted([s for s in structures if s])
|
structures = sorted([s for s in structures if s])
|
||||||
structure_labels = {
|
structure_labels = {
|
||||||
'article': 'Article', 'guide': 'Guide', 'comparison': 'Comparison',
|
'article': 'Article', 'guide': 'Guide', 'comparison': 'Comparison',
|
||||||
@@ -1057,8 +1109,17 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
for s in structures
|
for s in structures
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct sources
|
# Get sources (filtered by other fields)
|
||||||
sources = list(set(queryset.values_list('source', flat=True)))
|
source_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
source_qs = source_qs.filter(status=status_filter)
|
||||||
|
if site_status_filter:
|
||||||
|
source_qs = source_qs.filter(site_status=site_status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
source_qs = source_qs.filter(content_type=content_type_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
source_qs = source_qs.filter(content_structure=content_structure_filter)
|
||||||
|
sources = list(set(source_qs.values_list('source', flat=True)))
|
||||||
sources = sorted([s for s in sources if s])
|
sources = sorted([s for s in sources if s])
|
||||||
source_labels = {
|
source_labels = {
|
||||||
'igny8': 'IGNY8',
|
'igny8': 'IGNY8',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
bulkDeleteClusters,
|
bulkDeleteClusters,
|
||||||
bulkUpdateClustersStatus,
|
bulkUpdateClustersStatus,
|
||||||
autoGenerateIdeas,
|
autoGenerateIdeas,
|
||||||
|
fetchPlannerClusterFilterOptions,
|
||||||
Cluster,
|
Cluster,
|
||||||
ClusterFilters,
|
ClusterFilters,
|
||||||
ClusterCreateData,
|
ClusterCreateData,
|
||||||
@@ -91,73 +92,62 @@ export default function Clusters() {
|
|||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
const hasReloadedRef = useRef(false);
|
const hasReloadedRef = useRef(false);
|
||||||
|
|
||||||
// Load dynamic filter options based on current site's data
|
// Parse difficulty filter to min/max values
|
||||||
const loadFilterOptions = useCallback(async () => {
|
// Backend uses: 1=0-10, 2=11-30, 3=31-50, 4=51-70, 5=71-100
|
||||||
|
const getDifficultyRange = useCallback((filter: string): { min?: number; max?: number } => {
|
||||||
|
if (!filter) return {};
|
||||||
|
const level = parseInt(filter, 10);
|
||||||
|
if (isNaN(level)) return {};
|
||||||
|
// Map difficulty level to raw difficulty range matching backend logic
|
||||||
|
const ranges: Record<number, { min: number; max: number }> = {
|
||||||
|
1: { min: 0, max: 10 },
|
||||||
|
2: { min: 11, max: 30 },
|
||||||
|
3: { min: 31, max: 50 },
|
||||||
|
4: { min: 51, max: 70 },
|
||||||
|
5: { min: 71, max: 100 },
|
||||||
|
};
|
||||||
|
return ranges[level] || {};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load dynamic filter options based on current site's data and applied filters
|
||||||
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
|
// given the other currently applied filters
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
|
status?: string;
|
||||||
|
difficulty_min?: number;
|
||||||
|
difficulty_max?: number;
|
||||||
|
volume_min?: number;
|
||||||
|
volume_max?: number;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/planner/clusters/filter_options/?site_id=${activeSite.id}`, {
|
const options = await fetchPlannerClusterFilterOptions(activeSite.id, currentFilters);
|
||||||
credentials: 'include',
|
setStatusOptions(options.statuses || []);
|
||||||
});
|
setDifficultyOptions(options.difficulties || []);
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setDifficultyOptions(result.data.difficulties || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter options:', error);
|
console.error('Error loading filter options:', error);
|
||||||
}
|
}
|
||||||
}, [activeSite]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when site changes
|
// Load filter options when site changes (initial load with no filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [loadFilterOptions]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Reload filter options when filters change (cascading)
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSite) return;
|
const { min: difficultyMin, max: difficultyMax } = getDifficultyRange(difficultyFilter);
|
||||||
|
loadFilterOptions({
|
||||||
const loadCascadingOptions = async () => {
|
status: statusFilter || undefined,
|
||||||
try {
|
difficulty_min: difficultyMin,
|
||||||
const params = new URLSearchParams();
|
difficulty_max: difficultyMax,
|
||||||
params.append('site_id', activeSite.id.toString());
|
volume_min: volumeMin !== '' ? Number(volumeMin) : undefined,
|
||||||
if (statusFilter) params.append('status', statusFilter);
|
volume_max: volumeMax !== '' ? Number(volumeMax) : undefined,
|
||||||
if (difficultyFilter) {
|
search: searchTerm || undefined,
|
||||||
// Map difficulty level to min/max range for backend
|
});
|
||||||
const difficultyNum = parseInt(difficultyFilter);
|
}, [statusFilter, difficultyFilter, volumeMin, volumeMax, searchTerm, loadFilterOptions, getDifficultyRange]);
|
||||||
const ranges: Record<number, { min: number; max: number }> = {
|
|
||||||
1: { min: 0, max: 10 },
|
|
||||||
2: { min: 11, max: 30 },
|
|
||||||
3: { min: 31, max: 50 },
|
|
||||||
4: { min: 51, max: 70 },
|
|
||||||
5: { min: 71, max: 100 },
|
|
||||||
};
|
|
||||||
const range = ranges[difficultyNum];
|
|
||||||
if (range) {
|
|
||||||
params.append('difficulty_min', range.min.toString());
|
|
||||||
params.append('difficulty_max', range.max.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (volumeMin) params.append('volume_min', volumeMin.toString());
|
|
||||||
if (volumeMax) params.append('volume_max', volumeMax.toString());
|
|
||||||
if (searchTerm) params.append('search', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/planner/clusters/filter_options/?${params.toString()}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setDifficultyOptions(result.data.difficulties || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading cascading filter options:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCascadingOptions();
|
|
||||||
}, [activeSite, statusFilter, difficultyFilter, volumeMin, volumeMax, searchTerm]);
|
|
||||||
|
|
||||||
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
|
|||||||
@@ -107,12 +107,20 @@ export default function Ideas() {
|
|||||||
loadClusters();
|
loadClusters();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load dynamic filter options based on current site's data
|
// Load dynamic filter options based on current site's data and applied filters
|
||||||
const loadFilterOptions = useCallback(async () => {
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
|
// given the other currently applied filters
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
|
status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
cluster?: string;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = await fetchPlannerIdeasFilterOptions(activeSite.id);
|
const options = await fetchPlannerIdeasFilterOptions(activeSite.id, currentFilters);
|
||||||
setStatusOptions(options.statuses || []);
|
setStatusOptions(options.statuses || []);
|
||||||
setContentTypeOptions(options.content_types || []);
|
setContentTypeOptions(options.content_types || []);
|
||||||
setContentStructureOptions(options.content_structures || []);
|
setContentStructureOptions(options.content_structures || []);
|
||||||
@@ -122,42 +130,21 @@ export default function Ideas() {
|
|||||||
}
|
}
|
||||||
}, [activeSite]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when site changes
|
// Load filter options when site changes (initial load with no filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [loadFilterOptions]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Reload filter options when filters change (cascading)
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSite) return;
|
loadFilterOptions({
|
||||||
|
status: statusFilter || undefined,
|
||||||
const loadCascadingOptions = async () => {
|
content_type: typeFilter || undefined,
|
||||||
try {
|
content_structure: structureFilter || undefined,
|
||||||
const params = new URLSearchParams();
|
cluster: clusterFilter || undefined,
|
||||||
params.append('site_id', activeSite.id.toString());
|
search: searchTerm || undefined,
|
||||||
if (statusFilter) params.append('status', statusFilter);
|
});
|
||||||
if (typeFilter) params.append('content_type', typeFilter);
|
}, [statusFilter, typeFilter, structureFilter, clusterFilter, searchTerm, loadFilterOptions]);
|
||||||
if (structureFilter) params.append('content_structure', structureFilter);
|
|
||||||
if (clusterFilter) params.append('cluster', clusterFilter);
|
|
||||||
if (searchTerm) params.append('search', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/planner/ideas/filter_options/?${params.toString()}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setContentTypeOptions(result.data.content_types || []);
|
|
||||||
setContentStructureOptions(result.data.content_structures || []);
|
|
||||||
setClusterOptions(result.data.clusters || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading cascading filter options:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCascadingOptions();
|
|
||||||
}, [activeSite, statusFilter, typeFilter, structureFilter, clusterFilter, searchTerm]);
|
|
||||||
|
|
||||||
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate';
|
|||||||
import {
|
import {
|
||||||
fetchContent,
|
fetchContent,
|
||||||
fetchImages,
|
fetchImages,
|
||||||
|
fetchWriterContentFilterOptions,
|
||||||
Content,
|
Content,
|
||||||
ContentListResponse,
|
ContentListResponse,
|
||||||
ContentFilters,
|
ContentFilters,
|
||||||
@@ -69,38 +70,44 @@ export default function Approved() {
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
// Load dynamic filter options with cascading
|
// Load dynamic filter options based on current site's data and applied filters
|
||||||
const loadFilterOptions = useCallback(async () => {
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
|
// given the other currently applied filters
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
|
status?: string;
|
||||||
|
site_status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
|
||||||
params.append('site_id', activeSite.id.toString());
|
setStatusOptions(options.statuses || []);
|
||||||
if (statusFilter) params.append('status', statusFilter);
|
setSiteStatusOptions(options.site_statuses || []);
|
||||||
if (siteStatusFilter) params.append('site_status', siteStatusFilter);
|
setContentTypeOptions(options.content_types || []);
|
||||||
if (contentTypeFilter) params.append('content_type', contentTypeFilter);
|
setContentStructureOptions(options.content_structures || []);
|
||||||
if (contentStructureFilter) params.append('content_structure', contentStructureFilter);
|
|
||||||
if (searchTerm) params.append('search', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/writer/content/filter_options/?${params.toString()}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setSiteStatusOptions(result.data.site_statuses || []);
|
|
||||||
setContentTypeOptions(result.data.content_types || []);
|
|
||||||
setContentStructureOptions(result.data.content_structures || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter options:', error);
|
console.error('Error loading filter options:', error);
|
||||||
}
|
}
|
||||||
}, [activeSite, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when dependencies change
|
// Load filter options when site changes (initial load with no filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [loadFilterOptions]);
|
}, [activeSite]);
|
||||||
|
|
||||||
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions({
|
||||||
|
status: statusFilter || undefined,
|
||||||
|
site_status: siteStatusFilter || undefined,
|
||||||
|
content_type: contentTypeFilter || undefined,
|
||||||
|
content_structure: contentStructureFilter || undefined,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
});
|
||||||
|
}, [statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm, loadFilterOptions]);
|
||||||
|
|
||||||
// Load total metrics for footer widget and header metrics (not affected by pagination)
|
// Load total metrics for footer widget and header metrics (not affected by pagination)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate';
|
|||||||
import {
|
import {
|
||||||
fetchContent,
|
fetchContent,
|
||||||
fetchImages,
|
fetchImages,
|
||||||
|
fetchWriterContentFilterOptions,
|
||||||
Content as ContentType,
|
Content as ContentType,
|
||||||
ContentFilters,
|
ContentFilters,
|
||||||
generateImagePrompts,
|
generateImagePrompts,
|
||||||
@@ -74,38 +75,46 @@ export default function Content() {
|
|||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
const hasReloadedRef = useRef(false);
|
const hasReloadedRef = useRef(false);
|
||||||
|
|
||||||
// Load dynamic filter options
|
// Load dynamic filter options based on current site's data and applied filters
|
||||||
const loadFilterOptions = useCallback(async () => {
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
|
// given the other currently applied filters
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
|
status?: string;
|
||||||
|
site_status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
source?: string;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
|
||||||
params.append('site_id', activeSite.id.toString());
|
setStatusOptions(options.statuses || []);
|
||||||
if (statusFilter) params.append('status', statusFilter);
|
setSourceOptions(options.sources || []);
|
||||||
if (sourceFilter) params.append('source', sourceFilter);
|
setContentTypeOptions(options.content_types || []);
|
||||||
if (contentTypeFilter) params.append('content_type', contentTypeFilter);
|
setContentStructureOptions(options.content_structures || []);
|
||||||
if (contentStructureFilter) params.append('content_structure', contentStructureFilter);
|
|
||||||
if (searchTerm) params.append('search', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/writer/content/filter_options/?${params.toString()}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setSourceOptions(result.data.sources || []);
|
|
||||||
setContentTypeOptions(result.data.content_types || []);
|
|
||||||
setContentStructureOptions(result.data.content_structures || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter options:', error);
|
console.error('Error loading filter options:', error);
|
||||||
}
|
}
|
||||||
}, [activeSite, statusFilter, sourceFilter, contentTypeFilter, contentStructureFilter, searchTerm]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when dependencies change
|
// Load filter options when site changes (initial load with no filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [loadFilterOptions]);
|
}, [activeSite]);
|
||||||
|
|
||||||
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions({
|
||||||
|
status: statusFilter || undefined,
|
||||||
|
content_type: contentTypeFilter || undefined,
|
||||||
|
content_structure: contentStructureFilter || undefined,
|
||||||
|
source: sourceFilter || undefined,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
});
|
||||||
|
}, [statusFilter, contentTypeFilter, contentStructureFilter, sourceFilter, searchTerm, loadFilterOptions]);
|
||||||
|
|
||||||
|
|
||||||
// Load total metrics for footer widget and header metrics (site-wide totals, no sector filter)
|
// Load total metrics for footer widget and header metrics (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate';
|
|||||||
import {
|
import {
|
||||||
fetchContent,
|
fetchContent,
|
||||||
fetchImages,
|
fetchImages,
|
||||||
|
fetchWriterContentFilterOptions,
|
||||||
Content,
|
Content,
|
||||||
ContentListResponse,
|
ContentListResponse,
|
||||||
ContentFilters,
|
ContentFilters,
|
||||||
@@ -67,38 +68,44 @@ export default function Review() {
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
// Load dynamic filter options
|
// Load dynamic filter options based on current site's data and applied filters
|
||||||
const loadFilterOptions = useCallback(async () => {
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
|
// given the other currently applied filters
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
|
status?: string;
|
||||||
|
site_status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
|
||||||
params.append('site_id', activeSite.id.toString());
|
setStatusOptions(options.statuses || []);
|
||||||
params.append('status', 'review'); // Always review status
|
setSiteStatusOptions(options.site_statuses || []);
|
||||||
if (siteStatusFilter) params.append('site_status', siteStatusFilter);
|
setContentTypeOptions(options.content_types || []);
|
||||||
if (contentTypeFilter) params.append('content_type', contentTypeFilter);
|
setContentStructureOptions(options.content_structures || []);
|
||||||
if (contentStructureFilter) params.append('content_structure', contentStructureFilter);
|
|
||||||
if (searchTerm) params.append('search', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/writer/content/filter_options/?${params.toString()}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setSiteStatusOptions(result.data.site_statuses || []);
|
|
||||||
setContentTypeOptions(result.data.content_types || []);
|
|
||||||
setContentStructureOptions(result.data.content_structures || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter options:', error);
|
console.error('Error loading filter options:', error);
|
||||||
}
|
}
|
||||||
}, [activeSite, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when dependencies change
|
// Load filter options when site changes (initial load with no filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions({ status: 'review' }); // Always pass review status
|
||||||
}, [loadFilterOptions]);
|
}, [activeSite]);
|
||||||
|
|
||||||
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions({
|
||||||
|
status: 'review', // Always review status
|
||||||
|
site_status: siteStatusFilter || undefined,
|
||||||
|
content_type: contentTypeFilter || undefined,
|
||||||
|
content_structure: contentStructureFilter || undefined,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
});
|
||||||
|
}, [siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm, loadFilterOptions]);
|
||||||
|
|
||||||
// Load content - filtered for review status
|
// Load content - filtered for review status
|
||||||
const loadContent = useCallback(async () => {
|
const loadContent = useCallback(async () => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
bulkUpdateTasksStatus,
|
bulkUpdateTasksStatus,
|
||||||
autoGenerateContent,
|
autoGenerateContent,
|
||||||
autoGenerateImages,
|
autoGenerateImages,
|
||||||
|
fetchWriterTaskFilterOptions,
|
||||||
Task,
|
Task,
|
||||||
TasksFilters,
|
TasksFilters,
|
||||||
TaskCreateData,
|
TaskCreateData,
|
||||||
@@ -106,40 +107,47 @@ export default function Tasks() {
|
|||||||
|
|
||||||
const hasReloadedRef = useRef<boolean>(false);
|
const hasReloadedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
// Load dynamic filter options with cascading
|
// Load dynamic filter options based on current site's data and applied filters
|
||||||
const loadFilterOptions = useCallback(async () => {
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
|
// given the other currently applied filters
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
|
status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
cluster?: string;
|
||||||
|
source?: string;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const options = await fetchWriterTaskFilterOptions(activeSite.id, currentFilters);
|
||||||
params.append('site_id', activeSite.id.toString());
|
setStatusOptions(options.statuses || []);
|
||||||
if (statusFilter) params.append('status', statusFilter);
|
setContentTypeOptions(options.content_types || []);
|
||||||
if (typeFilter) params.append('content_type', typeFilter);
|
setContentStructureOptions(options.content_structures || []);
|
||||||
if (structureFilter) params.append('content_structure', structureFilter);
|
setClusterOptions(options.clusters || []);
|
||||||
if (clusterFilter) params.append('cluster', clusterFilter);
|
setSourceOptions(options.sources || []);
|
||||||
if (sourceFilter) params.append('source', sourceFilter);
|
|
||||||
if (searchTerm) params.append('search', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/writer/tasks/filter_options/?${params.toString()}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setStatusOptions(result.data.statuses || []);
|
|
||||||
setContentTypeOptions(result.data.content_types || []);
|
|
||||||
setContentStructureOptions(result.data.content_structures || []);
|
|
||||||
setClusterOptions(result.data.clusters || []);
|
|
||||||
setSourceOptions(result.data.sources || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter options:', error);
|
console.error('Error loading filter options:', error);
|
||||||
}
|
}
|
||||||
}, [activeSite, statusFilter, typeFilter, structureFilter, clusterFilter, sourceFilter, searchTerm]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when dependencies change
|
// Load filter options when site changes (initial load with no filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [loadFilterOptions]);
|
}, [activeSite]);
|
||||||
|
|
||||||
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions({
|
||||||
|
status: statusFilter || undefined,
|
||||||
|
content_type: typeFilter || undefined,
|
||||||
|
content_structure: structureFilter || undefined,
|
||||||
|
cluster: clusterFilter || undefined,
|
||||||
|
source: sourceFilter || undefined,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
});
|
||||||
|
}, [statusFilter, typeFilter, structureFilter, clusterFilter, sourceFilter, searchTerm, loadFilterOptions]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -789,11 +789,33 @@ export async function fetchPlannerKeywordFilterOptions(
|
|||||||
// Clusters filter options
|
// Clusters filter options
|
||||||
export interface PlannerClusterFilterOptions {
|
export interface PlannerClusterFilterOptions {
|
||||||
statuses: FilterOption[];
|
statuses: FilterOption[];
|
||||||
|
difficulties: FilterOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlannerClusterFilterOptions(siteId?: number): Promise<PlannerClusterFilterOptions> {
|
export interface ClusterFilterOptionsRequest {
|
||||||
const queryParams = siteId ? `?site_id=${siteId}` : '';
|
status?: string;
|
||||||
return fetchAPI(`/v1/planner/clusters/filter_options/${queryParams}`);
|
difficulty_min?: number;
|
||||||
|
difficulty_max?: number;
|
||||||
|
volume_min?: number;
|
||||||
|
volume_max?: number;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPlannerClusterFilterOptions(
|
||||||
|
siteId?: number,
|
||||||
|
filters?: ClusterFilterOptionsRequest
|
||||||
|
): Promise<PlannerClusterFilterOptions> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (siteId) params.append('site_id', siteId.toString());
|
||||||
|
if (filters?.status) params.append('status', filters.status);
|
||||||
|
if (filters?.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString());
|
||||||
|
if (filters?.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString());
|
||||||
|
if (filters?.volume_min !== undefined) params.append('volume_min', filters.volume_min.toString());
|
||||||
|
if (filters?.volume_max !== undefined) params.append('volume_max', filters.volume_max.toString());
|
||||||
|
if (filters?.search) params.append('search', filters.search);
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return fetchAPI(`/v1/planner/clusters/filter_options/${queryString ? `?${queryString}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ideas filter options
|
// Ideas filter options
|
||||||
@@ -804,9 +826,63 @@ export interface PlannerIdeasFilterOptions {
|
|||||||
clusters: FilterOption[];
|
clusters: FilterOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlannerIdeasFilterOptions(siteId?: number): Promise<PlannerIdeasFilterOptions> {
|
export interface IdeasFilterOptionsRequest {
|
||||||
const queryParams = siteId ? `?site_id=${siteId}` : '';
|
status?: string;
|
||||||
return fetchAPI(`/v1/planner/ideas/filter_options/${queryParams}`);
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
cluster?: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPlannerIdeasFilterOptions(
|
||||||
|
siteId?: number,
|
||||||
|
filters?: IdeasFilterOptionsRequest
|
||||||
|
): Promise<PlannerIdeasFilterOptions> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (siteId) params.append('site_id', siteId.toString());
|
||||||
|
if (filters?.status) params.append('status', filters.status);
|
||||||
|
if (filters?.content_type) params.append('content_type', filters.content_type);
|
||||||
|
if (filters?.content_structure) params.append('content_structure', filters.content_structure);
|
||||||
|
if (filters?.cluster) params.append('cluster', filters.cluster);
|
||||||
|
if (filters?.search) params.append('search', filters.search);
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return fetchAPI(`/v1/planner/ideas/filter_options/${queryString ? `?${queryString}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks filter options (Writer module)
|
||||||
|
export interface WriterTaskFilterOptions {
|
||||||
|
statuses: FilterOption[];
|
||||||
|
content_types: FilterOption[];
|
||||||
|
content_structures: FilterOption[];
|
||||||
|
clusters: FilterOption[];
|
||||||
|
sources: FilterOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskFilterOptionsRequest {
|
||||||
|
status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
cluster?: string;
|
||||||
|
source?: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchWriterTaskFilterOptions(
|
||||||
|
siteId?: number,
|
||||||
|
filters?: TaskFilterOptionsRequest
|
||||||
|
): Promise<WriterTaskFilterOptions> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (siteId) params.append('site_id', siteId.toString());
|
||||||
|
if (filters?.status) params.append('status', filters.status);
|
||||||
|
if (filters?.content_type) params.append('content_type', filters.content_type);
|
||||||
|
if (filters?.content_structure) params.append('content_structure', filters.content_structure);
|
||||||
|
if (filters?.cluster) params.append('cluster', filters.cluster);
|
||||||
|
if (filters?.source) params.append('source', filters.source);
|
||||||
|
if (filters?.search) params.append('search', filters.search);
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return fetchAPI(`/v1/writer/tasks/filter_options/${queryString ? `?${queryString}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content filter options (Writer module)
|
// Content filter options (Writer module)
|
||||||
@@ -818,9 +894,30 @@ export interface WriterContentFilterOptions {
|
|||||||
sources: FilterOption[];
|
sources: FilterOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWriterContentFilterOptions(siteId?: number): Promise<WriterContentFilterOptions> {
|
export interface ContentFilterOptionsRequest {
|
||||||
const queryParams = siteId ? `?site_id=${siteId}` : '';
|
status?: string;
|
||||||
return fetchAPI(`/v1/writer/content/filter_options/${queryParams}`);
|
site_status?: string;
|
||||||
|
content_type?: string;
|
||||||
|
content_structure?: string;
|
||||||
|
source?: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchWriterContentFilterOptions(
|
||||||
|
siteId?: number,
|
||||||
|
filters?: ContentFilterOptionsRequest
|
||||||
|
): Promise<WriterContentFilterOptions> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (siteId) params.append('site_id', siteId.toString());
|
||||||
|
if (filters?.status) params.append('status', filters.status);
|
||||||
|
if (filters?.site_status) params.append('site_status', filters.site_status);
|
||||||
|
if (filters?.content_type) params.append('content_type', filters.content_type);
|
||||||
|
if (filters?.content_structure) params.append('content_structure', filters.content_structure);
|
||||||
|
if (filters?.source) params.append('source', filters.source);
|
||||||
|
if (filters?.search) params.append('search', filters.search);
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return fetchAPI(`/v1/writer/content/filter_options/${queryString ? `?${queryString}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clusters-specific API functions
|
// Clusters-specific API functions
|
||||||
|
|||||||
Reference in New Issue
Block a user