fixes but still nto fixed
This commit is contained in:
351
docs/30-FRONTEND/FILTERS-IMPLEMENTATION.md
Normal file
351
docs/30-FRONTEND/FILTERS-IMPLEMENTATION.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# IGNY8 Filters Implementation
|
||||
|
||||
**Last Updated:** January 15, 2026
|
||||
**Version:** 1.0.0
|
||||
|
||||
> This document describes the current filter implementation across all pages that use the `TablePageTemplate` component.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
Page Component (e.g., Keywords.tsx)
|
||||
└── createPageConfig() function
|
||||
├── Returns: columns, filters, formFields, headerMetrics
|
||||
└── Uses: handlers (state setters, filter values)
|
||||
└── TablePageTemplate.tsx
|
||||
├── Renders filters based on FilterConfig[]
|
||||
├── Manages filter visibility toggle
|
||||
└── Passes filterValues and onFilterChange to children
|
||||
```
|
||||
|
||||
### Filter Flow
|
||||
|
||||
1. **Page Component** defines filter state (`useState`)
|
||||
2. **Config Function** creates `FilterConfig[]` with options
|
||||
3. **TablePageTemplate** renders filter UI components
|
||||
4. **Filter Change** → `onFilterChange` callback → update state → re-fetch data
|
||||
|
||||
---
|
||||
|
||||
## Filter Types
|
||||
|
||||
| Type | Component | Description |
|
||||
|------|-----------|-------------|
|
||||
| `text` | `<Input>` | Text search input |
|
||||
| `select` | `<SelectDropdown>` | Single-select dropdown |
|
||||
| `custom` | `customRender()` | Custom component (e.g., volume range) |
|
||||
| `daterange` | (planned) | Date range picker |
|
||||
| `range` | (planned) | Numeric range |
|
||||
|
||||
---
|
||||
|
||||
## Backend FilterSet Classes
|
||||
|
||||
### Planner Module (`/backend/igny8_core/modules/planner/views.py`)
|
||||
|
||||
#### KeywordsFilter
|
||||
```python
|
||||
class KeywordsFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Keywords
|
||||
fields = ['status', 'cluster_id', 'seed_keyword__country', 'seed_keyword_id', 'created_at__gte', 'created_at__lte']
|
||||
```
|
||||
|
||||
#### ClustersFilter
|
||||
```python
|
||||
class ClustersFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Clusters
|
||||
fields = ['status', 'created_at__gte', 'created_at__lte']
|
||||
```
|
||||
|
||||
#### ContentIdeasFilter
|
||||
```python
|
||||
class ContentIdeasFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = ContentIdeas
|
||||
fields = ['status', 'keyword_cluster_id', 'content_type', 'content_structure', 'created_at__gte', 'created_at__lte']
|
||||
```
|
||||
|
||||
### Writer Module (`/backend/igny8_core/modules/writer/views.py`)
|
||||
|
||||
#### TasksFilter
|
||||
```python
|
||||
class TasksFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Tasks
|
||||
fields = ['status', 'cluster_id', 'content_type', 'content_structure', 'created_at__gte', 'created_at__lte']
|
||||
```
|
||||
|
||||
#### ImagesFilter
|
||||
```python
|
||||
class ImagesFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Images
|
||||
fields = ['task_id', 'content_id', 'image_type', 'status', 'created_at__gte', 'created_at__lte']
|
||||
```
|
||||
|
||||
#### ContentFilter
|
||||
```python
|
||||
class ContentFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Content
|
||||
fields = ['cluster_id', 'status', 'content_type', 'content_structure', 'source', 'created_at__gte', 'created_at__lte']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Page-by-Page Filter Implementation
|
||||
|
||||
### 1. Keywords Page (`/planner/keywords`)
|
||||
|
||||
**Config File:** `frontend/src/config/pages/keywords.config.tsx`
|
||||
**Page File:** `frontend/src/pages/Planner/Keywords.tsx`
|
||||
|
||||
| Filter Key | Type | Options | Backend Field |
|
||||
|------------|------|---------|---------------|
|
||||
| `search` | text | - | `search` (SearchFilter) |
|
||||
| `status` | select | `new`, `mapped` | `status` |
|
||||
| `country` | select | `US`, `CA`, `GB`, `AE`, `AU`, `IN`, `PK` | `seed_keyword__country` |
|
||||
| `difficulty` | select | `1-5` (mapped labels) | Custom in `get_queryset()` |
|
||||
| `cluster` | select | Dynamic (cluster list) | `cluster_id` |
|
||||
| `volume` | custom | Min/Max inputs | Custom `volume_min`, `volume_max` |
|
||||
|
||||
**Special Handling:**
|
||||
- Difficulty filter uses 1-5 scale that maps to raw score ranges (0-10, 11-30, 31-50, 51-70, 71-100)
|
||||
- Volume range uses custom dropdown with min/max inputs
|
||||
|
||||
---
|
||||
|
||||
### 2. Clusters Page (`/planner/clusters`)
|
||||
|
||||
**Config File:** `frontend/src/config/pages/clusters.config.tsx`
|
||||
**Page File:** `frontend/src/pages/Planner/Clusters.tsx`
|
||||
|
||||
| Filter Key | Type | Options | Backend Field |
|
||||
|------------|------|---------|---------------|
|
||||
| `search` | text | - | `search` (SearchFilter) |
|
||||
| `status` | select | `new`, `mapped` | `status` |
|
||||
| `difficulty` | select | `1-5` (mapped labels) | Custom filtering |
|
||||
| `volume` | custom | Min/Max inputs | Custom `volume_min`, `volume_max` |
|
||||
|
||||
---
|
||||
|
||||
### 3. Ideas Page (`/planner/ideas`)
|
||||
|
||||
**Config File:** `frontend/src/config/pages/ideas.config.tsx`
|
||||
**Page File:** `frontend/src/pages/Planner/Ideas.tsx`
|
||||
|
||||
| Filter Key | Type | Options | Backend Field |
|
||||
|------------|------|---------|---------------|
|
||||
| `search` | text | - | `search` (SearchFilter) |
|
||||
| `status` | select | `new`, `queued`, `completed` | `status` |
|
||||
| `content_structure` | select | 14 structure types | `content_structure` |
|
||||
| `content_type` | select | `post`, `page`, `product`, `taxonomy` | `content_type` |
|
||||
| `keyword_cluster_id` | select | Dynamic (cluster list) | `keyword_cluster_id` |
|
||||
|
||||
**Structure Options:**
|
||||
- Post: `article`, `guide`, `comparison`, `review`, `listicle`
|
||||
- Page: `landing_page`, `business_page`, `service_page`, `general`, `cluster_hub`
|
||||
- Product: `product_page`
|
||||
- Taxonomy: `category_archive`, `tag_archive`, `attribute_archive`
|
||||
|
||||
---
|
||||
|
||||
### 4. Content Page (`/writer/content`)
|
||||
|
||||
**Config File:** `frontend/src/config/pages/content.config.tsx`
|
||||
**Page File:** `frontend/src/pages/Writer/Content.tsx`
|
||||
|
||||
| Filter Key | Type | Options | Backend Field |
|
||||
|------------|------|---------|---------------|
|
||||
| `search` | text | - | `search` (SearchFilter) |
|
||||
| `status` | select | `draft`, `published` | `status` |
|
||||
| `content_type` | select | `post`, `page`, `product`, `taxonomy` | `content_type` |
|
||||
| `content_structure` | select | 14 structure types | `content_structure` |
|
||||
| `source` | select | `igny8`, `wordpress` | `source` |
|
||||
|
||||
---
|
||||
|
||||
### 5. Review Page (`/writer/review`)
|
||||
|
||||
**Config File:** `frontend/src/config/pages/review.config.tsx`
|
||||
**Page File:** `frontend/src/pages/Writer/Review.tsx`
|
||||
|
||||
| Filter Key | Type | Options | Backend Field |
|
||||
|------------|------|---------|---------------|
|
||||
| `search` | text | - | `search` (SearchFilter) |
|
||||
| `status` | select | `draft`, `review`, `approved`, `published` | `status` |
|
||||
| `site_status` | select | `not_published`, `scheduled`, `publishing`, `published`, `failed` | `site_status` |
|
||||
| `content_type` | select | From `CONTENT_TYPE_OPTIONS` | `content_type` |
|
||||
| `content_structure` | select | From `ALL_CONTENT_STRUCTURES` | `content_structure` |
|
||||
|
||||
---
|
||||
|
||||
### 6. Approved Page (`/writer/approved`)
|
||||
|
||||
**Config File:** `frontend/src/config/pages/approved.config.tsx`
|
||||
**Page File:** `frontend/src/pages/Writer/Approved.tsx`
|
||||
|
||||
| Filter Key | Type | Options | Backend Field |
|
||||
|------------|------|---------|---------------|
|
||||
| `search` | text | - | `search` (SearchFilter) |
|
||||
| `status` | select | `draft`, `review`, `approved`, `published` | `status` |
|
||||
| `site_status` | select | `not_published`, `scheduled`, `publishing`, `published`, `failed` | `site_status` |
|
||||
| `content_type` | select | From `CONTENT_TYPE_OPTIONS` | `content_type` |
|
||||
| `content_structure` | select | From `ALL_CONTENT_STRUCTURES` | `content_structure` |
|
||||
|
||||
---
|
||||
|
||||
## Shared Constants
|
||||
|
||||
**File:** `frontend/src/config/structureMapping.ts`
|
||||
|
||||
```typescript
|
||||
export const CONTENT_TYPE_OPTIONS = [
|
||||
{ value: 'post', label: 'Post' },
|
||||
{ value: 'page', label: 'Page' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
{ value: 'taxonomy', label: 'Taxonomy' },
|
||||
];
|
||||
|
||||
export const ALL_CONTENT_STRUCTURES = [
|
||||
{ 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' },
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Difficulty Mapping
|
||||
|
||||
**File:** `frontend/src/utils/difficulty.ts`
|
||||
|
||||
The difficulty filter uses a 1-5 scale with human-readable labels:
|
||||
|
||||
| Value | Label | Raw Score Range |
|
||||
|-------|-------|-----------------|
|
||||
| 1 | Very Easy | 0-10 |
|
||||
| 2 | Easy | 11-30 |
|
||||
| 3 | Medium | 31-50 |
|
||||
| 4 | Hard | 51-70 |
|
||||
| 5 | Very Hard | 71-100 |
|
||||
|
||||
**Important:** The database stores raw SEO difficulty scores (0-100), but the UI displays and filters using the 1-5 scale. The mapping must be consistent between frontend display and backend filtering.
|
||||
|
||||
---
|
||||
|
||||
## Filter State Management Pattern
|
||||
|
||||
Each page follows this pattern:
|
||||
|
||||
```tsx
|
||||
// 1. Define filter state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [difficultyFilter, setDifficultyFilter] = useState('');
|
||||
|
||||
// 2. Pass to config function
|
||||
const config = createPageConfig({
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
// ...handlers
|
||||
});
|
||||
|
||||
// 3. Config returns filters array
|
||||
filters: [
|
||||
{ key: 'search', type: 'text', placeholder: '...' },
|
||||
{ key: 'status', type: 'select', options: [...] },
|
||||
]
|
||||
|
||||
// 4. TablePageTemplate renders filters
|
||||
// 5. onFilterChange triggers state update
|
||||
// 6. useEffect with dependencies re-fetches data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Query Parameters
|
||||
|
||||
When filters are applied, the frontend constructs API calls like:
|
||||
|
||||
```
|
||||
GET /api/v1/planner/keywords/?status=new&cluster_id=123&search=term&page=1&page_size=50
|
||||
GET /api/v1/planner/clusters/?status=mapped&difficulty=3&volume_min=100
|
||||
GET /api/v1/planner/ideas/?content_structure=guide&content_type=post
|
||||
GET /api/v1/writer/content/?status=draft&source=igny8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TablePageTemplate Filter Rendering
|
||||
|
||||
The template handles filter rendering in `renderFiltersRow()`:
|
||||
|
||||
```tsx
|
||||
{filters.map((filter) => {
|
||||
if (filter.type === 'text') {
|
||||
return <Input ... />;
|
||||
}
|
||||
if (filter.type === 'select') {
|
||||
return <SelectDropdown ... />;
|
||||
}
|
||||
if (filter.type === 'custom' && filter.customRender) {
|
||||
return filter.customRender();
|
||||
}
|
||||
})}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Limitations
|
||||
|
||||
1. **Static Options**: Filter options are hardcoded in config files, not dynamically loaded from backend
|
||||
2. **No Cascading**: Changing one filter doesn't update available options in other filters
|
||||
3. **Difficulty Mapping**: Backend filters by exact match, not by mapped ranges
|
||||
4. **No Filter Persistence**: Filters reset on page navigation
|
||||
|
||||
---
|
||||
|
||||
## Planned Improvements
|
||||
|
||||
1. **Dynamic Filter Options API**: Backend endpoint to return available options based on current data
|
||||
2. **Cascading Filters**: When status is selected, only show clusters/types that exist with that status
|
||||
3. **URL State**: Persist filter state in URL query parameters
|
||||
4. **Filter Presets**: Save commonly used filter combinations
|
||||
|
||||
---
|
||||
|
||||
## File References
|
||||
|
||||
| Component | Path |
|
||||
|-----------|------|
|
||||
| TablePageTemplate | `frontend/src/templates/TablePageTemplate.tsx` |
|
||||
| Keywords Config | `frontend/src/config/pages/keywords.config.tsx` |
|
||||
| Clusters Config | `frontend/src/config/pages/clusters.config.tsx` |
|
||||
| Ideas Config | `frontend/src/config/pages/ideas.config.tsx` |
|
||||
| Content Config | `frontend/src/config/pages/content.config.tsx` |
|
||||
| Review Config | `frontend/src/config/pages/review.config.tsx` |
|
||||
| Approved Config | `frontend/src/config/pages/approved.config.tsx` |
|
||||
| Structure Mapping | `frontend/src/config/structureMapping.ts` |
|
||||
| Difficulty Utils | `frontend/src/utils/difficulty.ts` |
|
||||
| Planner Views | `backend/igny8_core/modules/planner/views.py` |
|
||||
| Writer Views | `backend/igny8_core/modules/writer/views.py` |
|
||||
Reference in New Issue
Block a user