@superdangerous/app-framework 4.14.0 → 4.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/validation.d.ts +12 -12
- package/dist/services/emailService.d.ts +146 -0
- package/dist/services/emailService.d.ts.map +1 -0
- package/dist/services/emailService.js +649 -0
- package/dist/services/emailService.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/package.json +9 -1
- package/src/index.ts +14 -0
- package/src/services/emailService.ts +812 -0
- package/src/services/index.ts +14 -0
- package/ui/data-table/components/BatchActionsBar.tsx +53 -0
- package/ui/data-table/components/ColumnVisibility.tsx +111 -0
- package/ui/data-table/components/DataTable.tsx +492 -0
- package/ui/data-table/components/DataTablePage.tsx +238 -0
- package/ui/data-table/components/Pagination.tsx +203 -0
- package/ui/data-table/components/PaginationControls.tsx +122 -0
- package/ui/data-table/components/TableFilters.tsx +139 -0
- package/ui/data-table/components/index.ts +41 -0
- package/ui/data-table/components/types.ts +181 -0
- package/ui/data-table/hooks/index.ts +17 -0
- package/ui/data-table/hooks/useColumnOrder.ts +233 -0
- package/ui/data-table/hooks/useColumnVisibility.ts +128 -0
- package/ui/data-table/hooks/usePagination.ts +160 -0
- package/ui/data-table/hooks/useResizableColumns.ts +280 -0
- package/ui/data-table/index.ts +84 -0
- package/ui/dist/index.d.mts +207 -5
- package/ui/dist/index.d.ts +207 -5
- package/ui/dist/index.js +36 -43
- package/ui/dist/index.js.map +1 -1
- package/ui/dist/index.mjs +36 -43
- package/ui/dist/index.mjs.map +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Table Components
|
|
3
|
+
*
|
|
4
|
+
* Reusable components for building data tables:
|
|
5
|
+
* - DataTable: Full-featured generic data table
|
|
6
|
+
* - DataTablePage: Full-page layout with header controls
|
|
7
|
+
* - PaginationControls: Compact inline pagination
|
|
8
|
+
* - Pagination: Full pagination with page numbers
|
|
9
|
+
* - BatchActionsBar: Multi-select action bar
|
|
10
|
+
* - ColumnVisibility: Column toggle dropdown
|
|
11
|
+
* - TableFilters: Search and filter controls
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { DataTable } from './DataTable';
|
|
15
|
+
export type {
|
|
16
|
+
DataTableProps,
|
|
17
|
+
ColumnDef,
|
|
18
|
+
ColumnWidth,
|
|
19
|
+
ColumnVisibility as ColumnVisibilityConfig,
|
|
20
|
+
HeaderCellProps,
|
|
21
|
+
CellProps,
|
|
22
|
+
ExternalPaginationState,
|
|
23
|
+
ColumnConfigCompat,
|
|
24
|
+
ColumnSizeConfig,
|
|
25
|
+
} from './types';
|
|
26
|
+
|
|
27
|
+
export { DataTablePage } from './DataTablePage';
|
|
28
|
+
export type { DataTablePageProps, FilterOption } from './DataTablePage';
|
|
29
|
+
|
|
30
|
+
export { PaginationControls } from './PaginationControls';
|
|
31
|
+
export type { PaginationControlsProps } from './PaginationControls';
|
|
32
|
+
|
|
33
|
+
export { Pagination } from './Pagination';
|
|
34
|
+
|
|
35
|
+
export { BatchActionsBar } from './BatchActionsBar';
|
|
36
|
+
export type { BatchActionsBarProps } from './BatchActionsBar';
|
|
37
|
+
|
|
38
|
+
export { ColumnVisibility } from './ColumnVisibility';
|
|
39
|
+
|
|
40
|
+
export { TableFilters } from './TableFilters';
|
|
41
|
+
export type { TableFiltersProps, FilterOption as TableFilterOption } from './TableFilters';
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column width configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface ColumnWidth {
|
|
7
|
+
default: number;
|
|
8
|
+
min: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Column visibility configuration
|
|
14
|
+
*/
|
|
15
|
+
export interface ColumnVisibility {
|
|
16
|
+
default: boolean;
|
|
17
|
+
locked?: boolean; // If true, column cannot be hidden
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Props passed to header cell render function
|
|
22
|
+
*/
|
|
23
|
+
export interface HeaderCellProps {
|
|
24
|
+
columnId: string;
|
|
25
|
+
isSorted: boolean;
|
|
26
|
+
sortDirection?: 'asc' | 'desc';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Props passed to cell render function
|
|
31
|
+
*/
|
|
32
|
+
export interface CellProps {
|
|
33
|
+
columnId: string;
|
|
34
|
+
isDragging: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Column definition for DataTable
|
|
39
|
+
*/
|
|
40
|
+
export interface ColumnDef<T> {
|
|
41
|
+
/** Unique column identifier */
|
|
42
|
+
id: string;
|
|
43
|
+
|
|
44
|
+
/** Header content - string or render function */
|
|
45
|
+
header: string | ((props: HeaderCellProps) => ReactNode);
|
|
46
|
+
|
|
47
|
+
/** Cell content render function */
|
|
48
|
+
cell: (item: T, props: CellProps) => ReactNode;
|
|
49
|
+
|
|
50
|
+
/** Key to use for sorting (if sortable) */
|
|
51
|
+
sortKey?: string;
|
|
52
|
+
|
|
53
|
+
/** Width configuration */
|
|
54
|
+
width?: ColumnWidth;
|
|
55
|
+
|
|
56
|
+
/** Visibility configuration */
|
|
57
|
+
visibility?: ColumnVisibility;
|
|
58
|
+
|
|
59
|
+
/** Additional CSS class for cells */
|
|
60
|
+
className?: string;
|
|
61
|
+
|
|
62
|
+
/** Whether this column should use column style from resize hook */
|
|
63
|
+
resizable?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// DragState is exported from ../hooks/useColumnOrder
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* External pagination state (from usePagination hook)
|
|
70
|
+
*/
|
|
71
|
+
export interface ExternalPaginationState<T> {
|
|
72
|
+
paginatedData: T[];
|
|
73
|
+
page: number;
|
|
74
|
+
pageSize: number;
|
|
75
|
+
totalPages: number;
|
|
76
|
+
totalItems: number;
|
|
77
|
+
startIndex: number;
|
|
78
|
+
endIndex: number;
|
|
79
|
+
canGoNext: boolean;
|
|
80
|
+
canGoPrev: boolean;
|
|
81
|
+
pageSizeOptions: number[];
|
|
82
|
+
setPage: (page: number) => void;
|
|
83
|
+
setPageSize: (size: number) => void;
|
|
84
|
+
nextPage: () => void;
|
|
85
|
+
prevPage: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* DataTable props
|
|
90
|
+
*/
|
|
91
|
+
export interface DataTableProps<T> {
|
|
92
|
+
/** Data array to display */
|
|
93
|
+
data: T[];
|
|
94
|
+
|
|
95
|
+
/** Column definitions */
|
|
96
|
+
columns: ColumnDef<T>[];
|
|
97
|
+
|
|
98
|
+
/** Storage key for persisting table state (column widths, order, visibility) */
|
|
99
|
+
storageKey: string;
|
|
100
|
+
|
|
101
|
+
/** Function to get unique ID from item */
|
|
102
|
+
getRowId: (item: T) => string;
|
|
103
|
+
|
|
104
|
+
// Selection
|
|
105
|
+
/** Enable row selection with checkboxes */
|
|
106
|
+
selectable?: boolean;
|
|
107
|
+
/** Set of selected row IDs */
|
|
108
|
+
selectedIds?: Set<string>;
|
|
109
|
+
/** Callback when selection changes */
|
|
110
|
+
onSelectionChange?: (ids: Set<string>) => void;
|
|
111
|
+
|
|
112
|
+
// Row interactions
|
|
113
|
+
/** Callback when row is clicked */
|
|
114
|
+
onRowClick?: (item: T) => void;
|
|
115
|
+
/** Callback when row is right-clicked (for context menu) */
|
|
116
|
+
onRowContextMenu?: (item: T, position: { x: number; y: number }) => void;
|
|
117
|
+
|
|
118
|
+
// Sorting
|
|
119
|
+
/** Current sort field */
|
|
120
|
+
sortField?: string;
|
|
121
|
+
/** Current sort order */
|
|
122
|
+
sortOrder?: 'asc' | 'desc';
|
|
123
|
+
/** Callback when sort changes */
|
|
124
|
+
onSort?: (field: string) => void;
|
|
125
|
+
|
|
126
|
+
// Actions column
|
|
127
|
+
/** Render function for actions column (always last, sticky) */
|
|
128
|
+
actionsColumn?: (item: T) => ReactNode;
|
|
129
|
+
/** Width for actions column */
|
|
130
|
+
actionsColumnWidth?: number;
|
|
131
|
+
|
|
132
|
+
// Pagination
|
|
133
|
+
/** Page size for pagination (used when no external pagination provided) */
|
|
134
|
+
pageSize?: number;
|
|
135
|
+
/** External pagination state from usePagination hook (overrides internal pagination) */
|
|
136
|
+
pagination?: ExternalPaginationState<T>;
|
|
137
|
+
/** Hide the built-in pagination controls (use when pagination is shown elsewhere) */
|
|
138
|
+
hidePagination?: boolean;
|
|
139
|
+
|
|
140
|
+
// Styling
|
|
141
|
+
/** Additional CSS class for table container */
|
|
142
|
+
className?: string;
|
|
143
|
+
/** Function to compute row CSS class */
|
|
144
|
+
rowClassName?: (item: T) => string;
|
|
145
|
+
|
|
146
|
+
// Features
|
|
147
|
+
/** Enable header right-click for column visibility menu */
|
|
148
|
+
enableHeaderContextMenu?: boolean;
|
|
149
|
+
/** Columns that cannot be reordered */
|
|
150
|
+
lockedColumns?: string[];
|
|
151
|
+
/** Default column order (if not persisted) */
|
|
152
|
+
defaultColumnOrder?: string[];
|
|
153
|
+
|
|
154
|
+
// Loading state
|
|
155
|
+
/** Show loading indicator */
|
|
156
|
+
loading?: boolean;
|
|
157
|
+
|
|
158
|
+
// Empty state
|
|
159
|
+
/** Content to show when no data */
|
|
160
|
+
emptyState?: ReactNode;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Column config for visibility hook (for compatibility)
|
|
165
|
+
*/
|
|
166
|
+
export interface ColumnConfigCompat {
|
|
167
|
+
id: string;
|
|
168
|
+
label: string;
|
|
169
|
+
defaultVisible?: boolean;
|
|
170
|
+
locked?: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Column config for resize hook (for compatibility)
|
|
175
|
+
*/
|
|
176
|
+
export interface ColumnSizeConfig {
|
|
177
|
+
key: string;
|
|
178
|
+
defaultWidth: number;
|
|
179
|
+
minWidth: number;
|
|
180
|
+
maxWidth?: number;
|
|
181
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Table Hooks
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive hooks for building data tables with:
|
|
5
|
+
* - Pagination with localStorage persistence
|
|
6
|
+
* - Column visibility toggling
|
|
7
|
+
* - Column resizing with drag handles
|
|
8
|
+
* - Column reordering with drag-and-drop
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { usePagination } from './usePagination';
|
|
12
|
+
export { useColumnVisibility } from './useColumnVisibility';
|
|
13
|
+
export type { ColumnConfig, ColumnVisibilityState } from './useColumnVisibility';
|
|
14
|
+
export { useResizableColumns } from './useResizableColumns';
|
|
15
|
+
export type { ResizableColumnResult } from './useResizableColumns';
|
|
16
|
+
export { useColumnOrder, useColumnDragDrop } from './useColumnOrder';
|
|
17
|
+
export type { ColumnOrderConfig, DragState } from './useColumnOrder';
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ColumnOrderConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
locked?: boolean; // Locked columns can't be moved
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseColumnOrderOptions {
|
|
10
|
+
storageKey: string;
|
|
11
|
+
defaultOrder: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UseColumnOrderReturn {
|
|
15
|
+
columnOrder: string[];
|
|
16
|
+
moveColumn: (fromIndex: number, toIndex: number) => void;
|
|
17
|
+
moveColumnById: (columnId: string, direction: 'left' | 'right') => void;
|
|
18
|
+
resetOrder: () => void;
|
|
19
|
+
getOrderedColumns: <T extends { id: string }>(columns: T[]) => T[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook for managing column order with localStorage persistence
|
|
24
|
+
*/
|
|
25
|
+
export function useColumnOrder({
|
|
26
|
+
storageKey,
|
|
27
|
+
defaultOrder,
|
|
28
|
+
}: UseColumnOrderOptions): UseColumnOrderReturn {
|
|
29
|
+
const [columnOrder, setColumnOrder] = useState<string[]>(() => {
|
|
30
|
+
try {
|
|
31
|
+
const stored = localStorage.getItem(storageKey);
|
|
32
|
+
if (stored) {
|
|
33
|
+
const parsed = JSON.parse(stored);
|
|
34
|
+
const storedSet = new Set(parsed);
|
|
35
|
+
const defaultSet = new Set(defaultOrder);
|
|
36
|
+
|
|
37
|
+
// Remove old columns that aren't in default anymore
|
|
38
|
+
const validStored = parsed.filter((col: string) => defaultSet.has(col));
|
|
39
|
+
|
|
40
|
+
// Find missing columns and insert them at their default positions
|
|
41
|
+
const missingColumns = defaultOrder.filter(col => !storedSet.has(col));
|
|
42
|
+
|
|
43
|
+
if (missingColumns.length > 0) {
|
|
44
|
+
// Build new order by inserting missing columns at their default positions
|
|
45
|
+
const result = [...validStored];
|
|
46
|
+
for (const missing of missingColumns) {
|
|
47
|
+
const defaultIndex = defaultOrder.indexOf(missing);
|
|
48
|
+
// Find the best insertion point based on surrounding columns in default order
|
|
49
|
+
let insertAt = result.length;
|
|
50
|
+
for (let i = 0; i < result.length; i++) {
|
|
51
|
+
const currentDefaultIndex = defaultOrder.indexOf(result[i]);
|
|
52
|
+
if (currentDefaultIndex > defaultIndex) {
|
|
53
|
+
insertAt = i;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
result.splice(insertAt, 0, missing);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return validStored.length > 0 ? validStored : defaultOrder;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.warn('Failed to load column order from localStorage:', e);
|
|
66
|
+
}
|
|
67
|
+
return defaultOrder;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Persist to localStorage
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
try {
|
|
73
|
+
localStorage.setItem(storageKey, JSON.stringify(columnOrder));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.warn('Failed to save column order to localStorage:', e);
|
|
76
|
+
}
|
|
77
|
+
}, [storageKey, columnOrder]);
|
|
78
|
+
|
|
79
|
+
const moveColumn = useCallback((fromIndex: number, toIndex: number) => {
|
|
80
|
+
if (fromIndex === toIndex) return;
|
|
81
|
+
|
|
82
|
+
setColumnOrder(prev => {
|
|
83
|
+
const newOrder = [...prev];
|
|
84
|
+
const [removed] = newOrder.splice(fromIndex, 1);
|
|
85
|
+
newOrder.splice(toIndex, 0, removed);
|
|
86
|
+
return newOrder;
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const moveColumnById = useCallback((columnId: string, direction: 'left' | 'right') => {
|
|
91
|
+
setColumnOrder(prev => {
|
|
92
|
+
const currentIndex = prev.indexOf(columnId);
|
|
93
|
+
if (currentIndex === -1) return prev;
|
|
94
|
+
|
|
95
|
+
const newIndex = direction === 'left'
|
|
96
|
+
? Math.max(0, currentIndex - 1)
|
|
97
|
+
: Math.min(prev.length - 1, currentIndex + 1);
|
|
98
|
+
|
|
99
|
+
if (currentIndex === newIndex) return prev;
|
|
100
|
+
|
|
101
|
+
const newOrder = [...prev];
|
|
102
|
+
const [removed] = newOrder.splice(currentIndex, 1);
|
|
103
|
+
newOrder.splice(newIndex, 0, removed);
|
|
104
|
+
return newOrder;
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const resetOrder = useCallback(() => {
|
|
109
|
+
setColumnOrder(defaultOrder);
|
|
110
|
+
}, [defaultOrder]);
|
|
111
|
+
|
|
112
|
+
const getOrderedColumns = useCallback(<T extends { id: string }>(columns: T[]): T[] => {
|
|
113
|
+
const columnMap = new Map(columns.map(col => [col.id, col]));
|
|
114
|
+
return columnOrder
|
|
115
|
+
.map(id => columnMap.get(id))
|
|
116
|
+
.filter((col): col is T => col !== undefined);
|
|
117
|
+
}, [columnOrder]);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
columnOrder,
|
|
121
|
+
moveColumn,
|
|
122
|
+
moveColumnById,
|
|
123
|
+
resetOrder,
|
|
124
|
+
getOrderedColumns,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Drag and drop helpers for column reordering
|
|
130
|
+
*/
|
|
131
|
+
export interface DragState {
|
|
132
|
+
isDragging: boolean;
|
|
133
|
+
draggedId: string | null;
|
|
134
|
+
dropIndex: number | null; // Index where the column will be inserted
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function useColumnDragDrop(
|
|
138
|
+
columnOrder: string[],
|
|
139
|
+
moveColumn: (from: number, to: number) => void,
|
|
140
|
+
lockedColumns: string[] = []
|
|
141
|
+
) {
|
|
142
|
+
const [dragState, setDragState] = useState<DragState>({
|
|
143
|
+
isDragging: false,
|
|
144
|
+
draggedId: null,
|
|
145
|
+
dropIndex: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const handleDragStart = useCallback((columnId: string) => {
|
|
149
|
+
if (lockedColumns.includes(columnId)) return;
|
|
150
|
+
setDragState({
|
|
151
|
+
isDragging: true,
|
|
152
|
+
draggedId: columnId,
|
|
153
|
+
dropIndex: null,
|
|
154
|
+
});
|
|
155
|
+
}, [lockedColumns]);
|
|
156
|
+
|
|
157
|
+
const handleDragOver = useCallback((columnId: string, e: React.DragEvent) => {
|
|
158
|
+
if (lockedColumns.includes(columnId)) return;
|
|
159
|
+
|
|
160
|
+
const targetIndex = columnOrder.indexOf(columnId);
|
|
161
|
+
if (targetIndex === -1) return;
|
|
162
|
+
|
|
163
|
+
// Determine drop index based on mouse position relative to column center
|
|
164
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
165
|
+
const midpoint = rect.left + rect.width / 2;
|
|
166
|
+
const dropIndex = e.clientX < midpoint ? targetIndex : targetIndex + 1;
|
|
167
|
+
|
|
168
|
+
setDragState(prev => ({
|
|
169
|
+
...prev,
|
|
170
|
+
dropIndex,
|
|
171
|
+
}));
|
|
172
|
+
}, [lockedColumns, columnOrder]);
|
|
173
|
+
|
|
174
|
+
const handleDrop = useCallback(() => {
|
|
175
|
+
if (!dragState.draggedId || dragState.dropIndex === null) {
|
|
176
|
+
setDragState({ isDragging: false, draggedId: null, dropIndex: null });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fromIndex = columnOrder.indexOf(dragState.draggedId);
|
|
181
|
+
let toIndex = dragState.dropIndex;
|
|
182
|
+
|
|
183
|
+
// Adjust if moving from before the drop position
|
|
184
|
+
if (fromIndex < toIndex) {
|
|
185
|
+
toIndex = toIndex - 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
|
189
|
+
moveColumn(fromIndex, toIndex);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setDragState({ isDragging: false, draggedId: null, dropIndex: null });
|
|
193
|
+
}, [dragState.draggedId, dragState.dropIndex, columnOrder, moveColumn]);
|
|
194
|
+
|
|
195
|
+
const handleDragEnd = useCallback(() => {
|
|
196
|
+
setDragState({ isDragging: false, draggedId: null, dropIndex: null });
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
const getDragHandleProps = useCallback((columnId: string) => ({
|
|
200
|
+
draggable: !lockedColumns.includes(columnId),
|
|
201
|
+
onDragStart: (e: React.DragEvent) => {
|
|
202
|
+
if (lockedColumns.includes(columnId)) {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
207
|
+
handleDragStart(columnId);
|
|
208
|
+
},
|
|
209
|
+
onDragOver: (e: React.DragEvent) => {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
e.dataTransfer.dropEffect = 'move';
|
|
212
|
+
handleDragOver(columnId, e);
|
|
213
|
+
},
|
|
214
|
+
onDrop: (e: React.DragEvent) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
handleDrop();
|
|
217
|
+
},
|
|
218
|
+
onDragEnd: handleDragEnd,
|
|
219
|
+
}), [lockedColumns, handleDragStart, handleDragOver, handleDrop, handleDragEnd]);
|
|
220
|
+
|
|
221
|
+
// Helper to check if drop indicator should show on the left of a column
|
|
222
|
+
const showDropIndicator = useCallback((columnId: string) => {
|
|
223
|
+
if (!dragState.isDragging || dragState.dropIndex === null) return false;
|
|
224
|
+
const columnIndex = columnOrder.indexOf(columnId);
|
|
225
|
+
return columnIndex === dragState.dropIndex;
|
|
226
|
+
}, [dragState.isDragging, dragState.dropIndex, columnOrder]);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
dragState,
|
|
230
|
+
getDragHandleProps,
|
|
231
|
+
showDropIndicator,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ColumnConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
defaultVisible?: boolean;
|
|
7
|
+
locked?: boolean; // If true, column cannot be hidden
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ColumnVisibilityState {
|
|
11
|
+
[key: string]: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UseColumnVisibilityOptions {
|
|
15
|
+
columns: ColumnConfig[];
|
|
16
|
+
storageKey: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UseColumnVisibilityReturn {
|
|
20
|
+
visibleColumns: ColumnVisibilityState;
|
|
21
|
+
isColumnVisible: (columnId: string) => boolean;
|
|
22
|
+
toggleColumn: (columnId: string) => void;
|
|
23
|
+
showAllColumns: () => void;
|
|
24
|
+
hideAllColumns: () => void;
|
|
25
|
+
columns: ColumnConfig[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useColumnVisibility({
|
|
29
|
+
columns,
|
|
30
|
+
storageKey,
|
|
31
|
+
}: UseColumnVisibilityOptions): UseColumnVisibilityReturn {
|
|
32
|
+
const fullStorageKey = `column-visibility-${storageKey}`;
|
|
33
|
+
|
|
34
|
+
// Initialize state from localStorage or defaults (with SSR guard)
|
|
35
|
+
const [visibleColumns, setVisibleColumns] = useState<ColumnVisibilityState>(() => {
|
|
36
|
+
// Default state
|
|
37
|
+
const defaults: ColumnVisibilityState = {};
|
|
38
|
+
columns.forEach(col => {
|
|
39
|
+
defaults[col.id] = col.defaultVisible !== false;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// SSR guard - localStorage is not available on server
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return defaults;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const stored = localStorage.getItem(fullStorageKey);
|
|
49
|
+
if (stored) {
|
|
50
|
+
const parsed = JSON.parse(stored);
|
|
51
|
+
// Merge with defaults to handle new columns
|
|
52
|
+
const merged: ColumnVisibilityState = {};
|
|
53
|
+
columns.forEach(col => {
|
|
54
|
+
if (col.locked) {
|
|
55
|
+
merged[col.id] = true;
|
|
56
|
+
} else if (parsed[col.id] !== undefined) {
|
|
57
|
+
merged[col.id] = parsed[col.id];
|
|
58
|
+
} else {
|
|
59
|
+
merged[col.id] = col.defaultVisible !== false;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return merged;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Error loading column visibility state:', e);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return defaults;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Persist to localStorage (with SSR guard)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (typeof window === 'undefined') return;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
localStorage.setItem(fullStorageKey, JSON.stringify(visibleColumns));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error('Error saving column visibility state:', e);
|
|
79
|
+
}
|
|
80
|
+
}, [visibleColumns, fullStorageKey]);
|
|
81
|
+
|
|
82
|
+
const isColumnVisible = useCallback(
|
|
83
|
+
(columnId: string): boolean => {
|
|
84
|
+
const col = columns.find(c => c.id === columnId);
|
|
85
|
+
if (col?.locked) return true;
|
|
86
|
+
return visibleColumns[columnId] !== false;
|
|
87
|
+
},
|
|
88
|
+
[visibleColumns, columns]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const toggleColumn = useCallback(
|
|
92
|
+
(columnId: string) => {
|
|
93
|
+
const col = columns.find(c => c.id === columnId);
|
|
94
|
+
if (col?.locked) return; // Cannot toggle locked columns
|
|
95
|
+
|
|
96
|
+
setVisibleColumns(prev => ({
|
|
97
|
+
...prev,
|
|
98
|
+
[columnId]: !prev[columnId],
|
|
99
|
+
}));
|
|
100
|
+
},
|
|
101
|
+
[columns]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const showAllColumns = useCallback(() => {
|
|
105
|
+
const allVisible: ColumnVisibilityState = {};
|
|
106
|
+
columns.forEach(col => {
|
|
107
|
+
allVisible[col.id] = true;
|
|
108
|
+
});
|
|
109
|
+
setVisibleColumns(allVisible);
|
|
110
|
+
}, [columns]);
|
|
111
|
+
|
|
112
|
+
const hideAllColumns = useCallback(() => {
|
|
113
|
+
const onlyLocked: ColumnVisibilityState = {};
|
|
114
|
+
columns.forEach(col => {
|
|
115
|
+
onlyLocked[col.id] = col.locked === true;
|
|
116
|
+
});
|
|
117
|
+
setVisibleColumns(onlyLocked);
|
|
118
|
+
}, [columns]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
visibleColumns,
|
|
122
|
+
isColumnVisible,
|
|
123
|
+
toggleColumn,
|
|
124
|
+
showAllColumns,
|
|
125
|
+
hideAllColumns,
|
|
126
|
+
columns,
|
|
127
|
+
};
|
|
128
|
+
}
|