@superdangerous/app-framework 4.9.2 → 4.15.0

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.
Files changed (54) hide show
  1. package/README.md +8 -2
  2. package/dist/api/logsRouter.d.ts +4 -1
  3. package/dist/api/logsRouter.d.ts.map +1 -1
  4. package/dist/api/logsRouter.js +100 -118
  5. package/dist/api/logsRouter.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/middleware/validation.d.ts +48 -43
  11. package/dist/middleware/validation.d.ts.map +1 -1
  12. package/dist/middleware/validation.js +48 -43
  13. package/dist/middleware/validation.js.map +1 -1
  14. package/dist/services/emailService.d.ts +146 -0
  15. package/dist/services/emailService.d.ts.map +1 -0
  16. package/dist/services/emailService.js +649 -0
  17. package/dist/services/emailService.js.map +1 -0
  18. package/dist/services/index.d.ts +2 -0
  19. package/dist/services/index.d.ts.map +1 -1
  20. package/dist/services/index.js +2 -0
  21. package/dist/services/index.js.map +1 -1
  22. package/dist/services/websocketServer.d.ts +7 -4
  23. package/dist/services/websocketServer.d.ts.map +1 -1
  24. package/dist/services/websocketServer.js +22 -16
  25. package/dist/services/websocketServer.js.map +1 -1
  26. package/dist/types/index.d.ts +7 -8
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/package.json +11 -2
  29. package/src/api/logsRouter.ts +119 -138
  30. package/src/index.ts +14 -0
  31. package/src/middleware/validation.ts +82 -90
  32. package/src/services/emailService.ts +812 -0
  33. package/src/services/index.ts +14 -0
  34. package/src/services/websocketServer.ts +37 -23
  35. package/src/types/index.ts +7 -8
  36. package/ui/data-table/components/BatchActionsBar.tsx +53 -0
  37. package/ui/data-table/components/ColumnVisibility.tsx +111 -0
  38. package/ui/data-table/components/DataTablePage.tsx +238 -0
  39. package/ui/data-table/components/Pagination.tsx +203 -0
  40. package/ui/data-table/components/PaginationControls.tsx +122 -0
  41. package/ui/data-table/components/TableFilters.tsx +139 -0
  42. package/ui/data-table/components/index.ts +27 -0
  43. package/ui/data-table/hooks/index.ts +17 -0
  44. package/ui/data-table/hooks/useColumnOrder.ts +233 -0
  45. package/ui/data-table/hooks/useColumnVisibility.ts +128 -0
  46. package/ui/data-table/hooks/usePagination.ts +160 -0
  47. package/ui/data-table/hooks/useResizableColumns.ts +280 -0
  48. package/ui/data-table/index.ts +74 -0
  49. package/ui/dist/index.d.mts +207 -5
  50. package/ui/dist/index.d.ts +207 -5
  51. package/ui/dist/index.js +36 -43
  52. package/ui/dist/index.js.map +1 -1
  53. package/ui/dist/index.mjs +36 -43
  54. package/ui/dist/index.mjs.map +1 -1
@@ -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
+ }
@@ -0,0 +1,160 @@
1
+ import { useState, useMemo, useCallback, useEffect } from 'react';
2
+
3
+ interface UsePaginationOptions<T> {
4
+ data: T[];
5
+ pageSize?: number;
6
+ storageKey?: string;
7
+ }
8
+
9
+ interface PaginationState {
10
+ page: number;
11
+ pageSize: number;
12
+ }
13
+
14
+ interface UsePaginationResult<T> {
15
+ // Paginated data
16
+ paginatedData: T[];
17
+
18
+ // Current state
19
+ page: number;
20
+ pageSize: number;
21
+ totalPages: number;
22
+ totalItems: number;
23
+
24
+ // Navigation
25
+ setPage: (page: number) => void;
26
+ setPageSize: (size: number) => void;
27
+ nextPage: () => void;
28
+ prevPage: () => void;
29
+ firstPage: () => void;
30
+ lastPage: () => void;
31
+
32
+ // Info
33
+ startIndex: number;
34
+ endIndex: number;
35
+ canGoNext: boolean;
36
+ canGoPrev: boolean;
37
+
38
+ // Page size options
39
+ pageSizeOptions: number[];
40
+ }
41
+
42
+ const DEFAULT_PAGE_SIZE = 25;
43
+ const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
44
+
45
+ export function usePagination<T>({
46
+ data,
47
+ pageSize: initialPageSize = DEFAULT_PAGE_SIZE,
48
+ storageKey,
49
+ }: UsePaginationOptions<T>): UsePaginationResult<T> {
50
+ // Initialize state from localStorage if available
51
+ const [state, setState] = useState<PaginationState>(() => {
52
+ if (storageKey) {
53
+ try {
54
+ const stored = localStorage.getItem(`pagination-${storageKey}`);
55
+ if (stored) {
56
+ const parsed = JSON.parse(stored);
57
+ return {
58
+ page: 1, // Always start on first page
59
+ pageSize: parsed.pageSize || initialPageSize,
60
+ };
61
+ }
62
+ } catch {
63
+ // Ignore localStorage errors
64
+ }
65
+ }
66
+ return {
67
+ page: 1,
68
+ pageSize: initialPageSize,
69
+ };
70
+ });
71
+
72
+ const { page, pageSize } = state;
73
+
74
+ // Calculate total pages
75
+ const totalPages = useMemo(
76
+ () => Math.max(1, Math.ceil(data.length / pageSize)),
77
+ [data.length, pageSize]
78
+ );
79
+
80
+ // Reset to first page if current page is out of bounds
81
+ useEffect(() => {
82
+ if (page > totalPages) {
83
+ setState((prev) => ({ ...prev, page: Math.max(1, totalPages) }));
84
+ }
85
+ }, [totalPages, page]);
86
+
87
+ // Persist pageSize to localStorage
88
+ useEffect(() => {
89
+ if (storageKey) {
90
+ try {
91
+ localStorage.setItem(
92
+ `pagination-${storageKey}`,
93
+ JSON.stringify({ pageSize })
94
+ );
95
+ } catch {
96
+ // Ignore localStorage errors
97
+ }
98
+ }
99
+ }, [pageSize, storageKey]);
100
+
101
+ // Calculate start and end indices
102
+ const startIndex = (page - 1) * pageSize;
103
+ const endIndex = Math.min(startIndex + pageSize, data.length);
104
+
105
+ // Get paginated data
106
+ const paginatedData = useMemo(
107
+ () => data.slice(startIndex, endIndex),
108
+ [data, startIndex, endIndex]
109
+ );
110
+
111
+ // Navigation functions
112
+ const setPage = useCallback((newPage: number) => {
113
+ setState((prev) => ({
114
+ ...prev,
115
+ page: Math.max(1, Math.min(newPage, totalPages)),
116
+ }));
117
+ }, [totalPages]);
118
+
119
+ const setPageSize = useCallback((newSize: number) => {
120
+ setState({
121
+ page: 1, // Reset to first page when changing page size
122
+ pageSize: newSize,
123
+ });
124
+ }, []);
125
+
126
+ const nextPage = useCallback(() => {
127
+ setPage(page + 1);
128
+ }, [page, setPage]);
129
+
130
+ const prevPage = useCallback(() => {
131
+ setPage(page - 1);
132
+ }, [page, setPage]);
133
+
134
+ const firstPage = useCallback(() => {
135
+ setPage(1);
136
+ }, [setPage]);
137
+
138
+ const lastPage = useCallback(() => {
139
+ setPage(totalPages);
140
+ }, [totalPages, setPage]);
141
+
142
+ return {
143
+ paginatedData,
144
+ page,
145
+ pageSize,
146
+ totalPages,
147
+ totalItems: data.length,
148
+ setPage,
149
+ setPageSize,
150
+ nextPage,
151
+ prevPage,
152
+ firstPage,
153
+ lastPage,
154
+ startIndex: startIndex + 1, // 1-indexed for display
155
+ endIndex,
156
+ canGoNext: page < totalPages,
157
+ canGoPrev: page > 1,
158
+ pageSizeOptions: PAGE_SIZE_OPTIONS,
159
+ };
160
+ }