diff --git a/backend/igny8_core/modules/planner/views.py b/backend/igny8_core/modules/planner/views.py index cdc99cfd..3919757a 100644 --- a/backend/igny8_core/modules/planner/views.py +++ b/backend/igny8_core/modules/planner/views.py @@ -774,7 +774,7 @@ class ClusterViewSet(SiteSectorModelViewSet): # Search configuration - search by name search_fields = ['name'] - # Ordering configuration + # Ordering configuration - volume and difficulty are model fields kept in sync ordering_fields = ['name', 'created_at', 'keywords_count', 'volume', 'difficulty'] ordering = ['name'] # Default ordering diff --git a/frontend/src/components/common/ColumnSelector.tsx b/frontend/src/components/common/ColumnSelector.tsx index a2760d38..ff873f45 100644 --- a/frontend/src/components/common/ColumnSelector.tsx +++ b/frontend/src/components/common/ColumnSelector.tsx @@ -11,6 +11,7 @@ interface ColumnSelectorProps { visibleColumns: Set; onToggleColumn: (columnKey: string) => void; className?: string; + compact?: boolean; // New prop for compact mode (icon only) } export default function ColumnSelector({ @@ -18,6 +19,7 @@ export default function ColumnSelector({ visibleColumns, onToggleColumn, className = '', + compact = false, }: ColumnSelectorProps) { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -53,7 +55,10 @@ export default function ColumnSelector({ ref={buttonRef} type="button" onClick={() => setIsOpen(!isOpen)} - className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700 transition-colors" + className={`inline-flex items-center justify-center gap-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700 transition-colors ${ + compact ? 'w-8 h-8 p-0' : 'px-3 py-2' + }`} + title={compact ? `Columns (${visibleCount}/${totalCount})` : undefined} > - Columns - - ({visibleCount}/{totalCount}) - - + {!compact && ( + <> + Columns + + ({visibleCount}/{totalCount}) + + + + )} + {compact && ( + + {visibleCount} + + )} {isOpen && ( diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 36183c01..0dfaf5e7 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -134,7 +134,7 @@ export const createClustersPageConfig = ( { key: 'ideas_count', label: 'Ideas', - sortable: true, + sortable: false, // Backend doesn't support sorting by ideas_count sortField: 'ideas_count', width: '120px', render: (value: number) => value.toLocaleString(), @@ -180,7 +180,7 @@ export const createClustersPageConfig = ( { key: 'content_count', label: 'Content', - sortable: true, + sortable: false, // Backend doesn't support sorting by content_count sortField: 'content_count', width: '120px', render: (value: number) => value.toLocaleString(), @@ -220,7 +220,7 @@ export const createClustersPageConfig = ( { key: 'mapped_pages', label: 'Mapped Pages', - sortable: true, + sortable: false, // Backend doesn't support sorting by mapped_pages sortField: 'mapped_pages', defaultVisible: false, width: '120px', @@ -229,7 +229,7 @@ export const createClustersPageConfig = ( { key: 'updated_at', label: 'Updated', - sortable: true, + sortable: false, // Backend doesn't support sorting by updated_at sortField: 'updated_at', defaultVisible: false, render: (value: string) => formatRelativeDate(value), diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index 19aa86ab..9257338c 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -116,7 +116,7 @@ export const createIdeasPageConfig = ( { key: 'content_structure', label: 'Structure', - sortable: true, + sortable: false, // Backend doesn't support sorting by content_structure sortField: 'content_structure', width: '130px', render: (value: string) => { @@ -134,7 +134,7 @@ export const createIdeasPageConfig = ( { key: 'content_type', label: 'Type', - sortable: true, + sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', width: '110px', render: (value: string) => { @@ -161,14 +161,14 @@ export const createIdeasPageConfig = ( { key: 'keyword_cluster_name', label: 'Cluster', - sortable: true, + sortable: false, // Backend doesn't support sorting by keyword_cluster_id sortField: 'keyword_cluster_id', width: '200px', render: (_value: string, row: ContentIdea) => row.keyword_cluster_name || '-', }, { ...statusColumn, - sortable: true, + sortable: false, // Backend doesn't support sorting by status sortField: 'status', render: (value: string) => { const statusColors: Record = { @@ -202,7 +202,7 @@ export const createIdeasPageConfig = ( { key: 'updated_at', label: 'Updated', - sortable: true, + sortable: false, // Backend doesn't support sorting by updated_at sortField: 'updated_at', defaultVisible: false, render: (value: string) => formatRelativeDate(value), diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index 3c646499..17df873b 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -144,8 +144,8 @@ export const createKeywordsPageConfig = ( columns: [ { ...keywordColumn, - sortable: true, - sortField: 'keyword', + sortable: false, // Backend doesn't support sorting by keyword field + sortField: 'seed_keyword__keyword', }, // Sector column - only show when viewing all sectors ...(showSectorColumn ? [{ @@ -159,19 +159,19 @@ export const createKeywordsPageConfig = ( { ...volumeColumn, sortable: true, - sortField: 'volume', + sortField: 'seed_keyword__volume', // Backend expects seed_keyword__volume render: (value: number) => value.toLocaleString(), }, { ...clusterColumn, - sortable: true, + sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', render: (_value: string, row: Keyword) => row.cluster_name || '-', }, { ...difficultyColumn, sortable: true, - sortField: 'difficulty', + sortField: 'seed_keyword__difficulty', // Backend expects seed_keyword__difficulty align: 'center' as const, render: (value: number) => { const difficultyNum = getDifficultyNumber(value); @@ -198,8 +198,8 @@ export const createKeywordsPageConfig = ( }, { ...intentColumn, - sortable: true, - sortField: 'intent', + sortable: false, // Backend doesn't support sorting by intent + sortField: 'seed_keyword__intent', render: (value: string) => { // Map intent values to badge colors // Transactional and Commercial → success (green, like active) diff --git a/frontend/src/config/pages/published.config.tsx b/frontend/src/config/pages/published.config.tsx index d8550a96..1d45a159 100644 --- a/frontend/src/config/pages/published.config.tsx +++ b/frontend/src/config/pages/published.config.tsx @@ -149,7 +149,7 @@ export function createPublishedPageConfig(params: { { key: 'content_type', label: 'Type', - sortable: true, + sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', width: '110px', render: (value: string) => { @@ -165,7 +165,7 @@ export function createPublishedPageConfig(params: { { key: 'content_structure', label: 'Structure', - sortable: true, + sortable: false, // Backend doesn't support sorting by content_structure sortField: 'content_structure', width: '130px', render: (value: string) => { @@ -248,7 +248,7 @@ export function createPublishedPageConfig(params: { { key: 'word_count', label: 'Words', - sortable: true, + sortable: false, // Backend doesn't support sorting by word_count sortField: 'word_count', numeric: true, width: '100px', diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index bbe3f300..345c310d 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -136,7 +136,7 @@ export const createTasksPageConfig = ( { key: 'cluster_name', label: 'Cluster', - sortable: true, + sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', width: '200px', render: (_value: string, row: Task) => row.cluster_name || '-', @@ -162,7 +162,7 @@ export const createTasksPageConfig = ( { key: 'content_type', label: 'Type', - sortable: true, + sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', width: '110px', render: (value: string) => { diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 2535539a..f45762d9 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -559,7 +559,7 @@ export async function fetchKeywords(filters: KeywordFilters = {}): Promise; + // Map of page pathname to column preference with timestamp + pageColumns: Record; + + // Hydration flag to know when persist has loaded + _hasHydrated: boolean; + setHasHydrated: (state: boolean) => void; // Actions setPageColumns: (pathname: string, columnKeys: string[]) => void; @@ -19,50 +28,107 @@ interface ColumnVisibilityState { resetPageColumns: (pathname: string) => void; } +// Helper function to get user ID directly from localStorage (synchronous, no race conditions) +// CRITICAL: We must read directly from localStorage, not from useAuthStore.getState().user +// because the auth store might not be hydrated yet when this storage is accessed +const getUserIdFromStorage = (): string => { + try { + const authData = localStorage.getItem('auth-storage'); + if (authData) { + const parsed = JSON.parse(authData); + const userId = parsed?.state?.user?.id; + if (userId) { + return String(userId); + } + } + } catch (error) { + // Silent fail - will use anonymous + } + return 'anonymous'; +}; + // Custom storage that uses user-specific keys +// IMPORTANT: The persist middleware passes the 'name' param, but we need to append user ID +// CRITICAL FIX: Get user ID directly from localStorage synchronously to avoid race conditions const userSpecificStorage: StateStorage = { getItem: (name: string) => { - const user = useAuthStore.getState().user; - const userId = user?.id || 'anonymous'; - const key = `igny8-column-visibility-user-${userId}`; + const userId = getUserIdFromStorage(); + const key = `${name}-user-${userId}`; const value = localStorage.getItem(key); + if (typeof window !== 'undefined' && window.location.pathname.includes('/writer/')) { + console.log('🔍 STORAGE GET:', { key, hasValue: !!value, valuePreview: value?.substring(0, 50) }); + } return value; }, setItem: (name: string, value: string) => { - const user = useAuthStore.getState().user; - const userId = user?.id || 'anonymous'; - const key = `igny8-column-visibility-user-${userId}`; + const userId = getUserIdFromStorage(); + const key = `${name}-user-${userId}`; + if (typeof window !== 'undefined' && window.location.pathname.includes('/writer/')) { + console.log('💾 STORAGE SET:', { key, valuePreview: value?.substring(0, 50) }); + } localStorage.setItem(key, value); }, removeItem: (name: string) => { - const user = useAuthStore.getState().user; - const userId = user?.id || 'anonymous'; - const key = `igny8-column-visibility-user-${userId}`; + const userId = getUserIdFromStorage(); + const key = `${name}-user-${userId}`; localStorage.removeItem(key); }, }; +// 30 days in milliseconds +const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; + export const useColumnVisibilityStore = create()( persist( (set, get) => ({ pageColumns: {}, + _hasHydrated: false, + + setHasHydrated: (state: boolean) => { + set({ _hasHydrated: state }); + }, setPageColumns: (pathname: string, columnKeys: string[]) => { + if (pathname.includes('/writer/')) { + console.log('📝 setPageColumns:', { pathname, columns: columnKeys, timestamp: new Date().toISOString() }); + } set((state) => ({ pageColumns: { ...state.pageColumns, - [pathname]: columnKeys, + [pathname]: { + columns: columnKeys, + timestamp: Date.now(), + }, }, })); }, getPageColumns: (pathname: string) => { - return get().pageColumns[pathname] || []; + const preference = get().pageColumns[pathname]; + if (pathname.includes('/writer/')) { + console.log('📖 getPageColumns:', { pathname, hasPreference: !!preference, columns: preference?.columns }); + } + if (!preference) return []; + + // Check if preference has expired (older than 30 days) + const now = Date.now(); + if (now - preference.timestamp > THIRTY_DAYS_MS) { + // Remove expired preference + set((state) => { + const newPageColumns = { ...state.pageColumns }; + delete newPageColumns[pathname]; + return { pageColumns: newPageColumns }; + }); + return []; + } + + return preference.columns; }, toggleColumn: (pathname: string, columnKey: string) => { set((state) => { - const currentColumns = state.pageColumns[pathname] || []; + const preference = state.pageColumns[pathname]; + const currentColumns = preference?.columns || []; const newColumns = currentColumns.includes(columnKey) ? currentColumns.filter((key) => key !== columnKey) : [...currentColumns, columnKey]; @@ -70,7 +136,10 @@ export const useColumnVisibilityStore = create()( return { pageColumns: { ...state.pageColumns, - [pathname]: newColumns, + [pathname]: { + columns: newColumns, + timestamp: Date.now(), + }, }, }; }); @@ -90,6 +159,12 @@ export const useColumnVisibilityStore = create()( partialize: (state) => ({ pageColumns: state.pageColumns, }), + onRehydrateStorage: () => (state) => { + if (state && typeof window !== 'undefined' && window.location.pathname.includes('/writer/')) { + console.log('💧 REHYDRATED:', { pageColumns: state.pageColumns }); + } + state?.setHasHydrated(true); + }, } ) ); diff --git a/frontend/src/templates/TablePageTemplate.tsx b/frontend/src/templates/TablePageTemplate.tsx index d5307ab2..564dabbf 100644 --- a/frontend/src/templates/TablePageTemplate.tsx +++ b/frontend/src/templates/TablePageTemplate.tsx @@ -29,7 +29,7 @@ import SelectDropdown from '../components/form/SelectDropdown'; import { Dropdown } from '../components/ui/dropdown/Dropdown'; import { DropdownItem } from '../components/ui/dropdown/DropdownItem'; import AlertModal from '../components/ui/alert/AlertModal'; -import { ChevronDownIcon, MoreDotIcon, PlusIcon } from '../icons'; +import { ChevronDownIcon, MoreDotIcon, PlusIcon, ListIcon } from '../icons'; import { useHeaderMetrics } from '../context/HeaderMetricsContext'; import { useToast } from '../components/ui/toast/ToastContainer'; import { getDeleteModalConfig } from '../config/pages/delete-modal.config'; @@ -185,6 +185,9 @@ export default function TablePageTemplate({ const rowActionButtonRefs = React.useRef>>(new Map()); const bulkActionsButtonRef = React.useRef(null); + // Filter toggle state - hidden by default + const [showFilters, setShowFilters] = useState(false); + // Get notification config for current page const deleteModalConfig = getDeleteModalConfig(location.pathname); const bulkActionModalConfig = getBulkActionModalConfig(location.pathname); @@ -237,72 +240,71 @@ export default function TablePageTemplate({ const { setMetrics } = useHeaderMetrics(); const toast = useToast(); const { pageSize, setPageSize } = usePageSizeStore(); - const { pageColumns, setPageColumns, getPageColumns } = useColumnVisibilityStore(); + const { setPageColumns, getPageColumns, _hasHydrated } = useColumnVisibilityStore(); - // Column visibility state management with Zustand store (same pattern as site/sector stores) - // Initialize visible columns from store or defaults - const initializeVisibleColumns = useMemo(() => { - const savedColumnKeys = getPageColumns(location.pathname); - - if (savedColumnKeys.length > 0) { - const savedSet = new Set(savedColumnKeys); - // 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]); - } - } - - // Default: show all columns that have defaultVisible !== false + // Column visibility state management with Zustand store + // Start with defaults, then update once hydration completes + const [visibleColumns, setVisibleColumns] = useState>(() => { + // Always start with defaults on initial render (before hydration) return new Set( columns .filter(col => col.defaultVisible !== false) .map(col => col.key) ); - }, [columns, location.pathname, getPageColumns]); + }); - const [visibleColumns, setVisibleColumns] = useState>(initializeVisibleColumns); + // Track if we've already loaded saved columns for this page to prevent loops + const hasLoadedSavedColumns = useRef(false); - // Update visible columns when columns prop or pathname changes + // Load saved columns from store after hydration completes (one-time per page) useEffect(() => { - const savedColumnKeys = getPageColumns(location.pathname); + console.log('🔄 LOAD EFFECT:', { hasHydrated: _hasHydrated, hasLoaded: hasLoadedSavedColumns.current, pathname: location.pathname }); + if (!_hasHydrated || hasLoadedSavedColumns.current) return; - if (savedColumnKeys.length > 0) { + // Mark as loaded FIRST to prevent save effect from firing during this update + hasLoadedSavedColumns.current = true; + console.log('✅ Marked hasLoaded = true'); + + const savedColumnKeys = getPageColumns(location.pathname); + console.log('📥 LOADING saved columns:', savedColumnKeys); + + if (savedColumnKeys && savedColumnKeys.length > 0) { const savedSet = new Set(savedColumnKeys); - // Validate that all saved columns still exist + // Validate that all saved columns still exist in current columns config 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); - const newSet = new Set([...Array.from(validColumns.map(col => col.key)), ...newColumns]); - setVisibleColumns(newSet); - // Update store with validated columns - setPageColumns(location.pathname, Array.from(newSet)); + const mergedColumns = [...Array.from(validColumns.map(col => col.key)), ...newColumns]; + console.log('✅ Setting visible columns to:', mergedColumns); + setVisibleColumns(new Set(mergedColumns)); return; } } - // Default: show all columns that have defaultVisible !== false - const defaultSet = new Set( - columns - .filter(col => col.defaultVisible !== false) - .map(col => col.key) - ); - setVisibleColumns(defaultSet); - // Save defaults to store - setPageColumns(location.pathname, Array.from(defaultSet)); - }, [columns, location.pathname, getPageColumns, setPageColumns]); + console.log('⚠️ No valid saved columns, keeping defaults'); + }, [_hasHydrated, location.pathname]); + // NOTE: NOT including 'getPageColumns' or 'columns' - would cause unnecessary re-runs - // Save to store whenever visibleColumns changes (Zustand persist middleware handles localStorage) + // Reset loaded flag when pathname changes (navigating to different page) useEffect(() => { + hasLoadedSavedColumns.current = false; + }, [location.pathname]); + + // Save to store whenever visibleColumns changes (only after hydration to avoid saving defaults) + useEffect(() => { + console.log('💾 SAVE EFFECT:', { + hasHydrated: _hasHydrated, + hasLoaded: hasLoadedSavedColumns.current, + willSave: _hasHydrated && hasLoadedSavedColumns.current, + columns: Array.from(visibleColumns) + }); + if (!_hasHydrated || !hasLoadedSavedColumns.current) return; // Don't save until hydration completes AND saved columns have been loaded setPageColumns(location.pathname, Array.from(visibleColumns)); - }, [visibleColumns, location.pathname, setPageColumns]); + }, [_hasHydrated, visibleColumns, location.pathname, setPageColumns]); // Filter columns based on visibility const visibleColumnsList = useMemo(() => { @@ -597,171 +599,167 @@ export default function TablePageTemplate({ return (
- {/* Filters Row - 75% centered, container inside stretched to 100% */} - {(renderFilters || filters.length > 0) && ( -
-
-
-
- {renderFilters ? ( -
- {renderFilters} -
- ) : ( - <> - {filters.map((filter) => { - // Handle custom render filters (for complex filters like volume range) - if (filter.type === 'custom' && (filter as any).customRender) { - return {(filter as any).customRender()}; - } - - if (filter.type === 'text') { - return ( - { - onFilterChange?.(filter.key, e.target.value); - }} - className="flex-1 min-w-[200px] h-9" - /> - ); - } else if (filter.type === 'select') { - const currentValue = filterValues[filter.key] || ''; - return ( - { - // Ensure we pass the value even if it's an empty string - const newValue = value === null || value === undefined ? '' : String(value); - onFilterChange?.(filter.key, newValue); - }} - className={filter.className || "flex-1 min-w-[140px]"} - /> - ); - } - return null; - })} - - )} -
- {hasActiveFilters && onFilterReset && ( + {/* Bulk Actions and Action Buttons Row - Fixed height container */} +
+ {/* Left side - Bulk Actions and Filter Toggle */} +
+ {/* Bulk Actions - Single button if only one action, dropdown if multiple */} + {bulkActions.length > 0 && ( +
+ {bulkActions.length === 1 ? ( + // Single button for single action - )} -
-
-
- )} - - {/* Bulk Actions and Action Buttons Row */} -
- {/* Bulk Actions - Single button if only one action, dropdown if multiple */} - {bulkActions.length > 0 && ( -
- {bulkActions.length === 1 ? ( - // Single button for single action - - ) : ( - // Dropdown for multiple actions - <> - - 0} - onClose={() => setIsBulkActionsDropdownOpen(false)} - anchorRef={bulkActionsButtonRef as React.RefObject} - placement="bottom-left" - className="w-48 p-2" - > - {bulkActions.map((action, index) => { - const isDelete = action.key === 'delete'; - const showDivider = isDelete && index > 0; - return ( - - {showDivider &&
} - { - handleBulkActionClick(action.key, selectedIds); - }} - className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ - isDelete - ? "text-error-500 hover:bg-error-50 hover:text-error-600 dark:text-error-400 dark:hover:bg-error-500/15 dark:hover:text-error-300" - : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" - }`} - > - {action.icon && {action.icon}} - {action.label} - -
- ); - })} -
- - )} + ) : ( + // Dropdown for multiple actions + <> + + 0} + onClose={() => setIsBulkActionsDropdownOpen(false)} + anchorRef={bulkActionsButtonRef as React.RefObject} + placement="bottom-left" + className="w-48 p-2" + > + {bulkActions.map((action, index) => { + const isDelete = action.key === 'delete'; + const showDivider = isDelete && index > 0; + return ( + + {showDivider &&
} + { + handleBulkActionClick(action.key, selectedIds); + }} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + isDelete + ? "text-error-500 hover:bg-error-50 hover:text-error-600 dark:text-error-400 dark:hover:bg-error-500/15 dark:hover:text-error-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + {action.icon && {action.icon}} + {action.label} + +
+ ); + })} +
+ + )} +
+ )} + + {/* Filter Toggle Button */} + {(renderFilters || filters.length > 0) && ( + + )} +
+ + {/* Center - Filters (when toggled on) */} + {showFilters && (renderFilters || filters.length > 0) && ( +
+
+
+ {renderFilters ? ( + renderFilters + ) : ( + <> + {filters.map((filter) => { + // Handle custom render filters (for complex filters like volume range) + if (filter.type === 'custom' && (filter as any).customRender) { + return {(filter as any).customRender()}; + } + + if (filter.type === 'text') { + return ( + { + onFilterChange?.(filter.key, e.target.value); + }} + className="w-full sm:flex-1 h-9" + /> + ); + } else if (filter.type === 'select') { + const currentValue = filterValues[filter.key] || ''; + return ( + { + // Ensure we pass the value even if it's an empty string + const newValue = value === null || value === undefined ? '' : String(value); + onFilterChange?.(filter.key, newValue); + }} + className={filter.className || "w-full sm:flex-1"} + /> + ); + } + return null; + })} + {hasActiveFilters && onFilterReset && ( + + )} + + )} +
+
)} - {/* Action Buttons - Right aligned */} + {/* Right side - Action Buttons */}
{/* Custom Actions */} {customActions} - - {/* Column Selector */} - ({ - key: col.key, - label: col.label, - defaultVisible: col.defaultVisible !== false, - }))} - visibleColumns={visibleColumns} - onToggleColumn={handleToggleColumn} - /> {onExportCSV && (