All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
✨ 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
163 lines
4.3 KiB
TypeScript
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,
|
|
};
|
|
}
|