# 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 { /* ... */ }} 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 ``` ## 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