tenantpilot/lib/hooks/useTablePreferences.ts
Ahmed Darrazi 41e80b6c0c
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
feat(policy-explorer-v2): implement MVP Phase 1-3
 New Features
- Advanced data table with TanStack Table v8 + Server Actions
- Server-side pagination (10/25/50/100 rows per page)
- Multi-column sorting with visual indicators
- Column management (show/hide, resize) persisted to localStorage
- URL state synchronization for shareable filtered views
- Sticky header with compact/comfortable density modes

📦 Components Added
- PolicyTableV2.tsx - Main table with TanStack integration
- PolicyTableColumns.tsx - 7 column definitions with sorting
- PolicyTablePagination.tsx - Pagination controls
- PolicyTableToolbar.tsx - Density toggle + column visibility menu
- ColumnVisibilityMenu.tsx - Show/hide columns dropdown

🔧 Hooks Added
- usePolicyTable.ts - TanStack Table initialization
- useURLState.ts - URL query param sync with nuqs
- useTablePreferences.ts - localStorage persistence

🎨 Server Actions Updated
- getPolicySettingsV2 - Pagination + sorting + filtering + Zod validation
- exportPolicySettingsCSV - Server-side CSV generation (max 5000 rows)

📚 Documentation Added
- Intune Migration Guide (1400+ lines) - Reverse engineering strategy
- Intune Reference Version tracking
- Tasks completed: 22/62 (Phase 1-3)

 Zero TypeScript compilation errors
 All MVP success criteria met (pagination, sorting, column management)
 Ready for Phase 4-7 (filtering, export, detail view, polish)

Refs: specs/004-policy-explorer-v2/tasks.md
2025-12-10 00:18:05 +01:00

163 lines
4.3 KiB
TypeScript

/**
* useTablePreferences Hook
*
* Manages localStorage persistence for user table preferences:
* - Column visibility (show/hide columns)
* - Column sizing (width in pixels)
* - Column order (reordering)
* - Density mode (compact vs comfortable)
* - Default page size
*
* Includes versioning for forward compatibility when adding new preferences.
*/
import { useState, useEffect, useCallback } from 'react';
import type { TablePreferences } from '@/lib/types/policy-table';
import type { VisibilityState, ColumnSizingState } from '@tanstack/react-table';
const STORAGE_KEY = 'policy-explorer-preferences';
const STORAGE_VERSION = 1;
// Default preferences
const DEFAULT_PREFERENCES: TablePreferences = {
version: STORAGE_VERSION,
columnVisibility: {},
columnSizing: {},
columnOrder: [],
density: 'comfortable',
defaultPageSize: 50,
};
/**
* Load preferences from localStorage with error handling
*/
function loadPreferences(): TablePreferences {
if (typeof window === 'undefined') {
return DEFAULT_PREFERENCES;
}
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) {
return DEFAULT_PREFERENCES;
}
const parsed = JSON.parse(stored) as TablePreferences;
// Version migration logic
if (parsed.version !== STORAGE_VERSION) {
// Future: Handle migrations between versions
console.log('Migrating preferences from version', parsed.version, 'to', STORAGE_VERSION);
return { ...DEFAULT_PREFERENCES, ...parsed, version: STORAGE_VERSION };
}
return parsed;
} catch (error) {
console.error('Failed to load table preferences:', error);
return DEFAULT_PREFERENCES;
}
}
/**
* Save preferences to localStorage with error handling
*/
function savePreferences(preferences: TablePreferences): void {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
} catch (error) {
console.error('Failed to save table preferences:', error);
// Handle quota exceeded
if (error instanceof Error && error.name === 'QuotaExceededError') {
console.warn('localStorage quota exceeded. Clearing old preferences.');
try {
localStorage.removeItem(STORAGE_KEY);
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
} catch (retryError) {
console.error('Failed to clear and save preferences:', retryError);
}
}
}
}
export function useTablePreferences() {
const [preferences, setPreferences] = useState<TablePreferences>(DEFAULT_PREFERENCES);
const [isLoaded, setIsLoaded] = useState(false);
// Load preferences on mount
useEffect(() => {
const loaded = loadPreferences();
setPreferences(loaded);
setIsLoaded(true);
}, []);
// Save preferences whenever they change
useEffect(() => {
if (isLoaded) {
savePreferences(preferences);
}
}, [preferences, isLoaded]);
// Update column visibility
const updateColumnVisibility = useCallback((visibility: VisibilityState) => {
setPreferences((prev) => ({
...prev,
columnVisibility: visibility,
}));
}, []);
// Update column sizing
const updateColumnSizing = useCallback((sizing: ColumnSizingState) => {
setPreferences((prev) => ({
...prev,
columnSizing: sizing,
}));
}, []);
// Update column order
const updateColumnOrder = useCallback((order: string[]) => {
setPreferences((prev) => ({
...prev,
columnOrder: order,
}));
}, []);
// Update density mode
const updateDensity = useCallback((density: 'compact' | 'comfortable') => {
setPreferences((prev) => ({
...prev,
density,
}));
}, []);
// Update default page size
const updateDefaultPageSize = useCallback((pageSize: 10 | 25 | 50 | 100) => {
setPreferences((prev) => ({
...prev,
defaultPageSize: pageSize,
}));
}, []);
// Reset all preferences to defaults
const resetPreferences = useCallback(() => {
setPreferences(DEFAULT_PREFERENCES);
if (typeof window !== 'undefined') {
localStorage.removeItem(STORAGE_KEY);
}
}, []);
return {
preferences,
isLoaded,
updateColumnVisibility,
updateColumnSizing,
updateColumnOrder,
updateDensity,
updateDefaultPageSize,
resetPreferences,
};
}