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
138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
/**
|
|
* useURLState Hook
|
|
*
|
|
* Synchronizes table state with URL query parameters for shareable filtered/sorted views.
|
|
* Uses `nuqs` library for type-safe URL state management with Next.js App Router.
|
|
*
|
|
* URL Parameters:
|
|
* - p: page (0-based index)
|
|
* - ps: pageSize (10, 25, 50, 100)
|
|
* - sb: sortBy (settingName, policyName, policyType, lastSyncedAt)
|
|
* - sd: sortDir (asc, desc)
|
|
* - pt: policyTypes (comma-separated)
|
|
* - q: searchQuery
|
|
*/
|
|
|
|
import { useQueryState, parseAsInteger, parseAsString, parseAsStringLiteral, parseAsArrayOf } from 'nuqs';
|
|
import { useCallback } from 'react';
|
|
|
|
const PAGE_SIZES = [10, 25, 50, 100] as const;
|
|
const SORT_BY_OPTIONS = ['settingName', 'policyName', 'policyType', 'lastSyncedAt'] as const;
|
|
const SORT_DIR_OPTIONS = ['asc', 'desc'] as const;
|
|
|
|
export function useURLState() {
|
|
// Page (0-based)
|
|
const [page, setPage] = useQueryState(
|
|
'p',
|
|
parseAsInteger.withDefault(0)
|
|
);
|
|
|
|
// Page Size
|
|
const [pageSize, setPageSize] = useQueryState(
|
|
'ps',
|
|
parseAsInteger.withDefault(50)
|
|
);
|
|
|
|
// Sort By
|
|
const [sortBy, setSortBy] = useQueryState(
|
|
'sb',
|
|
parseAsString.withDefault('settingName')
|
|
);
|
|
|
|
// Sort Direction
|
|
const [sortDir, setSortDir] = useQueryState(
|
|
'sd',
|
|
parseAsStringLiteral(['asc', 'desc'] as const).withDefault('asc')
|
|
);
|
|
|
|
// Policy Types (comma-separated)
|
|
const [policyTypes, setPolicyTypes] = useQueryState(
|
|
'pt',
|
|
parseAsArrayOf(parseAsString, ',').withDefault([])
|
|
);
|
|
|
|
// Search Query
|
|
const [searchQuery, setSearchQuery] = useQueryState(
|
|
'q',
|
|
parseAsString.withDefault('')
|
|
);
|
|
|
|
// Update page with validation
|
|
const updatePage = useCallback((newPage: number) => {
|
|
const validPage = Math.max(0, newPage);
|
|
setPage(validPage);
|
|
}, [setPage]);
|
|
|
|
// Update page size with validation
|
|
const updatePageSize = useCallback((newPageSize: number) => {
|
|
const validPageSize = PAGE_SIZES.includes(newPageSize as typeof PAGE_SIZES[number])
|
|
? newPageSize
|
|
: 50;
|
|
setPageSize(validPageSize);
|
|
// Reset to first page when changing page size
|
|
setPage(0);
|
|
}, [setPageSize, setPage]);
|
|
|
|
// Update sorting
|
|
const updateSorting = useCallback((newSortBy: string, newSortDir: 'asc' | 'desc') => {
|
|
setSortBy(newSortBy);
|
|
setSortDir(newSortDir);
|
|
}, [setSortBy, setSortDir]);
|
|
|
|
// Toggle sort direction
|
|
const toggleSortDir = useCallback(() => {
|
|
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
|
}, [sortDir, setSortDir]);
|
|
|
|
// Update policy types filter
|
|
const updatePolicyTypes = useCallback((types: string[]) => {
|
|
setPolicyTypes(types);
|
|
// Reset to first page when changing filters
|
|
setPage(0);
|
|
}, [setPolicyTypes, setPage]);
|
|
|
|
// Update search query
|
|
const updateSearchQuery = useCallback((query: string) => {
|
|
setSearchQuery(query);
|
|
// Reset to first page when searching
|
|
setPage(0);
|
|
}, [setSearchQuery, setPage]);
|
|
|
|
// Clear all filters
|
|
const clearFilters = useCallback(() => {
|
|
setPolicyTypes([]);
|
|
setSearchQuery('');
|
|
setPage(0);
|
|
}, [setPolicyTypes, setSearchQuery, setPage]);
|
|
|
|
// Reset all URL state
|
|
const resetURLState = useCallback(() => {
|
|
setPage(0);
|
|
setPageSize(50);
|
|
setSortBy('settingName');
|
|
setSortDir('asc');
|
|
setPolicyTypes([]);
|
|
setSearchQuery('');
|
|
}, [setPage, setPageSize, setSortBy, setSortDir, setPolicyTypes, setSearchQuery]);
|
|
|
|
return {
|
|
// Current state
|
|
page,
|
|
pageSize,
|
|
sortBy,
|
|
sortDir,
|
|
policyTypes,
|
|
searchQuery,
|
|
|
|
// Update functions
|
|
updatePage,
|
|
updatePageSize,
|
|
updateSorting,
|
|
toggleSortDir,
|
|
updatePolicyTypes,
|
|
updateSearchQuery,
|
|
clearFilters,
|
|
resetURLState,
|
|
};
|
|
}
|