@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.
- package/dist/{DataTable-BE0OXZKQ.d.ts → DataTable-D5cBRca8.d.ts} +1 -1
- package/dist/{DataTable-LWHFLTEW.js → DataTable-H2WIR2DN.js} +3 -3
- package/dist/{chunk-QPCAGLUS.js → chunk-4OX5PXHX.js} +5 -2
- package/dist/chunk-4OX5PXHX.js.map +1 -0
- package/dist/{chunk-75G3NZWN.js → chunk-5JJCXTVE.js} +293 -37
- package/dist/chunk-5JJCXTVE.js.map +1 -0
- package/dist/{chunk-HBGPLSA5.js → chunk-DMNMZKWS.js} +70 -24
- package/dist/chunk-DMNMZKWS.js.map +1 -0
- package/dist/{chunk-AZFPGDCJ.js → chunk-EWKCROSF.js} +133 -49
- package/dist/chunk-EWKCROSF.js.map +1 -0
- package/dist/{chunk-4BWGRQBG.js → chunk-NFPV7MRN.js} +22 -2
- package/dist/chunk-NFPV7MRN.js.map +1 -0
- package/dist/{chunk-DWYMGSGU.js → chunk-VJ7MPS2K.js} +2 -2
- package/dist/components.d.ts +3 -3
- package/dist/components.js +4 -4
- package/dist/{formatting-BfDeV-ja.d.ts → formatting-BiEv5oEk.d.ts} +32 -2
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -6
- package/dist/{types-BDg1mAGG.d.ts → types-D4TVpDa1.d.ts} +24 -1
- package/dist/{useToast-Bm6TnSK-.d.ts → useToast-DRah6K-g.d.ts} +5 -2
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +4 -4
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +18 -18
- package/docs/api/interfaces/DataTableColumn.md +115 -10
- package/docs/api/interfaces/DataTableProps.md +38 -38
- package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
- package/docs/api/interfaces/EmptyStateConfig.md +5 -5
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +39 -18
- package/docs/api-reference/utilities.md +26 -3
- package/docs/implementation-guides/data-tables.md +390 -0
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.tsx +4 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +25 -10
- package/src/components/DataTable/components/EditableRow.tsx +174 -16
- package/src/components/DataTable/components/UnifiedTableBody.tsx +205 -35
- package/src/components/DataTable/types.ts +34 -4
- package/src/components/FileDisplay/FileDisplay.test.tsx +184 -201
- package/src/components/FileDisplay/FileDisplay.tsx +40 -39
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +189 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +142 -35
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +4 -4
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/public/usePublicFileDisplay.ts +25 -15
- package/src/hooks/useEventTheme.test.ts +11 -0
- package/src/hooks/useFileDisplay.ts +11 -0
- package/src/hooks/useSecureDataAccess.test.ts +22 -5
- package/src/hooks/useToast.ts +11 -2
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +67 -3
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +72 -4
- package/src/services/__tests__/OrganisationService.pagination.test.ts +10 -2
- package/src/styles/core.css +11 -0
- package/src/utils/__tests__/formatting.unit.test.ts +33 -0
- package/src/utils/file-reference.test.ts +44 -5
- package/src/utils/file-reference.ts +49 -26
- package/src/utils/formatting.ts +57 -2
- package/src/validation/__tests__/passwordSchema.unit.test.ts +3 -3
- package/dist/chunk-4BWGRQBG.js.map +0 -1
- package/dist/chunk-75G3NZWN.js.map +0 -1
- package/dist/chunk-AZFPGDCJ.js.map +0 -1
- package/dist/chunk-HBGPLSA5.js.map +0 -1
- package/dist/chunk-QPCAGLUS.js.map +0 -1
- /package/dist/{DataTable-LWHFLTEW.js.map → DataTable-H2WIR2DN.js.map} +0 -0
- /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
|
@@ -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
|
|
692
|
-
// Override the mock to return no read permission
|
|
693
|
-
mockUseDataTablePermissions.
|
|
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
|
-
|
|
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
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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=
|
|
242
|
+
className={`w-full h-7 ${hideSpinners ? 'datatable-number-no-spinners' : ''}`}
|
|
85
243
|
/>
|
|
86
244
|
);
|
|
87
245
|
}
|