tenantpilot/lib/hooks/useURLState.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

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,
};
}