filter fixes
This commit is contained in:
@@ -46,9 +46,14 @@ class ClustersFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class ContentIdeasFilter(django_filters.FilterSet):
|
class ContentIdeasFilter(django_filters.FilterSet):
|
||||||
"""Custom filter for ContentIdeas with date range support"""
|
"""Custom filter for ContentIdeas with date range support.
|
||||||
|
Uses CharFilter for content_type and content_structure to accept any value
|
||||||
|
(database may have legacy values not in current model choices).
|
||||||
|
"""
|
||||||
created_at__gte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte')
|
created_at__gte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte')
|
||||||
created_at__lte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte')
|
created_at__lte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte')
|
||||||
|
content_type = django_filters.CharFilter(field_name='content_type')
|
||||||
|
content_structure = django_filters.CharFilter(field_name='content_structure')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentIdeas
|
model = ContentIdeas
|
||||||
@@ -1378,6 +1383,8 @@ class ClusterViewSet(SiteSectorModelViewSet):
|
|||||||
'mapped': 'Mapped',
|
'mapped': 'Mapped',
|
||||||
}
|
}
|
||||||
status_options = [
|
status_options = [
|
||||||
|
{'value': '', 'label': 'All Status'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': status_labels.get(s, s.title())}
|
{'value': s, 'label': status_labels.get(s, s.title())}
|
||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
@@ -1428,6 +1435,8 @@ class ClusterViewSet(SiteSectorModelViewSet):
|
|||||||
5: '5 - Very Hard',
|
5: '5 - Very Hard',
|
||||||
}
|
}
|
||||||
difficulty_options = [
|
difficulty_options = [
|
||||||
|
{'value': '', 'label': 'All Difficulty'},
|
||||||
|
] + [
|
||||||
{'value': str(d), 'label': difficulty_labels[d]}
|
{'value': str(d), 'label': difficulty_labels[d]}
|
||||||
for d in sorted(difficulty_levels)
|
for d in sorted(difficulty_levels)
|
||||||
]
|
]
|
||||||
@@ -1669,6 +1678,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
'completed': 'Completed',
|
'completed': 'Completed',
|
||||||
}
|
}
|
||||||
status_options = [
|
status_options = [
|
||||||
|
{'value': '', 'label': 'All Status'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': status_labels.get(s, s.title())}
|
{'value': s, 'label': status_labels.get(s, s.title())}
|
||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
@@ -1690,6 +1701,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
'taxonomy': 'Taxonomy',
|
'taxonomy': 'Taxonomy',
|
||||||
}
|
}
|
||||||
content_type_options = [
|
content_type_options = [
|
||||||
|
{'value': '', 'label': 'All Types'},
|
||||||
|
] + [
|
||||||
{'value': t, 'label': type_labels.get(t, t.title())}
|
{'value': t, 'label': type_labels.get(t, t.title())}
|
||||||
for t in content_types
|
for t in content_types
|
||||||
]
|
]
|
||||||
@@ -1713,6 +1726,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
||||||
}
|
}
|
||||||
content_structure_options = [
|
content_structure_options = [
|
||||||
|
{'value': '', 'label': 'All Structures'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
||||||
for s in structures
|
for s in structures
|
||||||
]
|
]
|
||||||
@@ -1731,6 +1746,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
))
|
))
|
||||||
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
||||||
cluster_options = [
|
cluster_options = [
|
||||||
|
{'value': '', 'label': 'All Clusters'},
|
||||||
|
] + [
|
||||||
{'value': str(c['id']), 'label': c['name']}
|
{'value': str(c['id']), 'label': c['name']}
|
||||||
for c in clusters
|
for c in clusters
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ from igny8_core.business.billing.exceptions import InsufficientCreditsError
|
|||||||
|
|
||||||
# Custom FilterSets with date range filtering support
|
# Custom FilterSets with date range filtering support
|
||||||
class TasksFilter(django_filters.FilterSet):
|
class TasksFilter(django_filters.FilterSet):
|
||||||
"""Custom filter for Tasks with date range support"""
|
"""Custom filter for Tasks with date range support.
|
||||||
|
Uses CharFilter for content_type and content_structure to accept any value
|
||||||
|
(database may have legacy values not in current model choices).
|
||||||
|
"""
|
||||||
created_at__gte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte')
|
created_at__gte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte')
|
||||||
created_at__lte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte')
|
created_at__lte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte')
|
||||||
|
content_type = django_filters.CharFilter(field_name='content_type')
|
||||||
|
content_structure = django_filters.CharFilter(field_name='content_structure')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tasks
|
model = Tasks
|
||||||
@@ -47,9 +52,14 @@ class ImagesFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class ContentFilter(django_filters.FilterSet):
|
class ContentFilter(django_filters.FilterSet):
|
||||||
"""Custom filter for Content with date range support"""
|
"""Custom filter for Content with date range support.
|
||||||
|
Uses CharFilter for content_type and content_structure to accept any value
|
||||||
|
(database may have legacy values not in current model choices).
|
||||||
|
"""
|
||||||
created_at__gte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte')
|
created_at__gte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte')
|
||||||
created_at__lte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte')
|
created_at__lte = django_filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte')
|
||||||
|
content_type = django_filters.CharFilter(field_name='content_type')
|
||||||
|
content_structure = django_filters.CharFilter(field_name='content_structure')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Content
|
model = Content
|
||||||
@@ -318,6 +328,8 @@ class TasksViewSet(SiteSectorModelViewSet):
|
|||||||
'failed': 'Failed',
|
'failed': 'Failed',
|
||||||
}
|
}
|
||||||
status_options = [
|
status_options = [
|
||||||
|
{'value': '', 'label': 'All Status'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': status_labels.get(s, s.title())}
|
{'value': s, 'label': status_labels.get(s, s.title())}
|
||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
@@ -339,6 +351,8 @@ class TasksViewSet(SiteSectorModelViewSet):
|
|||||||
'taxonomy': 'Taxonomy',
|
'taxonomy': 'Taxonomy',
|
||||||
}
|
}
|
||||||
content_type_options = [
|
content_type_options = [
|
||||||
|
{'value': '', 'label': 'All Types'},
|
||||||
|
] + [
|
||||||
{'value': t, 'label': type_labels.get(t, t.title())}
|
{'value': t, 'label': type_labels.get(t, t.title())}
|
||||||
for t in content_types
|
for t in content_types
|
||||||
]
|
]
|
||||||
@@ -362,6 +376,8 @@ class TasksViewSet(SiteSectorModelViewSet):
|
|||||||
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
||||||
}
|
}
|
||||||
content_structure_options = [
|
content_structure_options = [
|
||||||
|
{'value': '', 'label': 'All Structures'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
||||||
for s in structures
|
for s in structures
|
||||||
]
|
]
|
||||||
@@ -381,6 +397,8 @@ class TasksViewSet(SiteSectorModelViewSet):
|
|||||||
))
|
))
|
||||||
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
||||||
cluster_options = [
|
cluster_options = [
|
||||||
|
{'value': '', 'label': 'All Clusters'},
|
||||||
|
] + [
|
||||||
{'value': str(c['id']), 'label': c['name']}
|
{'value': str(c['id']), 'label': c['name']}
|
||||||
for c in clusters
|
for c in clusters
|
||||||
]
|
]
|
||||||
@@ -959,14 +977,21 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
|
|
||||||
# Get filter parameters for cascading
|
# Get filter parameters for cascading
|
||||||
status_filter = request.query_params.get('status', '')
|
status_filter = request.query_params.get('status', '')
|
||||||
|
status_in_filter = request.query_params.get('status__in', '') # Comma-separated list of statuses
|
||||||
site_status_filter = request.query_params.get('site_status', '')
|
site_status_filter = request.query_params.get('site_status', '')
|
||||||
content_type_filter = request.query_params.get('content_type', '')
|
content_type_filter = request.query_params.get('content_type', '')
|
||||||
content_structure_filter = request.query_params.get('content_structure', '')
|
content_structure_filter = request.query_params.get('content_structure', '')
|
||||||
source_filter = request.query_params.get('source', '')
|
source_filter = request.query_params.get('source', '')
|
||||||
search = request.query_params.get('search', '')
|
search = request.query_params.get('search', '')
|
||||||
|
|
||||||
# Apply search to base queryset
|
# Apply base status__in filter to restrict entire result set (e.g., for Approved page)
|
||||||
base_qs = queryset
|
base_qs = queryset
|
||||||
|
if status_in_filter:
|
||||||
|
status_list = [s.strip() for s in status_in_filter.split(',') if s.strip()]
|
||||||
|
if status_list:
|
||||||
|
base_qs = base_qs.filter(status__in=status_list)
|
||||||
|
|
||||||
|
# Apply search to base queryset
|
||||||
if search:
|
if search:
|
||||||
base_qs = base_qs.filter(
|
base_qs = base_qs.filter(
|
||||||
Q(title__icontains=search) | Q(summary__icontains=search)
|
Q(title__icontains=search) | Q(summary__icontains=search)
|
||||||
@@ -991,6 +1016,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
'published': 'Published',
|
'published': 'Published',
|
||||||
}
|
}
|
||||||
status_options = [
|
status_options = [
|
||||||
|
{'value': '', 'label': 'All Content Status'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': status_labels.get(s, s.title())}
|
{'value': s, 'label': status_labels.get(s, s.title())}
|
||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
@@ -1015,6 +1042,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
'failed': 'Failed',
|
'failed': 'Failed',
|
||||||
}
|
}
|
||||||
site_status_options = [
|
site_status_options = [
|
||||||
|
{'value': '', 'label': 'All Site Status'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': site_status_labels.get(s, s.replace('_', ' ').title())}
|
{'value': s, 'label': site_status_labels.get(s, s.replace('_', ' ').title())}
|
||||||
for s in site_statuses
|
for s in site_statuses
|
||||||
]
|
]
|
||||||
@@ -1038,6 +1067,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
'taxonomy': 'Taxonomy',
|
'taxonomy': 'Taxonomy',
|
||||||
}
|
}
|
||||||
content_type_options = [
|
content_type_options = [
|
||||||
|
{'value': '', 'label': 'All Types'},
|
||||||
|
] + [
|
||||||
{'value': t, 'label': type_labels.get(t, t.title())}
|
{'value': t, 'label': type_labels.get(t, t.title())}
|
||||||
for t in content_types
|
for t in content_types
|
||||||
]
|
]
|
||||||
@@ -1063,6 +1094,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
||||||
}
|
}
|
||||||
content_structure_options = [
|
content_structure_options = [
|
||||||
|
{'value': '', 'label': 'All Structures'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
||||||
for s in structures
|
for s in structures
|
||||||
]
|
]
|
||||||
@@ -1084,6 +1117,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
'wordpress': 'WordPress',
|
'wordpress': 'WordPress',
|
||||||
}
|
}
|
||||||
source_options = [
|
source_options = [
|
||||||
|
{'value': '', 'label': 'All Sources'},
|
||||||
|
] + [
|
||||||
{'value': s, 'label': source_labels.get(s, s.title())}
|
{'value': s, 'label': source_labels.get(s, s.title())}
|
||||||
for s in sources
|
for s in sources
|
||||||
]
|
]
|
||||||
|
|||||||
168
docs/FILTER_GUIDELINES.md
Normal file
168
docs/FILTER_GUIDELINES.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Filter Implementation Guidelines
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
### 1. Dynamic Filter Options (Cascading Filters)
|
||||||
|
Filters should **ALWAYS** load their options from the backend based on **actual data in the current filtered result set**.
|
||||||
|
|
||||||
|
**Why?**
|
||||||
|
- Prevents showing filter values that would return 0 results
|
||||||
|
- Ensures filters only show what exists in the current data context
|
||||||
|
- Provides better UX - users see only relevant choices
|
||||||
|
|
||||||
|
### 2. Backend filter_options Endpoint Pattern
|
||||||
|
Each module's ViewSet should have a `filter_options` action that:
|
||||||
|
- Returns distinct values from actual data
|
||||||
|
- Prepends "All X" option for each filter type
|
||||||
|
- Supports cascading (applying other filters when calculating each option list)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@action(detail=False, methods=['get'], url_path='filter_options')
|
||||||
|
def filter_options(self, request):
|
||||||
|
"""
|
||||||
|
Returns distinct filter values from ACTUAL DATA in the queryset.
|
||||||
|
Supports cascading - each filter's options exclude itself but apply other filters.
|
||||||
|
Backend ALWAYS prepends the "All X" option.
|
||||||
|
"""
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
# Get current filter values
|
||||||
|
status_filter = request.query_params.get('status', '')
|
||||||
|
type_filter = request.query_params.get('content_type', '')
|
||||||
|
|
||||||
|
# For each filter option, apply OTHER filters (not self)
|
||||||
|
type_qs = queryset
|
||||||
|
if status_filter:
|
||||||
|
type_qs = type_qs.filter(status=status_filter)
|
||||||
|
|
||||||
|
content_types = list(set(type_qs.values_list('content_type', flat=True)))
|
||||||
|
content_types = sorted([t for t in content_types if t])
|
||||||
|
|
||||||
|
# BACKEND prepends "All" option
|
||||||
|
content_type_options = [
|
||||||
|
{'value': '', 'label': 'All Types'},
|
||||||
|
] + [
|
||||||
|
{'value': t, 'label': TYPE_LABELS.get(t, t.title())}
|
||||||
|
for t in content_types
|
||||||
|
]
|
||||||
|
|
||||||
|
return success_response(data={'content_types': content_type_options})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Frontend Filter Config Pattern
|
||||||
|
Frontend configs should use dynamic options **DIRECTLY** without adding "All" prefix:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// CORRECT - Backend already includes "All Types" option
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'content_type',
|
||||||
|
label: 'Type',
|
||||||
|
type: 'select',
|
||||||
|
options: handlers.contentTypeOptions, // Use directly from backend
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// WRONG - Don't add "All" in frontend (causes duplicates)
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'All Types' }, // DON'T DO THIS
|
||||||
|
...(handlers.contentTypeOptions || []),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Page-Specific Base Constraints
|
||||||
|
Pages with fixed default filters should pass constraints when loading filter_options:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// APPROVED PAGE - only show filters for approved/published content
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters) => {
|
||||||
|
const options = await fetchWriterContentFilterOptions(activeSite.id, {
|
||||||
|
...currentFilters,
|
||||||
|
status__in: 'approved,published', // Base constraint
|
||||||
|
});
|
||||||
|
setStatusOptions(options.statuses || []);
|
||||||
|
}, [activeSite]);
|
||||||
|
|
||||||
|
// DRAFTS PAGE - only show filters for draft content
|
||||||
|
const loadFilterOptions = useCallback(async (currentFilters) => {
|
||||||
|
const options = await fetchWriterContentFilterOptions(activeSite.id, {
|
||||||
|
...currentFilters,
|
||||||
|
status: 'draft', // Base constraint
|
||||||
|
});
|
||||||
|
setContentTypeOptions(options.content_types || []);
|
||||||
|
}, [activeSite]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Clear Filters Button
|
||||||
|
Every page with filters should provide `onFilterReset` handler:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<TablePageTemplate
|
||||||
|
filters={pageConfig.filters}
|
||||||
|
filterValues={{
|
||||||
|
search: searchTerm,
|
||||||
|
content_type: contentTypeFilter,
|
||||||
|
}}
|
||||||
|
onFilterChange={(key, value) => { /* ... */ }}
|
||||||
|
onFilterReset={() => {
|
||||||
|
setSearchTerm('');
|
||||||
|
setContentTypeFilter('');
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Filter Dependencies
|
||||||
|
Ensure filter state variables are in useCallback dependency arrays:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// CORRECT - all filter states in dependencies
|
||||||
|
const loadContent = useCallback(async () => {
|
||||||
|
const filters = {
|
||||||
|
status: statusFilter,
|
||||||
|
content_type: contentTypeFilter,
|
||||||
|
};
|
||||||
|
// ...
|
||||||
|
}, [statusFilter, contentTypeFilter, /* other deps */]);
|
||||||
|
|
||||||
|
// WRONG - missing filter in dependencies (filter won't trigger reload)
|
||||||
|
}, [statusFilter]); // Missing contentTypeFilter!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. What NOT to Do
|
||||||
|
|
||||||
|
❌ **Don't add "All X" prefix in frontend config (backend does it):**
|
||||||
|
```typescript
|
||||||
|
// BAD - causes duplicate "All Types" entries
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'All Types' },
|
||||||
|
...(handlers.contentTypeOptions || []),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Don't forget filter state in useCallback dependencies:**
|
||||||
|
```typescript
|
||||||
|
// BAD - contentStatusFilter changes won't reload data
|
||||||
|
}, [statusFilter, sortBy, searchTerm]); // Missing contentStatusFilter
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Don't forget onFilterReset handler:**
|
||||||
|
```typescript
|
||||||
|
// BAD - no Clear button will show
|
||||||
|
<TablePageTemplate
|
||||||
|
onFilterChange={...}
|
||||||
|
// Missing: onFilterReset
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
For each page with filters:
|
||||||
|
|
||||||
|
- [ ] Backend `filter_options` prepends "All X" option for each filter type
|
||||||
|
- [ ] Backend applies cascading logic (each filter excludes itself)
|
||||||
|
- [ ] Frontend config uses `handlers.xxxOptions` directly (no "All" prefix)
|
||||||
|
- [ ] Page passes base constraints (e.g., status='draft') to filter_options
|
||||||
|
- [ ] Page provides `onFilterReset` handler to TablePageTemplate
|
||||||
|
- [ ] All filter states are in useCallback/useEffect dependencies
|
||||||
|
- [ ] Filter values in filterValues object match filter keys in config
|
||||||
@@ -371,44 +371,25 @@ export function createApprovedPageConfig(params: {
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.statusOptions || [
|
options: params.statusOptions,
|
||||||
{ value: '', label: 'All' },
|
|
||||||
{ value: 'draft', label: 'Draft' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'approved', label: 'Approved' },
|
|
||||||
{ value: 'published', label: 'Published' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'site_status',
|
key: 'site_status',
|
||||||
label: 'Site Status',
|
label: 'Site Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.siteStatusOptions || [
|
options: params.siteStatusOptions,
|
||||||
{ 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',
|
key: 'content_type',
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.contentTypeOptions || [
|
options: params.contentTypeOptions,
|
||||||
{ value: '', label: 'All Types' },
|
|
||||||
...CONTENT_TYPE_OPTIONS,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_structure',
|
key: 'content_structure',
|
||||||
label: 'Structure',
|
label: 'Structure',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.contentStructureOptions || [
|
options: params.contentStructureOptions,
|
||||||
{ value: '', label: 'All Structures' },
|
|
||||||
...ALL_CONTENT_STRUCTURES,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -282,38 +282,13 @@ export const createClustersPageConfig = (
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.statusOptions,
|
||||||
{ value: '', label: 'All Status' },
|
|
||||||
// Use dynamic options if loaded (even if empty array)
|
|
||||||
// Only fall back to defaults if statusOptions is undefined (not loaded yet)
|
|
||||||
...(handlers.statusOptions !== undefined
|
|
||||||
? handlers.statusOptions
|
|
||||||
: [
|
|
||||||
{ value: 'new', label: 'New' },
|
|
||||||
{ value: 'mapped', label: 'Mapped' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'difficulty',
|
key: 'difficulty',
|
||||||
label: 'Difficulty',
|
label: 'Difficulty',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.difficultyOptions,
|
||||||
{ value: '', label: 'All Difficulty' },
|
|
||||||
// Use dynamic options if loaded (even if empty array)
|
|
||||||
// Only fall back to defaults if difficultyOptions is undefined (not loaded yet)
|
|
||||||
...(handlers.difficultyOptions !== undefined
|
|
||||||
? handlers.difficultyOptions
|
|
||||||
: [
|
|
||||||
{ value: '1', label: '1 - Very Easy' },
|
|
||||||
{ value: '2', label: '2 - Easy' },
|
|
||||||
{ value: '3', label: '3 - Medium' },
|
|
||||||
{ value: '4', label: '4 - Hard' },
|
|
||||||
{ value: '5', label: '5 - Very Hard' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'volume',
|
key: 'volume',
|
||||||
|
|||||||
@@ -387,55 +387,19 @@ export const createContentPageConfig = (
|
|||||||
key: 'content_type',
|
key: 'content_type',
|
||||||
label: 'Content Type',
|
label: 'Content Type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.contentTypeOptions,
|
||||||
{ value: '', label: 'All Types' },
|
|
||||||
...(handlers.contentTypeOptions !== undefined
|
|
||||||
? handlers.contentTypeOptions
|
|
||||||
: CONTENT_TYPE_OPTIONS
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_structure',
|
key: 'content_structure',
|
||||||
label: 'Content Structure',
|
label: 'Content Structure',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.contentStructureOptions,
|
||||||
{ value: '', label: 'All Structures' },
|
|
||||||
...(handlers.contentStructureOptions !== undefined
|
|
||||||
? handlers.contentStructureOptions
|
|
||||||
: [
|
|
||||||
{ value: 'article', label: 'Article' },
|
|
||||||
{ value: 'guide', label: 'Guide' },
|
|
||||||
{ value: 'comparison', label: 'Comparison' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'listicle', label: 'Listicle' },
|
|
||||||
{ value: 'landing_page', label: 'Landing Page' },
|
|
||||||
{ value: 'business_page', label: 'Business Page' },
|
|
||||||
{ value: 'service_page', label: 'Service Page' },
|
|
||||||
{ value: 'general', label: 'General' },
|
|
||||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
|
||||||
{ value: 'product_page', label: 'Product Page' },
|
|
||||||
{ value: 'category_archive', label: 'Category Archive' },
|
|
||||||
{ value: 'tag_archive', label: 'Tag Archive' },
|
|
||||||
{ value: 'attribute_archive', label: 'Attribute Archive' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'source',
|
key: 'source',
|
||||||
label: 'Source',
|
label: 'Source',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.sourceOptions,
|
||||||
{ value: '', label: 'All Sources' },
|
|
||||||
...(handlers.sourceOptions !== undefined
|
|
||||||
? handlers.sourceOptions
|
|
||||||
: [
|
|
||||||
{ value: 'igny8', label: 'IGNY8' },
|
|
||||||
{ value: 'wordpress', label: 'WordPress' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headerMetrics: [
|
headerMetrics: [
|
||||||
|
|||||||
@@ -249,82 +249,26 @@ export const createIdeasPageConfig = (
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.statusOptions,
|
||||||
{ value: '', label: 'All Status' },
|
|
||||||
// Use dynamic options if loaded (even if empty array)
|
|
||||||
// Only fall back to defaults if statusOptions is undefined (not loaded yet)
|
|
||||||
...(handlers.statusOptions !== undefined
|
|
||||||
? handlers.statusOptions
|
|
||||||
: [
|
|
||||||
{ value: 'new', label: 'New' },
|
|
||||||
{ value: 'queued', label: 'Queued' },
|
|
||||||
{ value: 'completed', label: 'Completed' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_structure',
|
key: 'content_structure',
|
||||||
label: 'Structure',
|
label: 'Structure',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.contentStructureOptions,
|
||||||
{ value: '', label: 'All Structures' },
|
|
||||||
// Use dynamic options if loaded (even if empty array)
|
|
||||||
// Only fall back to defaults if contentStructureOptions is undefined (not loaded yet)
|
|
||||||
...(handlers.contentStructureOptions !== undefined
|
|
||||||
? handlers.contentStructureOptions
|
|
||||||
: [
|
|
||||||
{ value: 'article', label: 'Article' },
|
|
||||||
{ value: 'guide', label: 'Guide' },
|
|
||||||
{ value: 'comparison', label: 'Comparison' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'listicle', label: 'Listicle' },
|
|
||||||
{ value: 'landing_page', label: 'Landing Page' },
|
|
||||||
{ value: 'business_page', label: 'Business Page' },
|
|
||||||
{ value: 'service_page', label: 'Service Page' },
|
|
||||||
{ value: 'general', label: 'General' },
|
|
||||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
|
||||||
{ value: 'product_page', label: 'Product Page' },
|
|
||||||
{ value: 'category_archive', label: 'Category Archive' },
|
|
||||||
{ value: 'tag_archive', label: 'Tag Archive' },
|
|
||||||
{ value: 'attribute_archive', label: 'Attribute Archive' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_type',
|
key: 'content_type',
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.contentTypeOptions,
|
||||||
{ value: '', label: 'All Types' },
|
|
||||||
// Use dynamic options if loaded (even if empty array)
|
|
||||||
// Only fall back to defaults if contentTypeOptions is undefined (not loaded yet)
|
|
||||||
...(handlers.contentTypeOptions !== undefined
|
|
||||||
? handlers.contentTypeOptions
|
|
||||||
: [
|
|
||||||
{ value: 'post', label: 'Post' },
|
|
||||||
{ value: 'page', label: 'Page' },
|
|
||||||
{ value: 'product', label: 'Product' },
|
|
||||||
{ value: 'taxonomy', label: 'Taxonomy' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'keyword_cluster_id',
|
key: 'keyword_cluster_id',
|
||||||
label: 'Cluster',
|
label: 'Cluster',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
dynamicOptions: 'clusters',
|
dynamicOptions: 'clusters',
|
||||||
options: [
|
options: handlers.clusterOptions,
|
||||||
{ value: '', label: 'All Clusters' },
|
|
||||||
// Use dynamic cluster options if loaded (even if empty array)
|
|
||||||
// Only fall back to full clusters list if clusterOptions is undefined (not loaded yet)
|
|
||||||
...(handlers.clusterOptions !== undefined
|
|
||||||
? handlers.clusterOptions
|
|
||||||
: handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name }))
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
formFields: (clusters: Array<{ id: number; name: string }>) => [
|
formFields: (clusters: Array<{ id: number; name: string }>) => [
|
||||||
|
|||||||
@@ -197,20 +197,14 @@ export const createImagesPageConfig = (
|
|||||||
key: 'content_status',
|
key: 'content_status',
|
||||||
label: 'Content Status',
|
label: 'Content Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: handlers.contentStatusOptions || [
|
options: handlers.contentStatusOptions,
|
||||||
{ value: '', label: 'All' },
|
|
||||||
{ value: 'draft', label: 'Draft' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'approved', label: 'Approved' },
|
|
||||||
{ value: 'published', label: 'Published' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'Image Status',
|
label: 'Image Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Image Status' },
|
||||||
{ value: 'complete', label: 'Complete' },
|
{ value: 'complete', label: 'Complete' },
|
||||||
{ value: 'partial', label: 'Partial' },
|
{ value: 'partial', label: 'Partial' },
|
||||||
{ value: 'pending', label: 'Pending' },
|
{ value: 'pending', label: 'Pending' },
|
||||||
|
|||||||
@@ -245,32 +245,19 @@ export function createReviewPageConfig(params: {
|
|||||||
key: 'site_status',
|
key: 'site_status',
|
||||||
label: 'Site Status',
|
label: 'Site Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.siteStatusOptions || [
|
options: params.siteStatusOptions,
|
||||||
{ 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',
|
key: 'content_type',
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.contentTypeOptions || [
|
options: params.contentTypeOptions,
|
||||||
{ value: '', label: 'All Types' },
|
|
||||||
...CONTENT_TYPE_OPTIONS,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_structure',
|
key: 'content_structure',
|
||||||
label: 'Structure',
|
label: 'Structure',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: params.contentStructureOptions || [
|
options: params.contentStructureOptions,
|
||||||
{ value: '', label: 'All Structures' },
|
|
||||||
...ALL_CONTENT_STRUCTURES,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headerMetrics: [
|
headerMetrics: [
|
||||||
|
|||||||
@@ -317,69 +317,25 @@ export const createTasksPageConfig = (
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.statusOptions,
|
||||||
{ value: '', label: 'All Status' },
|
|
||||||
...(handlers.statusOptions !== undefined
|
|
||||||
? handlers.statusOptions
|
|
||||||
: [
|
|
||||||
{ value: 'queued', label: 'Queued' },
|
|
||||||
{ value: 'completed', label: 'Completed' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_type',
|
key: 'content_type',
|
||||||
label: 'Content Type',
|
label: 'Content Type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.contentTypeOptions,
|
||||||
{ value: '', label: 'All Types' },
|
|
||||||
...(handlers.contentTypeOptions !== undefined
|
|
||||||
? handlers.contentTypeOptions
|
|
||||||
: CONTENT_TYPE_OPTIONS
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_structure',
|
key: 'content_structure',
|
||||||
label: 'Content Structure',
|
label: 'Content Structure',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: handlers.contentStructureOptions,
|
||||||
{ value: '', label: 'All Structures' },
|
|
||||||
...(handlers.contentStructureOptions !== undefined
|
|
||||||
? handlers.contentStructureOptions
|
|
||||||
: [
|
|
||||||
{ value: 'article', label: 'Article' },
|
|
||||||
{ value: 'guide', label: 'Guide' },
|
|
||||||
{ value: 'comparison', label: 'Comparison' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'listicle', label: 'Listicle' },
|
|
||||||
{ value: 'landing_page', label: 'Landing Page' },
|
|
||||||
{ value: 'business_page', label: 'Business Page' },
|
|
||||||
{ value: 'service_page', label: 'Service Page' },
|
|
||||||
{ value: 'general', label: 'General' },
|
|
||||||
{ value: 'cluster_hub', label: 'Cluster Hub' },
|
|
||||||
{ value: 'product_page', label: 'Product Page' },
|
|
||||||
{ value: 'category_archive', label: 'Category Archive' },
|
|
||||||
{ value: 'tag_archive', label: 'Tag Archive' },
|
|
||||||
{ value: 'attribute_archive', label: 'Attribute Archive' },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cluster_id',
|
key: 'cluster_id',
|
||||||
label: 'Cluster',
|
label: 'Cluster',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: (() => {
|
options: handlers.clusterOptions,
|
||||||
return [
|
|
||||||
{ value: '', label: 'All Clusters' },
|
|
||||||
...(handlers.clusterOptions !== undefined
|
|
||||||
? handlers.clusterOptions
|
|
||||||
: handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name }))
|
|
||||||
),
|
|
||||||
];
|
|
||||||
})(),
|
|
||||||
dynamicOptions: 'clusters',
|
dynamicOptions: 'clusters',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export default function Approved() {
|
|||||||
// Load dynamic filter options based on current site's data and applied filters
|
// 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
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
// given the other currently applied filters
|
// given the other currently applied filters
|
||||||
|
// APPROVED PAGE: Always constrain to approved+published statuses
|
||||||
const loadFilterOptions = useCallback(async (currentFilters?: {
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
status?: string;
|
status?: string;
|
||||||
site_status?: string;
|
site_status?: string;
|
||||||
@@ -109,7 +110,11 @@ export default function Approved() {
|
|||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
|
// Always pass status__in to constrain filter options to approved/published content only
|
||||||
|
const options = await fetchWriterContentFilterOptions(activeSite.id, {
|
||||||
|
...currentFilters,
|
||||||
|
status__in: 'approved,published', // Base constraint for this page
|
||||||
|
});
|
||||||
setStatusOptions(options.statuses || []);
|
setStatusOptions(options.statuses || []);
|
||||||
setSiteStatusOptions(options.site_statuses || []);
|
setSiteStatusOptions(options.site_statuses || []);
|
||||||
setContentTypeOptions(options.content_types || []);
|
setContentTypeOptions(options.content_types || []);
|
||||||
@@ -119,7 +124,7 @@ export default function Approved() {
|
|||||||
}
|
}
|
||||||
}, [activeSite]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when site changes (initial load with no filters)
|
// Load filter options when site changes (initial load with approved/published constraint)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [activeSite]);
|
}, [activeSite]);
|
||||||
@@ -175,6 +180,7 @@ export default function Approved() {
|
|||||||
...(searchTerm && { search: searchTerm }),
|
...(searchTerm && { search: searchTerm }),
|
||||||
// Default to approved+published if no status filter selected
|
// Default to approved+published if no status filter selected
|
||||||
...(statusFilter ? { status: statusFilter } : { status__in: 'approved,published' }),
|
...(statusFilter ? { status: statusFilter } : { status__in: 'approved,published' }),
|
||||||
|
...(siteStatusFilter && { site_status: siteStatusFilter }),
|
||||||
...(contentTypeFilter && { content_type: contentTypeFilter }),
|
...(contentTypeFilter && { content_type: contentTypeFilter }),
|
||||||
...(contentStructureFilter && { content_structure: contentStructureFilter }),
|
...(contentStructureFilter && { content_structure: contentStructureFilter }),
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
@@ -184,13 +190,7 @@ export default function Approved() {
|
|||||||
|
|
||||||
const data: ContentListResponse = await fetchContent(filters);
|
const data: ContentListResponse = await fetchContent(filters);
|
||||||
|
|
||||||
// Client-side filter for site_status if needed (backend may not support this filter yet)
|
setContent(data.results || []);
|
||||||
let filteredResults = data.results || [];
|
|
||||||
if (siteStatusFilter) {
|
|
||||||
filteredResults = filteredResults.filter(c => c.site_status === siteStatusFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent(filteredResults);
|
|
||||||
setTotalCount(data.count || 0);
|
setTotalCount(data.count || 0);
|
||||||
setTotalPages(Math.ceil((data.count || 0) / pageSize));
|
setTotalPages(Math.ceil((data.count || 0) / pageSize));
|
||||||
|
|
||||||
@@ -771,6 +771,14 @@ export default function Approved() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onFilterReset={() => {
|
||||||
|
setSearchTerm('');
|
||||||
|
setStatusFilter('');
|
||||||
|
setSiteStatusFilter('');
|
||||||
|
setContentTypeFilter('');
|
||||||
|
setContentStructureFilter('');
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function Content() {
|
|||||||
// Load dynamic filter options based on current site's data and applied filters
|
// 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
|
// This implements cascading filters - each filter's options reflect what's available
|
||||||
// given the other currently applied filters
|
// given the other currently applied filters
|
||||||
|
// DRAFTS PAGE: Always constrain to draft status
|
||||||
const loadFilterOptions = useCallback(async (currentFilters?: {
|
const loadFilterOptions = useCallback(async (currentFilters?: {
|
||||||
status?: string;
|
status?: string;
|
||||||
site_status?: string;
|
site_status?: string;
|
||||||
@@ -88,7 +89,11 @@ export default function Content() {
|
|||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
|
// Always pass status='draft' to constrain filter options to draft content only
|
||||||
|
const options = await fetchWriterContentFilterOptions(activeSite.id, {
|
||||||
|
...currentFilters,
|
||||||
|
status: 'draft', // Base constraint for this page
|
||||||
|
});
|
||||||
setSourceOptions(options.sources || []);
|
setSourceOptions(options.sources || []);
|
||||||
setContentTypeOptions(options.content_types || []);
|
setContentTypeOptions(options.content_types || []);
|
||||||
setContentStructureOptions(options.content_structures || []);
|
setContentStructureOptions(options.content_structures || []);
|
||||||
@@ -97,7 +102,7 @@ export default function Content() {
|
|||||||
}
|
}
|
||||||
}, [activeSite]);
|
}, [activeSite]);
|
||||||
|
|
||||||
// Load filter options when site changes (initial load with no filters)
|
// Load filter options when site changes (initial load with draft constraint)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [activeSite]);
|
}, [activeSite]);
|
||||||
@@ -105,13 +110,12 @@ export default function Content() {
|
|||||||
// Reload filter options when any filter changes (cascading filters)
|
// Reload filter options when any filter changes (cascading filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFilterOptions({
|
loadFilterOptions({
|
||||||
status: statusFilter || undefined,
|
|
||||||
content_type: contentTypeFilter || undefined,
|
content_type: contentTypeFilter || undefined,
|
||||||
content_structure: contentStructureFilter || undefined,
|
content_structure: contentStructureFilter || undefined,
|
||||||
source: sourceFilter || undefined,
|
source: sourceFilter || undefined,
|
||||||
search: searchTerm || undefined,
|
search: searchTerm || undefined,
|
||||||
});
|
});
|
||||||
}, [statusFilter, contentTypeFilter, contentStructureFilter, sourceFilter, searchTerm, loadFilterOptions]);
|
}, [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)
|
||||||
@@ -422,6 +426,13 @@ export default function Content() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onFilterReset={() => {
|
||||||
|
setSearchTerm('');
|
||||||
|
setSourceFilter('');
|
||||||
|
setContentTypeFilter('');
|
||||||
|
setContentStructureFilter('');
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export default function Images() {
|
|||||||
setShowContent(true);
|
setShowContent(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, toast]);
|
}, [currentPage, statusFilter, contentStatusFilter, sortBy, sortDirection, searchTerm, toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadImages();
|
loadImages();
|
||||||
|
|||||||
@@ -893,6 +893,7 @@ export interface WriterContentFilterOptions {
|
|||||||
|
|
||||||
export interface ContentFilterOptionsRequest {
|
export interface ContentFilterOptionsRequest {
|
||||||
status?: string;
|
status?: string;
|
||||||
|
status__in?: string; // Comma-separated list for base constraint (e.g., 'approved,published')
|
||||||
site_status?: string;
|
site_status?: string;
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
content_structure?: string;
|
content_structure?: string;
|
||||||
@@ -907,6 +908,7 @@ export async function fetchWriterContentFilterOptions(
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (siteId) params.append('site_id', siteId.toString());
|
if (siteId) params.append('site_id', siteId.toString());
|
||||||
if (filters?.status) params.append('status', filters.status);
|
if (filters?.status) params.append('status', filters.status);
|
||||||
|
if (filters?.status__in) params.append('status__in', filters.status__in);
|
||||||
if (filters?.site_status) params.append('site_status', filters.site_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_type) params.append('content_type', filters.content_type);
|
||||||
if (filters?.content_structure) params.append('content_structure', filters.content_structure);
|
if (filters?.content_structure) params.append('content_structure', filters.content_structure);
|
||||||
@@ -2582,6 +2584,7 @@ export interface ContentFilters {
|
|||||||
search?: string;
|
search?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
status__in?: string; // Comma-separated list of statuses (e.g., 'approved,published')
|
status__in?: string; // Comma-separated list of statuses (e.g., 'approved,published')
|
||||||
|
site_status?: string; // Site publishing status (not_published, scheduled, published, failed)
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
content_structure?: string;
|
content_structure?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
@@ -2674,6 +2677,11 @@ export async function fetchContent(filters: ContentFilters = {}): Promise<Conten
|
|||||||
if (filters.search) params.append('search', filters.search);
|
if (filters.search) params.append('search', filters.search);
|
||||||
if (filters.status) params.append('status', filters.status);
|
if (filters.status) params.append('status', filters.status);
|
||||||
if (filters.status__in) params.append('status__in', filters.status__in);
|
if (filters.status__in) params.append('status__in', filters.status__in);
|
||||||
|
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.cluster_id) params.append('cluster_id', filters.cluster_id.toString());
|
||||||
if (filters.task_id) params.append('task_id', filters.task_id.toString());
|
if (filters.task_id) params.append('task_id', filters.task_id.toString());
|
||||||
if (filters.site_id) params.append('site_id', filters.site_id.toString());
|
if (filters.site_id) params.append('site_id', filters.site_id.toString());
|
||||||
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
|
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
|
||||||
|
|||||||
Reference in New Issue
Block a user