@jmruthers/pace-core 0.5.105 → 0.5.107

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 (159) hide show
  1. package/dist/{DataTable-BE0OXZKQ.d.ts → DataTable-D5cBRca8.d.ts} +1 -1
  2. package/dist/{DataTable-LWHFLTEW.js → DataTable-H2WIR2DN.js} +3 -3
  3. package/dist/{chunk-QPCAGLUS.js → chunk-4OX5PXHX.js} +5 -2
  4. package/dist/chunk-4OX5PXHX.js.map +1 -0
  5. package/dist/{chunk-75G3NZWN.js → chunk-5JJCXTVE.js} +293 -37
  6. package/dist/chunk-5JJCXTVE.js.map +1 -0
  7. package/dist/{chunk-HBGPLSA5.js → chunk-DMNMZKWS.js} +70 -24
  8. package/dist/chunk-DMNMZKWS.js.map +1 -0
  9. package/dist/{chunk-AZFPGDCJ.js → chunk-EWKCROSF.js} +133 -49
  10. package/dist/chunk-EWKCROSF.js.map +1 -0
  11. package/dist/{chunk-4BWGRQBG.js → chunk-NFPV7MRN.js} +22 -2
  12. package/dist/chunk-NFPV7MRN.js.map +1 -0
  13. package/dist/{chunk-DWYMGSGU.js → chunk-VJ7MPS2K.js} +2 -2
  14. package/dist/components.d.ts +3 -3
  15. package/dist/components.js +4 -4
  16. package/dist/{formatting-BfDeV-ja.d.ts → formatting-BiEv5oEk.d.ts} +32 -2
  17. package/dist/hooks.d.ts +2 -2
  18. package/dist/hooks.js +3 -3
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +6 -6
  21. package/dist/{types-BDg1mAGG.d.ts → types-D4TVpDa1.d.ts} +24 -1
  22. package/dist/{useToast-Bm6TnSK-.d.ts → useToast-DRah6K-g.d.ts} +5 -2
  23. package/dist/utils.d.ts +3 -3
  24. package/dist/utils.js +2 -2
  25. package/docs/api/classes/ColumnFactory.md +1 -1
  26. package/docs/api/classes/ErrorBoundary.md +1 -1
  27. package/docs/api/classes/InvalidScopeError.md +1 -1
  28. package/docs/api/classes/MissingUserContextError.md +1 -1
  29. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  30. package/docs/api/classes/PermissionDeniedError.md +1 -1
  31. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  32. package/docs/api/classes/RBACAuditManager.md +1 -1
  33. package/docs/api/classes/RBACCache.md +1 -1
  34. package/docs/api/classes/RBACEngine.md +1 -1
  35. package/docs/api/classes/RBACError.md +1 -1
  36. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  37. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  38. package/docs/api/classes/StorageUtils.md +1 -1
  39. package/docs/api/enums/FileCategory.md +1 -1
  40. package/docs/api/interfaces/AggregateConfig.md +4 -4
  41. package/docs/api/interfaces/ButtonProps.md +1 -1
  42. package/docs/api/interfaces/CardProps.md +1 -1
  43. package/docs/api/interfaces/ColorPalette.md +1 -1
  44. package/docs/api/interfaces/ColorShade.md +1 -1
  45. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  46. package/docs/api/interfaces/DataRecord.md +1 -1
  47. package/docs/api/interfaces/DataTableAction.md +18 -18
  48. package/docs/api/interfaces/DataTableColumn.md +115 -10
  49. package/docs/api/interfaces/DataTableProps.md +38 -38
  50. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  51. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  52. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  53. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  54. package/docs/api/interfaces/FileMetadata.md +1 -1
  55. package/docs/api/interfaces/FileReference.md +1 -1
  56. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  57. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  58. package/docs/api/interfaces/FileUploadProps.md +1 -1
  59. package/docs/api/interfaces/FooterProps.md +1 -1
  60. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  61. package/docs/api/interfaces/InputProps.md +1 -1
  62. package/docs/api/interfaces/LabelProps.md +1 -1
  63. package/docs/api/interfaces/LoginFormProps.md +1 -1
  64. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  65. package/docs/api/interfaces/NavigationContextType.md +1 -1
  66. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  67. package/docs/api/interfaces/NavigationItem.md +1 -1
  68. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  69. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  70. package/docs/api/interfaces/Organisation.md +1 -1
  71. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  72. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  73. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  74. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  75. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  76. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  77. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  78. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  79. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  80. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  81. package/docs/api/interfaces/PaletteData.md +1 -1
  82. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  83. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  84. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  85. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  86. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  88. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  90. package/docs/api/interfaces/RBACConfig.md +1 -1
  91. package/docs/api/interfaces/RBACLogger.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  93. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  94. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  95. package/docs/api/interfaces/RouteConfig.md +1 -1
  96. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  97. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  98. package/docs/api/interfaces/StorageConfig.md +1 -1
  99. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  100. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  101. package/docs/api/interfaces/StorageListOptions.md +1 -1
  102. package/docs/api/interfaces/StorageListResult.md +1 -1
  103. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  104. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  105. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  106. package/docs/api/interfaces/StyleImport.md +1 -1
  107. package/docs/api/interfaces/SwitchProps.md +1 -1
  108. package/docs/api/interfaces/ToastActionElement.md +1 -1
  109. package/docs/api/interfaces/ToastProps.md +1 -1
  110. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  112. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  114. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  116. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  119. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  121. package/docs/api/interfaces/UserEventAccess.md +1 -1
  122. package/docs/api/interfaces/UserMenuProps.md +1 -1
  123. package/docs/api/interfaces/UserProfile.md +1 -1
  124. package/docs/api/modules.md +39 -18
  125. package/docs/api-reference/utilities.md +26 -3
  126. package/docs/implementation-guides/data-tables.md +390 -0
  127. package/package.json +1 -1
  128. package/src/components/DataTable/DataTable.tsx +4 -0
  129. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +25 -10
  130. package/src/components/DataTable/components/EditableRow.tsx +174 -16
  131. package/src/components/DataTable/components/UnifiedTableBody.tsx +205 -35
  132. package/src/components/DataTable/types.ts +34 -4
  133. package/src/components/FileDisplay/FileDisplay.test.tsx +184 -201
  134. package/src/components/FileDisplay/FileDisplay.tsx +40 -39
  135. package/src/components/NavigationMenu/NavigationMenu.test.tsx +189 -13
  136. package/src/components/NavigationMenu/NavigationMenu.tsx +142 -35
  137. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +4 -4
  138. package/src/components/Toast/Toast.tsx +1 -1
  139. package/src/hooks/public/usePublicFileDisplay.ts +25 -15
  140. package/src/hooks/useEventTheme.test.ts +11 -0
  141. package/src/hooks/useFileDisplay.ts +11 -0
  142. package/src/hooks/useSecureDataAccess.test.ts +22 -5
  143. package/src/hooks/useToast.ts +11 -2
  144. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +67 -3
  145. package/src/providers/__tests__/ProviderLifecycle.test.tsx +72 -4
  146. package/src/services/__tests__/OrganisationService.pagination.test.ts +10 -2
  147. package/src/styles/core.css +11 -0
  148. package/src/utils/__tests__/formatting.unit.test.ts +33 -0
  149. package/src/utils/file-reference.test.ts +44 -5
  150. package/src/utils/file-reference.ts +49 -26
  151. package/src/utils/formatting.ts +57 -2
  152. package/src/validation/__tests__/passwordSchema.unit.test.ts +3 -3
  153. package/dist/chunk-4BWGRQBG.js.map +0 -1
  154. package/dist/chunk-75G3NZWN.js.map +0 -1
  155. package/dist/chunk-AZFPGDCJ.js.map +0 -1
  156. package/dist/chunk-HBGPLSA5.js.map +0 -1
  157. package/dist/chunk-QPCAGLUS.js.map +0 -1
  158. /package/dist/{DataTable-LWHFLTEW.js.map → DataTable-H2WIR2DN.js.map} +0 -0
  159. /package/dist/{chunk-DWYMGSGU.js.map → chunk-VJ7MPS2K.js.map} +0 -0
@@ -1248,6 +1248,396 @@ function UserManagement() {
1248
1248
  }
1249
1249
  ```
1250
1250
 
1251
+ ## Editable Column Fields
1252
+
1253
+ DataTable provides advanced editing capabilities for columns, including searchable and creatable select fields, grouped options, and customizable number inputs.
1254
+
1255
+ ### Select Fields with Search and Create
1256
+
1257
+ Editable select columns support type-to-search functionality and the ability to create new options on-the-fly when needed.
1258
+
1259
+ #### Basic Searchable Select
1260
+
1261
+ Enable keyboard search in select dropdowns (enabled by default):
1262
+
1263
+ ```tsx
1264
+ const columns = [
1265
+ {
1266
+ accessorKey: 'category',
1267
+ header: 'Category',
1268
+ editable: true,
1269
+ fieldType: 'select',
1270
+ fieldOptions: [
1271
+ { value: 'fruit', label: 'Fruit' },
1272
+ { value: 'vegetable', label: 'Vegetable' },
1273
+ { value: 'grain', label: 'Grain' },
1274
+ ],
1275
+ selectSearchable: true, // Enable type-to-search (default: true)
1276
+ }
1277
+ ];
1278
+ ```
1279
+
1280
+ When `selectSearchable: true` is enabled (or not specified, as it defaults to true), users can:
1281
+ - Type to filter options in the dropdown
1282
+ - Navigate filtered results with keyboard arrows
1283
+ - See only matching options as they type
1284
+
1285
+ #### Creatable Select Fields
1286
+
1287
+ Enable creating new options when the desired option doesn't exist:
1288
+
1289
+ ```tsx
1290
+ const [categories, setCategories] = useState([
1291
+ { value: 'fruit', label: 'Fruit' },
1292
+ { value: 'vegetable', label: 'Vegetable' },
1293
+ ]);
1294
+
1295
+ const handleCreateCategory = useCallback(async (inputValue: string): Promise<string> => {
1296
+ // Create the new category (e.g., via API call)
1297
+ const newCategory = {
1298
+ value: inputValue.toLowerCase().replace(/\s+/g, '-'),
1299
+ label: inputValue,
1300
+ };
1301
+
1302
+ // Update your data source
1303
+ setCategories(prev => [...prev, newCategory]);
1304
+
1305
+ // Return the new value to be set in the cell
1306
+ return newCategory.value;
1307
+ }, []);
1308
+
1309
+ const columns = [
1310
+ {
1311
+ accessorKey: 'category',
1312
+ header: 'Category',
1313
+ editable: true,
1314
+ fieldType: 'select',
1315
+ fieldOptions: categories,
1316
+ searchable: true,
1317
+ creatable: true, // Enable creating new options
1318
+ onCreateNew: handleCreateCategory, // Callback to create new item
1319
+ }
1320
+ ];
1321
+ ```
1322
+
1323
+ **How it works:**
1324
+ 1. User clicks on a select field in edit mode
1325
+ 2. User types text that doesn't match any existing option
1326
+ 3. A "Create '[typed text]'" option appears at the bottom of the dropdown
1327
+ 4. User clicks the create option
1328
+ 5. `onCreateNew` callback is executed with the typed text
1329
+ 6. The callback should create the new item and return its value
1330
+ 7. The new value is set in the cell
1331
+
1332
+ #### Complete Creatable Select Example
1333
+
1334
+ ```tsx
1335
+ import { useState, useCallback, useMemo } from 'react';
1336
+ import { DataTable } from '@jmruthers/pace-core';
1337
+
1338
+ interface Item {
1339
+ id: string;
1340
+ name: string;
1341
+ category: string;
1342
+ }
1343
+
1344
+ function ItemsTable() {
1345
+ const [items, setItems] = useState<Item[]>([
1346
+ { id: '1', name: 'Apple', category: 'fruit' },
1347
+ { id: '2', name: 'Banana', category: 'fruit' },
1348
+ { id: '3', name: 'Carrot', category: 'vegetable' },
1349
+ ]);
1350
+
1351
+ const [categories, setCategories] = useState([
1352
+ { value: 'fruit', label: 'Fruit' },
1353
+ { value: 'vegetable', label: 'Vegetable' },
1354
+ ]);
1355
+
1356
+ const handleCreateCategory = useCallback(async (inputValue: string): Promise<string> => {
1357
+ // Simulate API call
1358
+ await new Promise(resolve => setTimeout(resolve, 300));
1359
+
1360
+ const newCategory = {
1361
+ value: inputValue.toLowerCase().replace(/\s+/g, '-'),
1362
+ label: inputValue,
1363
+ };
1364
+
1365
+ setCategories(prev => [...prev, newCategory]);
1366
+ console.log(`Created new category: "${inputValue}"`);
1367
+
1368
+ return newCategory.value;
1369
+ }, []);
1370
+
1371
+ const columns = useMemo(() => [
1372
+ {
1373
+ id: 'name',
1374
+ accessorKey: 'name',
1375
+ header: 'Item Name',
1376
+ sortable: true,
1377
+ editable: true,
1378
+ },
1379
+ {
1380
+ id: 'category',
1381
+ accessorKey: 'category',
1382
+ header: 'Category',
1383
+ sortable: true,
1384
+ editable: true,
1385
+ fieldType: 'select',
1386
+ fieldOptions: categories,
1387
+ selectSearchable: true,
1388
+ creatable: true,
1389
+ onCreateNew: handleCreateCategory,
1390
+ },
1391
+ ], [categories, handleCreateCategory]);
1392
+
1393
+ return (
1394
+ <DataTable
1395
+ data={items}
1396
+ columns={columns}
1397
+ rbac={{ pageName: 'items' }}
1398
+ features={{
1399
+ editing: true,
1400
+ sorting: true,
1401
+ }}
1402
+ onEditRow={(originalRow, newData) => {
1403
+ setItems(prev => prev.map(item =>
1404
+ item.id === originalRow.id ? { ...item, ...newData } : item
1405
+ ));
1406
+ }}
1407
+ getRowId={(row) => row.id}
1408
+ />
1409
+ );
1410
+ }
1411
+ ```
1412
+
1413
+ ### Grouped Select Options
1414
+
1415
+ Organize select options into logical groups with labels and visual separators:
1416
+
1417
+ ```tsx
1418
+ const columns = [
1419
+ {
1420
+ accessorKey: 'item',
1421
+ header: 'Item',
1422
+ editable: true,
1423
+ fieldType: 'select',
1424
+ fieldOptions: [
1425
+ // Simple option
1426
+ { value: 'all', label: 'All Items' },
1427
+
1428
+ // Separator
1429
+ { type: 'separator' },
1430
+
1431
+ // Group with label
1432
+ {
1433
+ type: 'group',
1434
+ label: 'Fruits',
1435
+ items: [
1436
+ { value: 'apple', label: 'Apple' },
1437
+ { value: 'banana', label: 'Banana' },
1438
+ { value: 'orange', label: 'Orange' },
1439
+ ]
1440
+ },
1441
+
1442
+ // Another group
1443
+ {
1444
+ type: 'group',
1445
+ label: 'Vegetables',
1446
+ items: [
1447
+ { value: 'carrot', label: 'Carrot' },
1448
+ { value: 'broccoli', label: 'Broccoli' },
1449
+ ]
1450
+ },
1451
+
1452
+ // Another separator
1453
+ { type: 'separator' },
1454
+
1455
+ // More simple options
1456
+ { value: 'other', label: 'Other' },
1457
+ ],
1458
+ selectSearchable: true, // Search works across all groups
1459
+ }
1460
+ ];
1461
+ ```
1462
+
1463
+ **Features:**
1464
+ - **Groups**: Use `{ type: 'group', label: 'Group Name', items: [...] }` to organize related options
1465
+ - **Separators**: Use `{ type: 'separator' }` for visual separation between option groups
1466
+ - **Search**: Search functionality works across all groups and items
1467
+ - **Creatable**: Can be combined with `creatable: true` to allow creating new items
1468
+
1469
+ ### Select Dropdown Customization
1470
+
1471
+ Customize the appearance and behavior of select dropdowns:
1472
+
1473
+ ```tsx
1474
+ const columns = [
1475
+ {
1476
+ accessorKey: 'category',
1477
+ header: 'Category',
1478
+ editable: true,
1479
+ fieldType: 'select',
1480
+ fieldOptions: categories,
1481
+ selectSearchable: true,
1482
+
1483
+ // Limit dropdown height
1484
+ selectMaxHeight: '12rem',
1485
+
1486
+ // Add custom CSS classes
1487
+ selectContentClassName: 'custom-select-dropdown',
1488
+
1489
+ // Add inline styles
1490
+ selectContentStyle: {
1491
+ minWidth: '300px',
1492
+ },
1493
+ }
1494
+ ];
1495
+ ```
1496
+
1497
+ ### Number Input Fields
1498
+
1499
+ Control the display of spinner arrows on number input fields:
1500
+
1501
+ ```tsx
1502
+ const columns = [
1503
+ {
1504
+ accessorKey: 'quantity',
1505
+ header: 'Quantity',
1506
+ editable: true,
1507
+ fieldType: 'number',
1508
+
1509
+ // Hide spinner arrows (default: true for DataTable)
1510
+ hideNumberSpinners: true,
1511
+ },
1512
+ {
1513
+ accessorKey: 'price',
1514
+ header: 'Price',
1515
+ editable: true,
1516
+ fieldType: 'number',
1517
+
1518
+ // Show spinner arrows
1519
+ hideNumberSpinners: false,
1520
+ }
1521
+ ];
1522
+ ```
1523
+
1524
+ **Behavior:**
1525
+ - **Default**: Spinner arrows are hidden by default (`hideNumberSpinners: true`) for cleaner UX
1526
+ - **Show Spinners**: Set `hideNumberSpinners: false` to display native HTML5 number input spinners
1527
+ - **Applies to**: All number-related field types (number, currency, percentage)
1528
+
1529
+ ### Complete Editable Fields Example
1530
+
1531
+ ```tsx
1532
+ import { useState, useCallback, useMemo } from 'react';
1533
+ import { DataTable, type DataTableColumn } from '@jmruthers/pace-core';
1534
+
1535
+ interface Product {
1536
+ id: string;
1537
+ name: string;
1538
+ category: string;
1539
+ price: number;
1540
+ quantity: number;
1541
+ status: string;
1542
+ }
1543
+
1544
+ function ProductsTable() {
1545
+ const [products, setProducts] = useState<Product[]>([...]);
1546
+
1547
+ const [categories, setCategories] = useState([
1548
+ { value: 'electronics', label: 'Electronics' },
1549
+ { value: 'clothing', label: 'Clothing' },
1550
+ ]);
1551
+
1552
+ const handleCreateCategory = useCallback(async (inputValue: string): Promise<string> => {
1553
+ const newCategory = {
1554
+ value: inputValue.toLowerCase().replace(/\s+/g, '-'),
1555
+ label: inputValue,
1556
+ };
1557
+ setCategories(prev => [...prev, newCategory]);
1558
+ return newCategory.value;
1559
+ }, []);
1560
+
1561
+ const columns: DataTableColumn<Product>[] = useMemo(() => [
1562
+ {
1563
+ id: 'name',
1564
+ accessorKey: 'name',
1565
+ header: 'Product Name',
1566
+ sortable: true,
1567
+ editable: true,
1568
+ fieldType: 'text',
1569
+ },
1570
+ {
1571
+ id: 'category',
1572
+ accessorKey: 'category',
1573
+ header: 'Category',
1574
+ sortable: true,
1575
+ editable: true,
1576
+ fieldType: 'select',
1577
+ fieldOptions: [
1578
+ { type: 'separator' },
1579
+ {
1580
+ type: 'group',
1581
+ label: 'Existing Categories',
1582
+ items: categories,
1583
+ },
1584
+ ],
1585
+ selectSearchable: true,
1586
+ creatable: true,
1587
+ onCreateNew: handleCreateCategory,
1588
+ selectMaxHeight: '12rem',
1589
+ },
1590
+ {
1591
+ id: 'price',
1592
+ accessorKey: 'price',
1593
+ header: 'Price',
1594
+ sortable: true,
1595
+ editable: true,
1596
+ fieldType: 'number',
1597
+ hideNumberSpinners: true, // Clean appearance
1598
+ },
1599
+ {
1600
+ id: 'quantity',
1601
+ accessorKey: 'quantity',
1602
+ header: 'Quantity',
1603
+ sortable: true,
1604
+ editable: true,
1605
+ fieldType: 'number',
1606
+ hideNumberSpinners: true,
1607
+ },
1608
+ {
1609
+ id: 'status',
1610
+ accessorKey: 'status',
1611
+ header: 'Status',
1612
+ editable: false, // Not editable
1613
+ },
1614
+ ], [categories, handleCreateCategory]);
1615
+
1616
+ return (
1617
+ <DataTable
1618
+ data={products}
1619
+ columns={columns}
1620
+ rbac={{ pageName: 'products' }}
1621
+ features={{
1622
+ editing: true,
1623
+ sorting: true,
1624
+ creation: true,
1625
+ }}
1626
+ onEditRow={(originalRow, newData) => {
1627
+ setProducts(prev => prev.map(product =>
1628
+ product.id === originalRow.id ? { ...product, ...newData } : product
1629
+ ));
1630
+ }}
1631
+ onCreateRow={async (newData) => {
1632
+ const newProduct = { ...newData, id: String(Date.now()) } as Product;
1633
+ setProducts(prev => [...prev, newProduct]);
1634
+ }}
1635
+ getRowId={(row) => row.id}
1636
+ />
1637
+ );
1638
+ }
1639
+ ```
1640
+
1251
1641
  ## Export and Import
1252
1642
 
1253
1643
  DataTable provides seamless CSV export and import functionality with automatic column mapping and support for reference fields.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.105",
3
+ "version": "0.5.107",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -35,6 +35,10 @@
35
35
  * - ✅ **Column Visibility** - Show/hide columns dynamically
36
36
  * - ✅ **Column Reordering** - Drag and drop column reordering
37
37
  * - ✅ **Intelligent Virtual Scrolling** - Automatic performance optimization for large datasets
38
+ * - ✅ **Searchable Select Fields** - Type-to-search in select dropdowns within editable columns
39
+ * - ✅ **Creatable Select Fields** - Create new options on-the-fly when they don't exist
40
+ * - ✅ **Grouped Select Options** - Organize select options into groups with labels and separators
41
+ * - ✅ **Customizable Number Inputs** - Hide spinner arrows on number input fields for cleaner UX
38
42
  * - ✅ **Responsive Design** - Mobile-friendly responsive layout
39
43
  * - ✅ **Accessibility** - WCAG 2.1 AA compliant with keyboard navigation
40
44
  * - ✅ **TypeScript** - Full TypeScript support with strict typing
@@ -688,16 +688,16 @@ describe('DataTableCore Component', () => {
688
688
  });
689
689
 
690
690
  describe('RBAC Integration', () => {
691
- it.skip('renders access denied when user lacks permission', () => {
692
- // Override the mock to return no read permission
693
- mockUseDataTablePermissions.mockReturnValueOnce({
691
+ it('renders access denied when user lacks permission', () => {
692
+ // Override the mock to return no read permission (use mockReturnValue for all calls)
693
+ mockUseDataTablePermissions.mockReturnValue({
694
694
  permissions: {
695
- canRead: { can: false },
696
- canCreate: { can: true },
697
- canUpdate: { can: true },
698
- canDelete: { can: true },
699
- canExport: { can: true },
700
- canImport: { can: true },
695
+ canRead: { can: false, isLoading: false },
696
+ canCreate: { can: true, isLoading: false },
697
+ canUpdate: { can: true, isLoading: false },
698
+ canDelete: { can: true, isLoading: false },
699
+ canExport: { can: true, isLoading: false },
700
+ canImport: { can: true, isLoading: false },
701
701
  },
702
702
  secureFeatures: createFullFeatures(),
703
703
  effectivePageId: 'test-page',
@@ -713,7 +713,22 @@ describe('DataTableCore Component', () => {
713
713
  />
714
714
  );
715
715
 
716
- expect(screen.getByText('Access Denied')).toBeInTheDocument();
716
+ // AccessDeniedPage renders "Access Denied" as an h2 heading
717
+ expect(screen.getByRole('heading', { name: /Access Denied/i })).toBeInTheDocument();
718
+
719
+ // Restore mock after test
720
+ mockUseDataTablePermissions.mockReturnValue({
721
+ permissions: {
722
+ canRead: { can: true },
723
+ canCreate: { can: true },
724
+ canUpdate: { can: true },
725
+ canDelete: { can: true },
726
+ canExport: { can: true },
727
+ canImport: { can: true },
728
+ },
729
+ secureFeatures: createFullFeatures(),
730
+ effectivePageId: 'test-page',
731
+ });
717
732
  });
718
733
  });
719
734
 
@@ -10,6 +10,9 @@ import {
10
10
  SelectItem,
11
11
  SelectTrigger,
12
12
  SelectValue,
13
+ SelectGroup,
14
+ SelectLabel,
15
+ SelectSeparator,
13
16
  } from '../../Select/Select';
14
17
  import type { CellValue, DataRecord, DataTableAction, EditableColumnDef } from '../types';
15
18
 
@@ -25,6 +28,167 @@ interface EditableRowProps<TData extends DataRecord> {
25
28
  hierarchical?: boolean;
26
29
  }
27
30
 
31
+ // Component for select fields with searchable and creatable support
32
+ function SelectEditField<TData extends DataRecord>({
33
+ columnDef,
34
+ accessorKey,
35
+ currentValue,
36
+ placeholder,
37
+ onChange,
38
+ className,
39
+ }: {
40
+ columnDef: EditableColumnDef<TData>;
41
+ accessorKey: string;
42
+ currentValue: CellValue;
43
+ placeholder?: string;
44
+ onChange: (value: CellValue) => void;
45
+ className?: string;
46
+ }) {
47
+ const isSearchable = columnDef.selectSearchable !== false; // Default to true for better UX
48
+ const isCreatable = columnDef.creatable === true;
49
+ const selectRef = React.useRef<HTMLFormElement>(null);
50
+ const [searchTerm, setSearchTerm] = React.useState('');
51
+ const [isOpen, setIsOpen] = React.useState(false);
52
+ const [showCreateOption, setShowCreateOption] = React.useState(false);
53
+
54
+ // Monitor search input value via DOM events to detect when user types
55
+ React.useEffect(() => {
56
+ if (!isOpen || !isSearchable || !isCreatable || !selectRef.current) return;
57
+
58
+ const searchInput = selectRef.current.querySelector<HTMLInputElement>('[data-testid="select-search-input"]');
59
+ if (!searchInput) return;
60
+
61
+ const handleInput = (e: Event) => {
62
+ const target = e.target as HTMLInputElement;
63
+ const currentSearch = target.value;
64
+ setSearchTerm(currentSearch);
65
+
66
+ // Check if search doesn't match any option (including items in groups)
67
+ if (currentSearch.trim()) {
68
+ const searchLower = currentSearch.toLowerCase().trim();
69
+
70
+ // Helper to check if an option matches
71
+ // Use explicit union type instead of typeof to avoid Babel parsing issues
72
+ type FieldOption =
73
+ | { value: string | number; label: string }
74
+ | { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> }
75
+ | { type: 'separator' };
76
+
77
+ const checkMatch = (opt: FieldOption): boolean => {
78
+ // Simple option
79
+ if ('value' in opt && !('type' in opt)) {
80
+ return opt.label.toLowerCase().includes(searchLower);
81
+ }
82
+ // Group - check items within the group
83
+ if ('type' in opt && opt.type === 'group') {
84
+ return (opt as { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> }).items.some((item: { value: string | number; label: string }) => item.label.toLowerCase().includes(searchLower));
85
+ }
86
+ // Separator - doesn't match
87
+ return false;
88
+ };
89
+
90
+ const hasMatch = (columnDef.fieldOptions || []).some(checkMatch);
91
+ setShowCreateOption(!hasMatch);
92
+ } else {
93
+ setShowCreateOption(false);
94
+ }
95
+ };
96
+
97
+ searchInput.addEventListener('input', handleInput);
98
+
99
+ return () => {
100
+ searchInput.removeEventListener('input', handleInput);
101
+ };
102
+ }, [isOpen, isSearchable, isCreatable, columnDef.fieldOptions]);
103
+
104
+ const handleCreateNew = React.useCallback(async () => {
105
+ if (!isCreatable || !columnDef.onCreateNew || !searchTerm.trim()) return;
106
+
107
+ try {
108
+ const newValue = await columnDef.onCreateNew(searchTerm.trim());
109
+ onChange(newValue);
110
+ setSearchTerm('');
111
+ setShowCreateOption(false);
112
+ } catch (error) {
113
+ console.error('Error creating new item:', error);
114
+ }
115
+ }, [isCreatable, columnDef.onCreateNew, searchTerm, onChange]);
116
+
117
+ return (
118
+ <Select
119
+ ref={selectRef}
120
+ value={String(currentValue)}
121
+ onValueChange={(newValue) => {
122
+ if (newValue.startsWith('__create_new__')) {
123
+ handleCreateNew();
124
+ } else {
125
+ onChange(newValue as CellValue);
126
+ }
127
+ }}
128
+ onOpenChange={(open) => {
129
+ setIsOpen(open);
130
+ if (!open) {
131
+ setSearchTerm('');
132
+ setShowCreateOption(false);
133
+ }
134
+ }}
135
+ >
136
+ <SelectTrigger className={className || "w-full h-7"}>
137
+ <SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
138
+ </SelectTrigger>
139
+ <SelectContent
140
+ searchable={isSearchable}
141
+ searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
142
+ maxHeight={columnDef.selectMaxHeight}
143
+ className={columnDef.selectContentClassName}
144
+ style={columnDef.selectContentStyle}
145
+ >
146
+ {columnDef.fieldOptions?.map((option, index) => {
147
+ // Simple option item
148
+ if ('value' in option && !('type' in option)) {
149
+ return (
150
+ <SelectItem key={`${option.value}-${index}`} value={String(option.value)}>
151
+ {option.label}
152
+ </SelectItem>
153
+ );
154
+ }
155
+
156
+ // Separator
157
+ if ('type' in option && option.type === 'separator') {
158
+ return <SelectSeparator key={`separator-${index}`} />;
159
+ }
160
+
161
+ // Group with label
162
+ if ('type' in option && option.type === 'group') {
163
+ const groupOption = option as { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> };
164
+ return (
165
+ <SelectGroup key={`group-${groupOption.label}-${index}`}>
166
+ <SelectLabel>{groupOption.label}</SelectLabel>
167
+ {groupOption.items.map((item: { value: string | number; label: string }) => (
168
+ <SelectItem key={`${item.value}-${index}`} value={String(item.value)}>
169
+ {item.label}
170
+ </SelectItem>
171
+ ))}
172
+ </SelectGroup>
173
+ );
174
+ }
175
+
176
+ return null;
177
+ })}
178
+ {showCreateOption && isCreatable && searchTerm.trim() && (
179
+ <SelectItem
180
+ key="__create_new__"
181
+ value={`__create_new__${searchTerm}`}
182
+ className="bg-main-100 font-medium border-t border-main-200"
183
+ >
184
+ Create "{searchTerm}"
185
+ </SelectItem>
186
+ )}
187
+ </SelectContent>
188
+ </Select>
189
+ );
190
+ }
191
+
28
192
  const renderEditField = <TData extends DataRecord>(
29
193
  column: Column<TData, unknown>,
30
194
  value: CellValue,
@@ -44,21 +208,14 @@ const renderEditField = <TData extends DataRecord>(
44
208
  const currentValue = editingData[accessorKey] ?? value ?? '';
45
209
 
46
210
  return (
47
- <Select
48
- value={String(currentValue)}
49
- onValueChange={(newValue) => onChange({ [accessorKey]: newValue as CellValue })}
50
- >
51
- <SelectTrigger className="w-full h-7">
52
- <SelectValue placeholder={`Select ${column.id}`} />
53
- </SelectTrigger>
54
- <SelectContent>
55
- {columnDef.fieldOptions.map(option => (
56
- <SelectItem key={option.value} value={String(option.value)}>
57
- {option.label}
58
- </SelectItem>
59
- ))}
60
- </SelectContent>
61
- </Select>
211
+ <SelectEditField
212
+ columnDef={columnDef}
213
+ accessorKey={accessorKey}
214
+ currentValue={currentValue}
215
+ placeholder={placeholder}
216
+ onChange={(newValue) => onChange({ [accessorKey]: newValue })}
217
+ className="w-full h-7"
218
+ />
62
219
  );
63
220
  }
64
221
 
@@ -75,13 +232,14 @@ const renderEditField = <TData extends DataRecord>(
75
232
  }
76
233
 
77
234
  if (columnDef.fieldType === 'number') {
235
+ const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
78
236
  return (
79
237
  <Input
80
238
  ref={inputRef}
81
239
  type="number"
82
240
  value={String(value ?? '')}
83
241
  onChange={(e) => onChange(e.target.value as unknown as CellValue)}
84
- className="w-full h-7"
242
+ className={`w-full h-7 ${hideSpinners ? 'datatable-number-no-spinners' : ''}`}
85
243
  />
86
244
  );
87
245
  }