From 5da20928733573801052f5514638a901e58820b5 Mon Sep 17 00:00:00 2001 From: Desktop Date: Wed, 12 Nov 2025 23:29:31 +0500 Subject: [PATCH] New Columns and columns visibility --- .../src/components/common/ColumnSelector.tsx | 131 ++++++++++++++++++ frontend/src/config/pages/clusters.config.tsx | 46 +++++- frontend/src/config/pages/content.config.tsx | 48 ++++++- frontend/src/config/pages/ideas.config.tsx | 13 +- frontend/src/config/pages/images.config.tsx | 7 +- frontend/src/config/pages/keywords.config.tsx | 50 ++++++- frontend/src/config/pages/tasks.config.tsx | 81 ++++++++++- frontend/src/pages/Planner/Clusters.tsx | 2 +- frontend/src/pages/Planner/Keywords.tsx | 2 +- frontend/src/pages/Writer/Dashboard.tsx | 2 +- frontend/src/pages/Writer/Images.tsx | 2 +- frontend/src/templates/TablePageTemplate.tsx | 98 ++++++++++++- 12 files changed, 460 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/common/ColumnSelector.tsx diff --git a/frontend/src/components/common/ColumnSelector.tsx b/frontend/src/components/common/ColumnSelector.tsx new file mode 100644 index 00000000..a2760d38 --- /dev/null +++ b/frontend/src/components/common/ColumnSelector.tsx @@ -0,0 +1,131 @@ +/** + * ColumnSelector Component + * Dropdown with checkboxes to show/hide table columns + */ +import React, { useState, useRef, useEffect } from 'react'; +import { ChevronDownIcon } from '../../icons'; +import Checkbox from '../form/input/Checkbox'; + +interface ColumnSelectorProps { + columns: Array<{ key: string; label: string; defaultVisible?: boolean }>; + visibleColumns: Set; + onToggleColumn: (columnKey: string) => void; + className?: string; +} + +export default function ColumnSelector({ + columns, + visibleColumns, + onToggleColumn, + className = '', +}: ColumnSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + } + }, [isOpen]); + + const visibleCount = visibleColumns.size; + const totalCount = columns.length; + + return ( +
+ + + {isOpen && ( +
+
+
+ Show Columns +
+
+ {columns.map((column) => { + const isVisible = visibleColumns.has(column.key); + return ( + + ); + })} +
+
+ +
+
+
+ )} +
+ ); +} + diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 12b28504..e649bd85 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -27,6 +27,7 @@ export interface ColumnConfig { align?: 'left' | 'center' | 'right'; width?: string; render?: (value: any, row: any) => React.ReactNode; + defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FormFieldConfig { @@ -116,21 +117,24 @@ export const createClustersPageConfig = ( { key: 'keywords_count', label: 'Keywords', - sortable: false, + sortable: true, + sortField: 'keywords_count', width: '120px', render: (value: number) => value.toLocaleString(), }, { key: 'ideas_count', label: 'Ideas', - sortable: false, + sortable: true, + sortField: 'ideas_count', width: '120px', render: (value: number) => value.toLocaleString(), }, { key: 'volume', label: 'Volume', - sortable: false, + sortable: true, + sortField: 'volume', width: '120px', render: (value: number) => value.toLocaleString(), }, @@ -138,7 +142,8 @@ export const createClustersPageConfig = ( ...difficultyColumn, key: 'difficulty', label: 'Difficulty', - sortable: false, + sortable: true, + sortField: 'difficulty', align: 'center' as const, render: (value: number) => { const difficultyNum = getDifficultyNumber(value); @@ -179,7 +184,8 @@ export const createClustersPageConfig = ( { key: 'content_count', label: 'Content', - sortable: false, + sortable: true, + sortField: 'content_count', width: '120px', render: (value: number) => value.toLocaleString(), }, @@ -202,6 +208,36 @@ export const createClustersPageConfig = ( sortField: 'created_at', render: (value: string) => formatRelativeDate(value), }, + // Optional columns - hidden by default + { + key: 'description', + label: 'Description', + sortable: false, + defaultVisible: false, + width: '250px', + render: (value: string | null) => ( + + {value || '-'} + + ), + }, + { + key: 'mapped_pages', + label: 'Mapped Pages', + sortable: true, + sortField: 'mapped_pages', + defaultVisible: false, + width: '120px', + render: (value: number) => value.toLocaleString(), + }, + { + key: 'updated_at', + label: 'Updated', + sortable: true, + sortField: 'updated_at', + defaultVisible: false, + render: (value: string) => formatRelativeDate(value), + }, ], filters: [ { diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index 00801b05..539d3d54 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -27,6 +27,7 @@ export interface ColumnConfig { toggleable?: boolean; toggleContentKey?: string; toggleContentLabel?: string; + defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FilterConfig { @@ -124,7 +125,8 @@ export const createContentPageConfig = ( { key: 'primary_keyword', label: 'Primary Keyword', - sortable: false, + sortable: true, + sortField: 'primary_keyword', width: '150px', render: (value: string, row: Content) => ( row.primary_keyword ? ( @@ -262,6 +264,50 @@ export const createContentPageConfig = ( ); }, }, + // Optional columns - hidden by default + { + key: 'task_title', + label: 'Task Title', + sortable: true, + sortField: 'task_id', + defaultVisible: false, + width: '200px', + render: (_value: string, row: Content) => ( + + {row.task_title || '-'} + + ), + }, + { + key: 'post_url', + label: 'Post URL', + sortable: false, + defaultVisible: false, + width: '200px', + render: (value: string | null, row: Content) => { + const url = value || row.post_url || null; + return url ? ( + + {url} + + ) : ( + - + ); + }, + }, + { + key: 'updated_at', + label: 'Updated', + sortable: true, + sortField: 'updated_at', + defaultVisible: false, + render: (value: string) => formatRelativeDate(value), + }, ], filters: [ { diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index bf7bd53a..958d62bd 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -22,6 +22,7 @@ export interface ColumnConfig { align?: 'left' | 'center' | 'right'; width?: string; render?: (value: any, row: any) => React.ReactNode; + defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FormFieldConfig { @@ -150,7 +151,8 @@ export const createIdeasPageConfig = ( { key: 'keyword_cluster_name', label: 'Cluster', - sortable: false, + sortable: true, + sortField: 'keyword_cluster_id', width: '200px', render: (_value: string, row: ContentIdea) => row.keyword_cluster_name || '-', }, @@ -188,6 +190,15 @@ export const createIdeasPageConfig = ( sortField: 'created_at', render: (value: string) => formatRelativeDate(value), }, + // Optional columns - hidden by default + { + key: 'updated_at', + label: 'Updated', + sortable: true, + sortField: 'updated_at', + defaultVisible: false, + render: (value: string) => formatRelativeDate(value), + }, ], filters: [ { diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index b966f365..6b9663d9 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -18,6 +18,7 @@ export interface ColumnConfig { align?: 'left' | 'center' | 'right'; width?: string; render?: (value: any, row: any) => React.ReactNode; + defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FilterConfig { @@ -60,7 +61,8 @@ export const createImagesPageConfig = ( { key: 'content_title', label: 'Content Title', - sortable: false, + sortable: true, + sortField: 'content_title', width: '250px', render: (_value: string, row: ContentImagesGroup) => (
@@ -105,7 +107,8 @@ export const createImagesPageConfig = ( columns.push({ key: 'overall_status', label: 'Status', - sortable: false, + sortable: true, + sortField: 'overall_status', width: '180px', render: (value: string, row: ContentImagesGroup) => { const statusColors: Record = { diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index d8cc7515..2ed4a629 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -36,6 +36,7 @@ export interface ColumnConfig { align?: 'left' | 'center' | 'right'; width?: string; render?: (value: any, row: any) => React.ReactNode; + defaultVisible?: boolean; // Whether column is visible by default (default: true) } // BulkActionConfig and RowActionConfig are now in table-actions.config.tsx @@ -163,7 +164,8 @@ export const createKeywordsPageConfig = ( }, { ...clusterColumn, - sortable: false, + sortable: true, + sortField: 'cluster_id', render: (_value: string, row: Keyword) => row.cluster_name || '-', }, { @@ -264,6 +266,52 @@ export const createKeywordsPageConfig = ( sortField: 'created_at', render: (value: string) => formatRelativeDate(value), }, + // Optional columns - hidden by default + { + key: 'updated_at', + label: 'Updated', + sortable: true, + sortField: 'updated_at', + defaultVisible: false, + render: (value: string) => formatRelativeDate(value), + }, + { + key: 'volume_override', + label: 'Volume Override', + sortable: true, + sortField: 'volume_override', + defaultVisible: false, + render: (value: number | null) => value ? value.toLocaleString() : '-', + }, + { + key: 'difficulty_override', + label: 'Difficulty Override', + sortable: true, + sortField: 'difficulty_override', + defaultVisible: false, + align: 'center' as const, + render: (value: number | null) => { + if (value === null || value === undefined) return '-'; + const difficultyNum = getDifficultyNumber(value); + return typeof difficultyNum === 'number' ? ( + + {difficultyNum} + + ) : ( + difficultyNum + ); + }, + }, ], filters: [ { diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index c191b4f3..51f5bbda 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -26,6 +26,7 @@ export interface ColumnConfig { toggleable?: boolean; // If true, this column will have a toggle button for expanding content toggleContentKey?: string; // Key of the field containing content to display when toggled toggleContentLabel?: string; // Label for the expanded content (e.g., "Content Outline", "Generated Content") + defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FormFieldConfig { @@ -115,7 +116,8 @@ export const createTasksPageConfig = ( { key: 'cluster_name', label: 'Cluster', - sortable: false, + sortable: true, + sortField: 'cluster_id', width: '200px', render: (_value: string, row: Task) => row.cluster_name || '-', }, @@ -176,6 +178,83 @@ export const createTasksPageConfig = ( sortField: 'created_at', render: (value: string) => formatRelativeDate(value), }, + // Optional columns - hidden by default + { + key: 'idea_title', + label: 'Idea', + sortable: true, + sortField: 'idea_id', + defaultVisible: false, + width: '200px', + render: (_value: string, row: Task) => ( + + {row.idea_title || '-'} + + ), + }, + { + key: 'keywords', + label: 'Keywords', + sortable: false, + defaultVisible: false, + width: '200px', + render: (value: string | null) => ( + + {value || '-'} + + ), + }, + { + key: 'meta_title', + label: 'Meta Title', + sortable: false, + defaultVisible: false, + width: '200px', + render: (value: string | null) => ( + + {value || '-'} + + ), + }, + { + key: 'meta_description', + label: 'Meta Description', + sortable: false, + defaultVisible: false, + width: '250px', + render: (value: string | null) => ( + + {value || '-'} + + ), + }, + { + key: 'post_url', + label: 'Post URL', + sortable: false, + defaultVisible: false, + width: '200px', + render: (value: string | null) => value ? ( + + {value} + + ) : ( + - + ), + }, + { + key: 'updated_at', + label: 'Updated', + sortable: true, + sortField: 'updated_at', + defaultVisible: false, + render: (value: string) => formatRelativeDate(value), + }, ], filters: [ { diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index cc19e3dc..750c6c4a 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -387,7 +387,7 @@ export default function Clusters() { <> , color: 'green' }} + badge={{ icon: , color: 'purple' }} /> , color: 'blue' }} + badge={{ icon: , color: 'green' }} /> , color: 'green' }} + badge={{ icon: , color: 'blue' }} /> {/* Hero Section - Key Metric */} diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 3bb67306..6707f505 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -387,7 +387,7 @@ export default function Images() { <> , color: 'purple' }} + badge={{ icon: , color: 'orange' }} /> { + // Use pathname to create unique storage key per page + return `table-columns-${location.pathname}`; + }; + + // Initialize visible columns from localStorage or defaults + const initializeVisibleColumns = () => { + const storageKey = getStorageKey(); + try { + const saved = localStorage.getItem(storageKey); + if (saved) { + const savedSet = new Set(JSON.parse(saved)); + // Validate that all saved columns still exist + const validColumns = columns.filter(col => savedSet.has(col.key)); + if (validColumns.length > 0) { + // Add any new columns with defaultVisible !== false + const newColumns = columns + .filter(col => !savedSet.has(col.key) && col.defaultVisible !== false) + .map(col => col.key); + return new Set([...Array.from(validColumns.map(col => col.key)), ...newColumns]); + } + } + } catch (e) { + // Ignore parse errors + } + // Default: show all columns that have defaultVisible !== false + return new Set( + columns + .filter(col => col.defaultVisible !== false) + .map(col => col.key) + ); + }; + + const [visibleColumns, setVisibleColumns] = useState>(initializeVisibleColumns); + + // Update visible columns when columns prop changes (e.g., when switching pages) + useEffect(() => { + const newVisibleColumns = initializeVisibleColumns(); + setVisibleColumns(newVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname]); // Re-initialize when pathname changes + + // Save to localStorage whenever visibleColumns changes + useEffect(() => { + const storageKey = getStorageKey(); + try { + localStorage.setItem(storageKey, JSON.stringify(Array.from(visibleColumns))); + } catch (e) { + // Ignore storage errors + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visibleColumns, location.pathname]); + + // Filter columns based on visibility + const visibleColumnsList = useMemo(() => { + return columns.filter(col => visibleColumns.has(col.key)); + }, [columns, visibleColumns]); + + // Toggle column visibility + const handleToggleColumn = (columnKey: string) => { + setVisibleColumns(prev => { + const newSet = new Set(prev); + if (newSet.has(columnKey)) { + newSet.delete(columnKey); + } else { + newSet.add(columnKey); + } + return newSet; + }); + }; + // Sync selectedIds with selection prop // Use JSON.stringify to compare array contents, not just reference, to avoid unnecessary updates const selectedIdsKey = selection?.selectedIds ? JSON.stringify(selection.selectedIds) : ''; @@ -648,7 +722,17 @@ export default function TablePageTemplate({ )} {/* Action Buttons - Right aligned */} -
+
+ {/* Column Selector */} + ({ + key: col.key, + label: col.label, + defaultVisible: col.defaultVisible !== false, + }))} + visibleColumns={visibleColumns} + onToggleColumn={handleToggleColumn} + /> {onExportCSV && (