filter fixes
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user