filter fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-19 10:49:01 +00:00
parent 8c8f2df5dd
commit cbb32b1c9d
14 changed files with 287 additions and 239 deletions

168
docs/FILTER_GUIDELINES.md Normal file
View 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