@jmruthers/pace-core 0.5.107 → 0.5.109

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 (184) hide show
  1. package/CHANGELOG.md +75 -177
  2. package/dist/{AuthService-1D2ifNfa.d.ts → AuthService-DrHrvXNZ.d.ts} +8 -1
  3. package/dist/{DataTable-H2WIR2DN.js → DataTable-5HITILXS.js} +7 -7
  4. package/dist/{PublicLoadingSpinner-48ewSMKK.d.ts → PublicLoadingSpinner-DgDWTFqn.d.ts} +4 -2
  5. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js → UnifiedAuthProvider-A7I23UCN.js} +3 -3
  6. package/dist/{api-KG4A2X7P.js → api-5I3E47G2.js} +2 -2
  7. package/dist/{chunk-DMNMZKWS.js → chunk-2W4WKJVF.js} +4 -4
  8. package/dist/{chunk-MOMYOQMC.js → chunk-3TKTL5AZ.js} +13 -13
  9. package/dist/{chunk-X4FRXJV6.js → chunk-AUXS7XSO.js} +57 -6
  10. package/dist/{chunk-X4FRXJV6.js.map → chunk-AUXS7XSO.js.map} +1 -1
  11. package/dist/{chunk-LT6RKRA7.js → chunk-D6MEKC27.js} +2 -2
  12. package/dist/{chunk-KBG34SVL.js → chunk-EYSXQ756.js} +2 -2
  13. package/dist/{chunk-ZXY5NTJB.js → chunk-EZ64QG2I.js} +2 -2
  14. package/dist/{chunk-S63MFSY6.js → chunk-F6TSYCKP.js} +4 -2
  15. package/dist/{chunk-S63MFSY6.js.map → chunk-F6TSYCKP.js.map} +1 -1
  16. package/dist/chunk-GZRXOUBE.js +176 -0
  17. package/dist/chunk-GZRXOUBE.js.map +1 -0
  18. package/dist/{chunk-EWKCROSF.js → chunk-P72NKAT5.js} +84 -28
  19. package/dist/chunk-P72NKAT5.js.map +1 -0
  20. package/dist/{chunk-VJ7MPS2K.js → chunk-S4D3Z723.js} +6 -6
  21. package/dist/{chunk-5JJCXTVE.js → chunk-UW2DE6JX.js} +108 -86
  22. package/dist/{chunk-5JJCXTVE.js.map → chunk-UW2DE6JX.js.map} +1 -1
  23. package/dist/{chunk-QDDUU625.js → chunk-WWNOVFDC.js} +4 -4
  24. package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
  25. package/dist/components.d.ts +1 -1
  26. package/dist/components.js +9 -9
  27. package/dist/{database-BXAfr2Y_.d.ts → database-C6jy7EOu.d.ts} +21 -9
  28. package/dist/{formatting-BiEv5oEk.d.ts → formatting-B1jSqgl-.d.ts} +16 -1
  29. package/dist/hooks.d.ts +2 -2
  30. package/dist/hooks.js +7 -7
  31. package/dist/index.d.ts +7 -7
  32. package/dist/index.js +16 -14
  33. package/dist/index.js.map +1 -1
  34. package/dist/providers.d.ts +4 -3
  35. package/dist/providers.js +2 -2
  36. package/dist/rbac/index.d.ts +1 -1
  37. package/dist/rbac/index.js +8 -8
  38. package/dist/types.d.ts +2 -2
  39. package/dist/{usePublicRouteParams-CnM-IK2I.d.ts → usePublicRouteParams-BdF8bZgs.d.ts} +1 -1
  40. package/dist/utils.d.ts +2 -15
  41. package/dist/utils.js +4 -145
  42. package/dist/utils.js.map +1 -1
  43. package/dist/validation.d.ts +1 -1
  44. package/docs/api/classes/ColumnFactory.md +1 -1
  45. package/docs/api/classes/ErrorBoundary.md +1 -1
  46. package/docs/api/classes/InvalidScopeError.md +1 -1
  47. package/docs/api/classes/MissingUserContextError.md +1 -1
  48. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  49. package/docs/api/classes/PermissionDeniedError.md +1 -1
  50. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  51. package/docs/api/classes/RBACAuditManager.md +1 -1
  52. package/docs/api/classes/RBACCache.md +1 -1
  53. package/docs/api/classes/RBACEngine.md +1 -1
  54. package/docs/api/classes/RBACError.md +1 -1
  55. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  56. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  57. package/docs/api/classes/StorageUtils.md +1 -1
  58. package/docs/api/enums/FileCategory.md +1 -1
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  65. package/docs/api/interfaces/DataRecord.md +1 -1
  66. package/docs/api/interfaces/DataTableAction.md +1 -1
  67. package/docs/api/interfaces/DataTableColumn.md +3 -3
  68. package/docs/api/interfaces/DataTableProps.md +1 -1
  69. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  70. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  71. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  73. package/docs/api/interfaces/FileMetadata.md +1 -1
  74. package/docs/api/interfaces/FileReference.md +1 -1
  75. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  76. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  77. package/docs/api/interfaces/FileUploadProps.md +1 -1
  78. package/docs/api/interfaces/FooterProps.md +1 -1
  79. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  80. package/docs/api/interfaces/InputProps.md +1 -1
  81. package/docs/api/interfaces/LabelProps.md +1 -1
  82. package/docs/api/interfaces/LoginFormProps.md +1 -1
  83. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  84. package/docs/api/interfaces/NavigationContextType.md +1 -1
  85. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  86. package/docs/api/interfaces/NavigationItem.md +1 -1
  87. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  88. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  89. package/docs/api/interfaces/Organisation.md +1 -1
  90. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  91. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  92. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  93. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  94. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  95. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  96. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  97. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  98. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  99. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  100. package/docs/api/interfaces/PaletteData.md +1 -1
  101. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  102. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  103. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  104. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  105. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  106. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  107. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  108. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  109. package/docs/api/interfaces/RBACConfig.md +1 -1
  110. package/docs/api/interfaces/RBACLogger.md +1 -1
  111. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  112. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  113. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  114. package/docs/api/interfaces/RouteConfig.md +1 -1
  115. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  116. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  117. package/docs/api/interfaces/StorageConfig.md +1 -1
  118. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  119. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  120. package/docs/api/interfaces/StorageListOptions.md +1 -1
  121. package/docs/api/interfaces/StorageListResult.md +1 -1
  122. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  123. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  124. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  125. package/docs/api/interfaces/StyleImport.md +1 -1
  126. package/docs/api/interfaces/SwitchProps.md +1 -1
  127. package/docs/api/interfaces/ToastActionElement.md +1 -1
  128. package/docs/api/interfaces/ToastProps.md +1 -1
  129. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  130. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  131. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  132. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  136. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  137. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  138. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  139. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  140. package/docs/api/interfaces/UserEventAccess.md +1 -1
  141. package/docs/api/interfaces/UserMenuProps.md +1 -1
  142. package/docs/api/interfaces/UserProfile.md +1 -1
  143. package/docs/api/modules.md +42 -6
  144. package/docs/api-reference/hooks.md +53 -0
  145. package/docs/api-reference/providers.md +60 -0
  146. package/docs/core-concepts/authentication.md +2 -0
  147. package/docs/implementation-guides/authentication.md +1 -0
  148. package/docs/security/README.md +59 -0
  149. package/package.json +1 -1
  150. package/src/components/DataTable/components/ColumnFilter.tsx +2 -1
  151. package/src/components/DataTable/components/EditableRow.tsx +7 -2
  152. package/src/components/DataTable/components/FilterRow.tsx +22 -11
  153. package/src/components/DataTable/components/PaginationControls.tsx +1 -1
  154. package/src/components/DataTable/components/UnifiedTableBody.tsx +39 -10
  155. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
  156. package/src/components/PaceAppLayout/PaceAppLayout.tsx +126 -25
  157. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +2 -1
  158. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +9 -9
  159. package/src/index.ts +3 -0
  160. package/src/providers/services/AuthServiceProvider.tsx +4 -3
  161. package/src/providers/services/UnifiedAuthProvider.tsx +1 -1
  162. package/src/rbac/engine.ts +2 -0
  163. package/src/services/AuthService.ts +79 -1
  164. package/src/services/__tests__/AuthService.test.ts +184 -0
  165. package/src/types/database.ts +21 -9
  166. package/src/types/rbac-functions.ts +2 -1
  167. package/src/utils/__tests__/sessionTracking.unit.test.ts +6 -171
  168. package/src/utils/sessionTracking.ts +7 -81
  169. package/dist/chunk-EWKCROSF.js.map +0 -1
  170. package/dist/chunk-NFPV7MRN.js +0 -94
  171. package/dist/chunk-NFPV7MRN.js.map +0 -1
  172. package/src/providers/AuthProvider.simplified.tsx +0 -974
  173. package/dist/{DataTable-H2WIR2DN.js.map → DataTable-5HITILXS.js.map} +0 -0
  174. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js.map → UnifiedAuthProvider-A7I23UCN.js.map} +0 -0
  175. package/dist/{api-KG4A2X7P.js.map → api-5I3E47G2.js.map} +0 -0
  176. package/dist/{chunk-DMNMZKWS.js.map → chunk-2W4WKJVF.js.map} +0 -0
  177. package/dist/{chunk-MOMYOQMC.js.map → chunk-3TKTL5AZ.js.map} +0 -0
  178. package/dist/{chunk-LT6RKRA7.js.map → chunk-D6MEKC27.js.map} +0 -0
  179. package/dist/{chunk-KBG34SVL.js.map → chunk-EYSXQ756.js.map} +0 -0
  180. package/dist/{chunk-ZXY5NTJB.js.map → chunk-EZ64QG2I.js.map} +0 -0
  181. package/dist/{chunk-VJ7MPS2K.js.map → chunk-S4D3Z723.js.map} +0 -0
  182. package/dist/{chunk-QDDUU625.js.map → chunk-WWNOVFDC.js.map} +0 -0
  183. package/dist/{chunk-GVRSXXAA.js.map → chunk-YFMENCR4.js.map} +0 -0
  184. package/dist/{validation-D8VcbTzC.d.ts → validation-DnhrNMju.d.ts} +2 -2
@@ -76,6 +76,59 @@ function App() {
76
76
  }
77
77
  ```
78
78
 
79
+ > **Note**: Login and logout tracking is automatically handled by `UnifiedAuthProvider`. No manual intervention is required.
80
+
81
+ ### useSessionTracking
82
+
83
+ Utility hook for manual session tracking of event switches and session expiration. **Note**: Login and logout are automatically tracked by `UnifiedAuthProvider`, so those methods are not available here.
84
+
85
+ ```typescript
86
+ function useSessionTracking(
87
+ supabaseClient: SupabaseClient,
88
+ appName?: string
89
+ ): {
90
+ trackEventSwitch: (eventId: string) => Promise<void>;
91
+ trackSessionExpired: () => Promise<void>;
92
+ }
93
+ ```
94
+
95
+ #### Usage
96
+
97
+ ```tsx
98
+ import { useSessionTracking } from '@jmruthers/pace-core';
99
+ import { supabase } from './lib/supabase';
100
+
101
+ function MyComponent() {
102
+ const { trackEventSwitch, trackSessionExpired } = useSessionTracking(
103
+ supabase,
104
+ 'MY_APP'
105
+ );
106
+
107
+ const handleEventSwitch = async (eventId: string) => {
108
+ await trackEventSwitch(eventId);
109
+ // Event switch logic...
110
+ };
111
+
112
+ const handleSessionExpiration = async () => {
113
+ await trackSessionExpired();
114
+ // Session expiration logic...
115
+ };
116
+
117
+ return (
118
+ // Component JSX
119
+ );
120
+ }
121
+ ```
122
+
123
+ #### Methods
124
+
125
+ | Method | Description |
126
+ |--------|-------------|
127
+ | `trackEventSwitch(eventId)` | Track when a user switches to a different event. |
128
+ | `trackSessionExpired()` | Track when a session expires. |
129
+
130
+ > **Automatic Tracking**: When using `UnifiedAuthProvider`, login and logout events are **automatically tracked**. You only need to use this hook for event switches or session expirations, which are not automatically tracked.
131
+
79
132
 
80
133
  ## Event Management Hooks
81
134
 
@@ -206,6 +206,66 @@ The inactivity tracker monitors the following user interactions:
206
206
  - **Automatic cleanup**: All timers and listeners are properly cleaned up
207
207
  - **Error handling**: Graceful fallback if localStorage or BroadcastChannel fail
208
208
 
209
+ #### Automatic Login History Tracking
210
+
211
+ The `UnifiedAuthProvider` automatically tracks all user logins for security auditing and compliance:
212
+
213
+ - **Automatic Tracking** - No manual intervention required, tracking happens automatically on login/logout
214
+ - **Complete Audit Trail** - Records user ID, email, timestamp, IP address, user agent, and application context
215
+ - **Database Storage** - All login events are stored in `rbac_user_login_history` table
216
+ - **Application Context** - Tracks which application the user logged into (when `appName` is provided)
217
+ - **Non-Blocking** - Tracking failures don't prevent authentication from succeeding
218
+ - **Privacy Compliant** - Users can only view their own login history (RLS enforced)
219
+
220
+ Login history is tracked automatically when you use `UnifiedAuthProvider`. No additional configuration is required:
221
+
222
+ ```tsx
223
+ <UnifiedAuthProvider
224
+ supabaseClient={supabase}
225
+ appName="MY_APP" // Enables app-specific tracking in login history
226
+ // ... other props
227
+ >
228
+ <AppContent />
229
+ </UnifiedAuthProvider>
230
+ ```
231
+
232
+ **What Gets Tracked:**
233
+
234
+ - User ID and email
235
+ - Login timestamp
236
+ - Session ID
237
+ - IP address (if available)
238
+ - User agent string
239
+ - Application ID (if `appName` is provided)
240
+ - Organisation ID
241
+ - Event ID (if applicable)
242
+
243
+ **Querying Login History:**
244
+
245
+ Login history can be queried directly from the database using RLS-protected queries:
246
+
247
+ ```sql
248
+ -- Get user's login history
249
+ SELECT
250
+ login_timestamp,
251
+ email,
252
+ ip_address,
253
+ user_agent,
254
+ app_id,
255
+ event_id
256
+ FROM rbac_user_login_history
257
+ WHERE user_id = auth.uid()
258
+ ORDER BY login_timestamp DESC
259
+ LIMIT 100;
260
+ ```
261
+
262
+ **Security Notes:**
263
+
264
+ - Login history insertion uses `SECURITY DEFINER` functions (bypasses RLS)
265
+ - RLS policies ensure users can only view their own login history
266
+ - Failed tracking attempts are logged as warnings but don't break authentication
267
+ - All tracking is asynchronous and non-blocking
268
+
209
269
  ## OrganisationProvider
210
270
 
211
271
  Manages multi-tenant organisation context and user organisation memberships. **Automatically sets database organisation context** to ensure RLS policies work correctly.
@@ -77,6 +77,7 @@ sequenceDiagram
77
77
  - **Persistent State** - Authentication state persists across page reloads
78
78
  - **Multi-Tab Support** - Authentication state synchronized across tabs
79
79
  - **Graceful Degradation** - Handles network issues and token expiry
80
+ - **Automatic Login History** - User login events are automatically tracked in `rbac_user_login_history` table
80
81
 
81
82
  ### Security Features
82
83
 
@@ -84,6 +85,7 @@ sequenceDiagram
84
85
  - **JWT Tokens** - Secure, stateless authentication
85
86
  - **CSRF Protection** - Cross-site request forgery prevention
86
87
  - **Audit Logging** - Complete action tracking for compliance
88
+ - **Login History Tracking** - Automatic tracking of all user logins with timestamps, IP addresses, user agents, and application context
87
89
 
88
90
  ## Multi-Tenancy
89
91
 
@@ -23,6 +23,7 @@ PACE Core provides a comprehensive authentication system built on Supabase that
23
23
  - **🔒 Session Persistence** - Secure session management with auto-refresh
24
24
  - **🎯 Permission Integration** - Built-in RBAC integration
25
25
  - **📊 Debug Support** - Comprehensive debugging and monitoring
26
+ - **📝 Automatic Login History** - All user logins automatically tracked for audit trails
26
27
 
27
28
  ## Quick Start
28
29
 
@@ -16,6 +16,7 @@ PACE Core is designed with security as a first-class concern, providing:
16
16
  - **Comprehensive RBAC system** with fine-grained permissions
17
17
  - **Secure data access** with row-level security
18
18
  - **Audit logging** for compliance and monitoring
19
+ - **Automatic login history tracking** for security audits and compliance
19
20
  - **Input validation** and sanitization
20
21
  - **XSS protection** and secure coding practices
21
22
  - **Auto-logout on inactivity** for enhanced security
@@ -59,6 +60,64 @@ Sessions are automatically managed by PACE Core:
59
60
  - **Session validation** on every request
60
61
  - **Automatic logout** on token expiration
61
62
  - **Inactivity auto-logout** after 30 minutes of inactivity (configurable)
63
+ - **Automatic login history tracking** - All user logins are automatically recorded
64
+
65
+ ### 2.1 Login History Tracking
66
+
67
+ PACE Core **automatically tracks all user logins** for security auditing and compliance - no manual intervention required.
68
+
69
+ - **Fully Automatic** - Simply use `UnifiedAuthProvider` with `appName` prop - tracking happens automatically
70
+ - **No Configuration Needed** - No calls to tracking functions, no setup code, no manual intervention
71
+ - **Complete Audit Trail** - Records user ID, email, timestamp, IP address, user agent, and application context
72
+ - **Database Storage** - All login events are stored in `rbac_user_login_history` table automatically
73
+ - **Application Context** - Tracks which application the user logged into (when `appName` is provided)
74
+ - **Non-Blocking** - Tracking failures don't prevent authentication from succeeding
75
+ - **Privacy Compliant** - Users can only view their own login history (RLS enforced)
76
+
77
+ **How It Works:**
78
+
79
+ Login history tracking is **completely automatic** when you use `UnifiedAuthProvider`. Simply provide the `appName` prop (which is already required) and tracking happens automatically:
80
+
81
+ ```tsx
82
+ import { UnifiedAuthProvider } from '@jmruthers/pace-core';
83
+
84
+ function App() {
85
+ return (
86
+ <UnifiedAuthProvider
87
+ supabaseClient={supabaseClient}
88
+ appName="MY_APP" // Optional: Enables app-specific tracking
89
+ // ... other props
90
+ >
91
+ <YourApp />
92
+ </UnifiedAuthProvider>
93
+ );
94
+ }
95
+ ```
96
+
97
+ **Querying Login History:**
98
+
99
+ Login history can be queried directly from the database:
100
+
101
+ ```sql
102
+ -- Get user's login history
103
+ SELECT
104
+ login_timestamp,
105
+ email,
106
+ ip_address,
107
+ user_agent,
108
+ app_id
109
+ FROM rbac_user_login_history
110
+ WHERE user_id = auth.uid()
111
+ ORDER BY login_timestamp DESC
112
+ LIMIT 100;
113
+ ```
114
+
115
+ **Security Notes:**
116
+
117
+ - Login history insertion uses `SECURITY DEFINER` functions (bypasses RLS)
118
+ - RLS policies ensure users can only view their own login history
119
+ - Failed tracking attempts are logged but don't break authentication
120
+ - All tracking is asynchronous and non-blocking
62
121
 
63
122
  ```tsx
64
123
  import { useUnifiedAuth } from '@jmruthers/pace-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.107",
3
+ "version": "0.5.109",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -62,13 +62,14 @@ export function ColumnFilter({
62
62
  );
63
63
 
64
64
  case 'number':
65
+ // Always hide spinner arrows for number filter inputs (cleaner UX)
65
66
  return (
66
67
  <Input
67
68
  type="number"
68
69
  value={columnFilterValue as string || ''}
69
70
  onChange={(e) => handleFilterChange(e.target.value ? Number(e.target.value) : undefined)}
70
71
  placeholder={placeholder || `Filter ${column.id}...`}
71
- className="h-8"
72
+ className="h-8 datatable-number-no-spinners"
72
73
  />
73
74
  );
74
75
 
@@ -44,7 +44,10 @@ function SelectEditField<TData extends DataRecord>({
44
44
  onChange: (value: CellValue) => void;
45
45
  className?: string;
46
46
  }) {
47
- const isSearchable = columnDef.selectSearchable !== false; // Default to true for better UX
47
+ // Determine if searchable - explicitly check for true to ensure visible search input appears
48
+ // When selectSearchable is true or undefined, show the visible search input box
49
+ // When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
50
+ const isSearchable = columnDef.selectSearchable !== false;
48
51
  const isCreatable = columnDef.creatable === true;
49
52
  const selectRef = React.useRef<HTMLFormElement>(null);
50
53
  const [searchTerm, setSearchTerm] = React.useState('');
@@ -137,7 +140,7 @@ function SelectEditField<TData extends DataRecord>({
137
140
  <SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
138
141
  </SelectTrigger>
139
142
  <SelectContent
140
- searchable={isSearchable}
143
+ searchable={Boolean(isSearchable)}
141
144
  searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
142
145
  maxHeight={columnDef.selectMaxHeight}
143
146
  className={columnDef.selectContentClassName}
@@ -232,6 +235,8 @@ const renderEditField = <TData extends DataRecord>(
232
235
  }
233
236
 
234
237
  if (columnDef.fieldType === 'number') {
238
+ // Hide spinner arrows by default for number, currency, and percentage fields
239
+ // Only show spinners if explicitly set to false
235
240
  const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
236
241
  return (
237
242
  <Input
@@ -12,7 +12,7 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
12
12
  const { columnFilters } = getState();
13
13
 
14
14
  // Get unique values for select filters
15
- const getColumnOptions = (columnId: string) => {
15
+ const getColumnOptions = React.useCallback((columnId: string) => {
16
16
  const column = table.getColumn(columnId);
17
17
  if (!column) return [];
18
18
 
@@ -40,44 +40,55 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
40
40
  return Array.from(uniqueValues)
41
41
  .sort()
42
42
  .map((value) => ({ value, label: value }));
43
- };
43
+ }, [table]);
44
44
 
45
45
  // Determine filter type based on column data
46
- const getFilterType = (columnId: string) => {
46
+ // IMPORTANT: Explicit filterType always takes priority - auto-detection only runs if filterType is not set
47
+ const getFilterType = React.useCallback((columnId: string) => {
47
48
  const column = table.getColumn(columnId);
48
49
  if (!column) return 'text';
49
50
 
50
51
  const columnDef = column.columnDef as any;
51
52
 
52
- // Check if column has explicit filter type configuration
53
- if (columnDef.filterType) {
54
- return columnDef.filterType;
53
+ // PRIORITY 1: Check if column has explicit filter type configuration
54
+ // This MUST be checked first and must respect any explicit value, including 'text'
55
+ // Use explicit !== undefined && !== null check to ensure 'text' is not treated as falsy
56
+ // This prevents auto-detection from overriding explicit filterType settings
57
+ const explicitFilterType = columnDef.filterType;
58
+ if (explicitFilterType !== undefined && explicitFilterType !== null && explicitFilterType !== '') {
59
+ // Explicit filterType set - return it immediately (no auto-detection)
60
+ // This ensures filterType: 'text' is always respected, even for columns with ≤10 unique values
61
+ return explicitFilterType as 'text' | 'select' | 'number' | 'date';
55
62
  }
56
63
 
57
- // Auto-detect select filter if filterSelectOptions is provided
64
+ // Only proceed with auto-detection if filterType was NOT explicitly set
65
+
66
+ // PRIORITY 2: Auto-detect select filter if filterSelectOptions is explicitly provided
58
67
  if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
59
68
  return 'select';
60
69
  }
61
70
 
62
- // Check if it's a date column
71
+ // PRIORITY 3: Check if it's a date column (by column ID pattern)
63
72
  if (columnId.toLowerCase().includes('date') || columnId.toLowerCase().includes('time')) {
64
73
  return 'date';
65
74
  }
66
75
 
67
- // Check if it's a number column
76
+ // PRIORITY 4: Check if it's a number column (by data type)
68
77
  const firstValue = table.getRowModel().rows[0]?.getValue(columnId);
69
78
  if (typeof firstValue === 'number') {
70
79
  return 'number';
71
80
  }
72
81
 
73
- // Check if it has limited unique values (good for select)
82
+ // PRIORITY 5: Auto-detect select filter if limited unique values (≤10)
83
+ // Only runs if filterType was NOT explicitly set (checked above)
74
84
  const uniqueValues = getColumnOptions(columnId);
75
85
  if (uniqueValues.length <= 10 && uniqueValues.length > 1) {
76
86
  return 'select';
77
87
  }
78
88
 
89
+ // Default to text filter
79
90
  return 'text';
80
- };
91
+ }, [table, getColumnOptions]);
81
92
 
82
93
  return (
83
94
  <tr className="border-b bg-sec-50/50">
@@ -259,7 +259,7 @@ export function EnhancedPaginationControls<TData extends DataRecord>({
259
259
  max={pageCount}
260
260
  value={jumpToPage}
261
261
  onChange={(e) => setJumpToPage(e.target.value)}
262
- className="w-16 h-6 px-2 border rounded text-xs"
262
+ className="w-16 h-6 px-2 border rounded text-xs datatable-number-no-spinners"
263
263
  placeholder="1"
264
264
  />
265
265
  <Button type="submit" size="sm" variant="outline" className="h-6 px-2 text-xs">
@@ -134,7 +134,10 @@ function SelectEditField<TData extends DataRecord>({
134
134
  placeholder?: string;
135
135
  onChange: (value: CellValue) => void;
136
136
  }) {
137
- const isSearchable = columnDef.selectSearchable !== false; // Default to true for better UX
137
+ // Determine if searchable - explicitly check for true to ensure visible search input appears
138
+ // When selectSearchable is true or undefined, show the visible search input box
139
+ // When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
140
+ const isSearchable = columnDef.selectSearchable !== false;
138
141
  const isCreatable = columnDef.creatable === true;
139
142
  const selectRef = React.useRef<HTMLFormElement>(null);
140
143
  const [searchTerm, setSearchTerm] = React.useState('');
@@ -227,7 +230,7 @@ function SelectEditField<TData extends DataRecord>({
227
230
  <SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
228
231
  </SelectTrigger>
229
232
  <SelectContent
230
- searchable={isSearchable}
233
+ searchable={Boolean(isSearchable)}
231
234
  searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
232
235
  maxHeight={columnDef.selectMaxHeight}
233
236
  className={columnDef.selectContentClassName}
@@ -312,8 +315,11 @@ const renderEditField = <TData extends DataRecord>(
312
315
  );
313
316
  }
314
317
 
315
- // Check for number type
318
+ // Check for number type (applies to number, currency, and percentage fields)
316
319
  if (columnDef.fieldType === 'number') {
320
+ // Hide spinner arrows by default for all number-related fields
321
+ // Currency and percentage columns use fieldType: 'number' with formatting in cell renderer
322
+ // Only show spinners if explicitly set to false
317
323
  const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
318
324
  return (
319
325
  <Input
@@ -879,6 +885,26 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
879
885
  }
880
886
 
881
887
  // Render edit fields for data columns
888
+ // Determine the correct key to use for creationData
889
+ // Priority: editAccessorKey > accessorKey > column.id
890
+ const columnDef = header.column.columnDef as EditableColumnDef<TData>;
891
+ const dataKey = columnDef.editAccessorKey || columnDef.accessorKey || header.column.id;
892
+
893
+ // Always render a cell to maintain alignment - renderEditField always returns something
894
+ const editField = renderEditField(
895
+ header.column,
896
+ creationData[dataKey] ?? creationData[header.column.id] ?? '',
897
+ (value) => {
898
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
899
+ onCreationDataChange({ ...creationData, ...(value as Record<string, CellValue>) });
900
+ } else {
901
+ // Use the determined dataKey for consistent data access
902
+ onCreationDataChange({ ...creationData, [dataKey]: value as CellValue });
903
+ }
904
+ },
905
+ creationData
906
+ );
907
+
882
908
  return (
883
909
  <td
884
910
  key={header.column.id}
@@ -887,13 +913,16 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
887
913
  className: "px-3 py-2"
888
914
  })}
889
915
  >
890
- {renderEditField(header.column, creationData[header.column.id], (value) => {
891
- if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
892
- onCreationDataChange({ ...creationData, ...(value as Record<string, CellValue>) });
893
- } else {
894
- onCreationDataChange({ ...creationData, [header.column.id]: value as CellValue });
895
- }
896
- }, creationData)}
916
+ {editField || (
917
+ // Fallback: render a text input if renderEditField somehow returns nothing
918
+ <Input
919
+ type="text"
920
+ value={String(creationData[dataKey] ?? creationData[header.column.id] ?? '')}
921
+ onChange={(e) => onCreationDataChange({ ...creationData, [dataKey]: e.target.value as CellValue })}
922
+ placeholder={`Enter ${columnDef.header || header.column.id}...`}
923
+ className="h-8"
924
+ />
925
+ )}
897
926
  </td>
898
927
  );
899
928
  })}
@@ -540,7 +540,7 @@ describe('PaceAppLayout Component', () => {
540
540
  eventId: 'event-123',
541
541
  appId: 'app-123',
542
542
  },
543
- permission: 'update',
543
+ permission: 'update:page.dashboard-page',
544
544
  pageId: 'dashboard-page',
545
545
  });
546
546
  }, { timeout: 3000 });
@@ -571,7 +571,7 @@ describe('PaceAppLayout Component', () => {
571
571
  eventId: 'event-123',
572
572
  appId: 'app-123',
573
573
  },
574
- permission: 'create',
574
+ permission: 'create:page.dashboard',
575
575
  pageId: 'dashboard',
576
576
  });
577
577
  }, { timeout: 3000 });