@okta/odyssey-react-mui 1.14.4 → 1.14.6

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 (108) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/Badge.js +1 -1
  3. package/dist/Badge.js.map +1 -1
  4. package/dist/DataTable/DataTable.js +178 -58
  5. package/dist/DataTable/DataTable.js.map +1 -1
  6. package/dist/DataTable/DataTableEmptyState.js +55 -0
  7. package/dist/DataTable/DataTableEmptyState.js.map +1 -0
  8. package/dist/DataTable/DataTablePagination.js +221 -0
  9. package/dist/DataTable/DataTablePagination.js.map +1 -0
  10. package/dist/DataTable/DataTableRowActions.js +34 -24
  11. package/dist/DataTable/DataTableRowActions.js.map +1 -1
  12. package/dist/DataTable/DataTableSettings.js +22 -10
  13. package/dist/DataTable/DataTableSettings.js.map +1 -1
  14. package/dist/DataTable/constants.js +1 -0
  15. package/dist/DataTable/constants.js.map +1 -1
  16. package/dist/DataTable/index.js +1 -0
  17. package/dist/DataTable/index.js.map +1 -1
  18. package/dist/DataTable/useRowReordering.js +3 -3
  19. package/dist/DataTable/useRowReordering.js.map +1 -1
  20. package/dist/DataTable/useScrollIndication.js +70 -0
  21. package/dist/DataTable/useScrollIndication.js.map +1 -0
  22. package/dist/Field.js.map +1 -1
  23. package/dist/Fieldset.js +17 -14
  24. package/dist/Fieldset.js.map +1 -1
  25. package/dist/Form.js +33 -23
  26. package/dist/Form.js.map +1 -1
  27. package/dist/MenuButton.js +1 -1
  28. package/dist/MenuButton.js.map +1 -1
  29. package/dist/SearchField.js +2 -2
  30. package/dist/SearchField.js.map +1 -1
  31. package/dist/labs/DataFilters.js +6 -2
  32. package/dist/labs/DataFilters.js.map +1 -1
  33. package/dist/labs/DataTable.js +3 -3
  34. package/dist/labs/DataTable.js.map +1 -1
  35. package/dist/labs/FileUpload.js +195 -0
  36. package/dist/labs/FileUpload.js.map +1 -0
  37. package/dist/labs/FileUploadIllustration.js +54 -0
  38. package/dist/labs/FileUploadIllustration.js.map +1 -0
  39. package/dist/labs/FileUploadPreview.js +109 -0
  40. package/dist/labs/FileUploadPreview.js.map +1 -0
  41. package/dist/labs/index.js +1 -0
  42. package/dist/labs/index.js.map +1 -1
  43. package/dist/properties/ts/odyssey-react-mui.js +12 -0
  44. package/dist/properties/ts/odyssey-react-mui.js.map +1 -1
  45. package/dist/src/DataTable/DataTable.d.ts +36 -18
  46. package/dist/src/DataTable/DataTable.d.ts.map +1 -1
  47. package/dist/src/DataTable/DataTableEmptyState.d.ts +21 -0
  48. package/dist/src/DataTable/DataTableEmptyState.d.ts.map +1 -0
  49. package/dist/src/DataTable/DataTablePagination.d.ts +33 -0
  50. package/dist/src/DataTable/DataTablePagination.d.ts.map +1 -0
  51. package/dist/src/DataTable/DataTableRowActions.d.ts.map +1 -1
  52. package/dist/src/DataTable/DataTableSettings.d.ts.map +1 -1
  53. package/dist/src/DataTable/constants.d.ts +1 -0
  54. package/dist/src/DataTable/constants.d.ts.map +1 -1
  55. package/dist/src/DataTable/index.d.ts +2 -1
  56. package/dist/src/DataTable/index.d.ts.map +1 -1
  57. package/dist/src/DataTable/useRowReordering.d.ts.map +1 -1
  58. package/dist/src/DataTable/useScrollIndication.d.ts +22 -0
  59. package/dist/src/DataTable/useScrollIndication.d.ts.map +1 -0
  60. package/dist/src/Field.d.ts +8 -7
  61. package/dist/src/Field.d.ts.map +1 -1
  62. package/dist/src/Fieldset.d.ts.map +1 -1
  63. package/dist/src/Form.d.ts.map +1 -1
  64. package/dist/src/OdysseyTranslationProvider.d.ts +1 -1
  65. package/dist/src/OdysseyTranslationProvider.d.ts.map +1 -1
  66. package/dist/src/SearchField.d.ts.map +1 -1
  67. package/dist/src/labs/DataFilters.d.ts +5 -1
  68. package/dist/src/labs/DataFilters.d.ts.map +1 -1
  69. package/dist/src/labs/DataTable.d.ts.map +1 -1
  70. package/dist/src/labs/FileUpload.d.ts +40 -0
  71. package/dist/src/labs/FileUpload.d.ts.map +1 -0
  72. package/dist/src/labs/FileUploadIllustration.d.ts +15 -0
  73. package/dist/src/labs/FileUploadIllustration.d.ts.map +1 -0
  74. package/dist/src/labs/FileUploadPreview.d.ts +21 -0
  75. package/dist/src/labs/FileUploadPreview.d.ts.map +1 -0
  76. package/dist/src/labs/index.d.ts +4 -0
  77. package/dist/src/labs/index.d.ts.map +1 -1
  78. package/dist/src/properties/ts/odyssey-react-mui.d.ts +12 -0
  79. package/dist/src/properties/ts/odyssey-react-mui.d.ts.map +1 -1
  80. package/dist/src/theme/components.d.ts.map +1 -1
  81. package/dist/theme/components.js +10 -1
  82. package/dist/theme/components.js.map +1 -1
  83. package/dist/tsconfig.production.tsbuildinfo +1 -1
  84. package/package.json +3 -3
  85. package/src/Badge.tsx +1 -1
  86. package/src/DataTable/DataTable.tsx +293 -85
  87. package/src/DataTable/DataTableEmptyState.tsx +62 -0
  88. package/src/DataTable/DataTablePagination.tsx +289 -0
  89. package/src/DataTable/DataTableRowActions.tsx +35 -37
  90. package/src/DataTable/DataTableSettings.tsx +43 -17
  91. package/src/DataTable/constants.ts +1 -0
  92. package/src/DataTable/index.tsx +7 -1
  93. package/src/DataTable/useRowReordering.tsx +5 -3
  94. package/src/DataTable/useScrollIndication.tsx +118 -0
  95. package/src/Field.tsx +9 -7
  96. package/src/Fieldset.tsx +24 -18
  97. package/src/Form.tsx +43 -27
  98. package/src/MenuButton.tsx +1 -1
  99. package/src/SearchField.tsx +1 -2
  100. package/src/labs/DataFilters.tsx +9 -0
  101. package/src/labs/DataTable.tsx +5 -9
  102. package/src/labs/FileUpload.tsx +301 -0
  103. package/src/labs/FileUploadIllustration.tsx +66 -0
  104. package/src/labs/FileUploadPreview.tsx +150 -0
  105. package/src/labs/index.ts +4 -2
  106. package/src/properties/odyssey-react-mui.properties +18 -0
  107. package/src/properties/ts/odyssey-react-mui.ts +1 -1
  108. package/src/theme/components.tsx +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okta/odyssey-react-mui",
3
- "version": "1.14.4",
3
+ "version": "1.14.6",
4
4
  "description": "React MUI components for Odyssey, Okta's design system",
5
5
  "author": "Okta, Inc.",
6
6
  "license": "Apache-2.0",
@@ -51,7 +51,7 @@
51
51
  "@mui/system": "^5.15.9",
52
52
  "@mui/utils": "^5.15.9",
53
53
  "@mui/x-date-pickers": "^5.0.15",
54
- "@okta/odyssey-design-tokens": "^1.14.4",
54
+ "@okta/odyssey-design-tokens": "^1.14.6",
55
55
  "date-fns": "^2.30.0",
56
56
  "i18next": "^23.8.2",
57
57
  "material-react-table": "^2.11.3",
@@ -63,5 +63,5 @@
63
63
  "react": ">=17 <19",
64
64
  "react-dom": ">=17 <19"
65
65
  },
66
- "gitHead": "12404ee89fca66a0d95fc5af2c0252727558d78d"
66
+ "gitHead": "e9b92857eb24eff1f87ff3ad415e67fb6508e0fc"
67
67
  }
package/src/Badge.tsx CHANGED
@@ -89,7 +89,7 @@ const Badge = ({
89
89
  const hasNotificationCount = badgeContent && badgeContent > 0;
90
90
 
91
91
  return hasNotificationCount ? (
92
- <Box sx={badgeStyles} data-se={testId} translate={translate}>
92
+ <Box sx={badgeStyles} testId={testId} translate={translate}>
93
93
  {formattedContent}
94
94
  </Box>
95
95
  ) : null;
@@ -10,7 +10,15 @@
10
10
  * See the License for the specific language governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
13
+ import {
14
+ ReactNode,
15
+ memo,
16
+ useCallback,
17
+ useEffect,
18
+ useMemo,
19
+ useRef,
20
+ useState,
21
+ } from "react";
14
22
  import {
15
23
  MRT_Cell,
16
24
  MRT_DensityState,
@@ -18,21 +26,20 @@ import {
18
26
  MRT_RowData,
19
27
  MRT_SortingState,
20
28
  MRT_TableOptions,
21
- MRT_Virtualizer,
29
+ MRT_RowSelectionState,
30
+ MRT_RowVirtualizer,
22
31
  MRT_VisibilityState,
23
- MaterialReactTable,
24
32
  useMaterialReactTable,
33
+ MRT_TableContainer,
25
34
  } from "material-react-table";
26
35
  import {
27
36
  ArrowDownIcon,
28
37
  ArrowUnsortedIcon,
29
38
  DragIndicatorIcon,
39
+ MoreIcon,
30
40
  } from "../icons.generated";
31
- import { densityValues } from "./constants";
32
- import {
33
- DataTablePagination,
34
- paginationTypeValues,
35
- } from "../labs/DataTablePagination";
41
+ import { densityValues, paginationTypeValues } from "./constants";
42
+ import { DataTablePagination } from "./DataTablePagination";
36
43
  import { DataFilter, DataFilters } from "../labs/DataFilters";
37
44
  import {
38
45
  DataTableRowActions,
@@ -40,8 +47,31 @@ import {
40
47
  } from "./DataTableRowActions";
41
48
  import { useRowReordering } from "./useRowReordering";
42
49
  import { DataTableSettings } from "./DataTableSettings";
50
+ import { MenuButton, MenuButtonProps } from "../MenuButton";
43
51
  import { Box } from "../Box";
44
52
  import { DataTableRowSelectionState } from ".";
53
+ import {
54
+ DesignTokens,
55
+ useOdysseyDesignTokens,
56
+ } from "../OdysseyDesignTokensContext";
57
+ import { useScrollIndication } from "./useScrollIndication";
58
+ import styled from "@emotion/styled";
59
+ import { DataTableEmptyState } from "./DataTableEmptyState";
60
+ import { Callout } from "../Callout";
61
+ import { t } from "i18next";
62
+
63
+ export type DataTableGetDataType = {
64
+ page?: number;
65
+ resultsPerPage?: number;
66
+ search?: string;
67
+ filters?: DataFilter[];
68
+ sort?: MRT_SortingState;
69
+ };
70
+
71
+ export type DataTableOnReorderRowsType = {
72
+ rowId: string;
73
+ newRowIndex: number;
74
+ };
45
75
 
46
76
  export type DataTableProps = {
47
77
  /**
@@ -58,8 +88,8 @@ export type DataTableProps = {
58
88
  */
59
89
  getRowId?: MRT_TableOptions<MRT_RowData>["getRowId"];
60
90
  /**
61
- * The initial density of the table. This is available even if the table density
62
- * isn't changeable.
91
+ * The initial density (height & padding) of the table rows. This is available even if the
92
+ * table density isn't changeable by the end user via hasChangeableDensity.
63
93
  */
64
94
  initialDensity?: (typeof densityValues)[number];
65
95
  /**
@@ -124,26 +154,14 @@ export type DataTableProps = {
124
154
  search,
125
155
  filters,
126
156
  sort,
127
- }: {
128
- page?: number;
129
- resultsPerPage?: number;
130
- search?: string;
131
- filters?: DataFilter[];
132
- sort?: MRT_SortingState;
133
- }) =>
157
+ }: DataTableGetDataType) =>
134
158
  | MRT_TableOptions<MRT_RowData>["data"]
135
159
  | Promise<MRT_TableOptions<MRT_RowData>["data"]>;
136
160
  /**
137
161
  * Callback that fires when the user reorders rows within the table. Can be used
138
162
  * to propogate order change to the backend.
139
163
  */
140
- onReorderRows?: ({
141
- rowId,
142
- newRowIndex,
143
- }: {
144
- rowId: string;
145
- newRowIndex: number;
146
- }) => void;
164
+ onReorderRows?: ({ rowId, newRowIndex }: DataTableOnReorderRowsType) => void;
147
165
  /**
148
166
  * The current page number.
149
167
  */
@@ -165,6 +183,24 @@ export type DataTableProps = {
165
183
  * Menu items to include in the optional actions menu on each row.
166
184
  */
167
185
  rowActionMenuItems?: DataTableRowActionsProps["rowActionMenuItems"];
186
+ /**
187
+ * Menu items to include in the bulk actions menu, which appears above the table if a row or rows are selected
188
+ */
189
+ bulkActionMenuItems?: (
190
+ selectedRows: MRT_RowSelectionState,
191
+ ) => MenuButtonProps["children"];
192
+ /**
193
+ * If `error` is not undefined, the DataTable will indicate an error.
194
+ */
195
+ errorMessage?: string;
196
+ /**
197
+ * The component to display when the table is displaying the initial empty state
198
+ */
199
+ emptyPlaceholder?: ReactNode;
200
+ /**
201
+ * The component to display when the query returns no results
202
+ */
203
+ noResultsPlaceholder?: ReactNode;
168
204
  };
169
205
 
170
206
  const displayColumnDefOptions = {
@@ -220,6 +256,66 @@ const displayColumnDefOptions = {
220
256
  },
221
257
  };
222
258
 
259
+ const ScrollableTableContainer = styled("div", {
260
+ shouldForwardProp: (prop) =>
261
+ prop !== "odysseyDesignTokens" &&
262
+ prop !== "isScrollableStart" &&
263
+ prop !== "isScrollableEnd",
264
+ })(
265
+ ({
266
+ odysseyDesignTokens,
267
+ isScrollableStart,
268
+ isScrollableEnd,
269
+ }: {
270
+ odysseyDesignTokens: DesignTokens;
271
+ isScrollableStart: boolean;
272
+ isScrollableEnd: boolean;
273
+ }) => ({
274
+ borderBlockEndColor: odysseyDesignTokens.HueNeutral100,
275
+ borderBlockEndStyle: "solid",
276
+ borderBlockEndWidth: odysseyDesignTokens.BorderWidthMain,
277
+ marginBlockEnd: odysseyDesignTokens.Spacing4,
278
+ position: "relative",
279
+ borderInlineStartColor: isScrollableStart
280
+ ? odysseyDesignTokens.HueNeutral200
281
+ : "transparent",
282
+ borderInlineStartStyle: "solid",
283
+ borderInlineStartWidth: odysseyDesignTokens.BorderWidthMain,
284
+ "::before": {
285
+ background:
286
+ "linear-gradient(-90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.33) 50%, rgba(0, 0, 0, 1) 100%)",
287
+ content: '""',
288
+ opacity: isScrollableStart ? "0.075" : "0",
289
+ pointerEvents: "none",
290
+ position: "absolute",
291
+ top: 0,
292
+ left: 0,
293
+ bottom: 0,
294
+ width: odysseyDesignTokens.Spacing6,
295
+ zIndex: 100,
296
+ transition: `opacity ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`,
297
+ },
298
+ borderInlineEndColor: isScrollableEnd
299
+ ? odysseyDesignTokens.HueNeutral200
300
+ : "transparent",
301
+ borderInlineEndStyle: "solid",
302
+ borderInlineEndWidth: odysseyDesignTokens.BorderWidthMain,
303
+ "::after": {
304
+ background:
305
+ "linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.33) 50%, rgba(0, 0, 0, 1) 100%)",
306
+ content: '""',
307
+ opacity: isScrollableEnd ? "0.075" : "0",
308
+ pointerEvents: "none",
309
+ position: "absolute",
310
+ top: 0,
311
+ right: 0,
312
+ bottom: 0,
313
+ width: odysseyDesignTokens.Spacing6,
314
+ transition: `opacity ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`,
315
+ },
316
+ }),
317
+ );
318
+
223
319
  const DataTable = ({
224
320
  columns,
225
321
  getRowId: getRowIdProp,
@@ -244,6 +340,10 @@ const DataTable = ({
244
340
  hasRowSelection,
245
341
  hasSearch,
246
342
  hasSorting,
343
+ bulkActionMenuItems,
344
+ errorMessage: errorMessageProp,
345
+ emptyPlaceholder,
346
+ noResultsPlaceholder,
247
347
  }: DataTableProps) => {
248
348
  const [data, setData] = useState<MRT_RowData[]>([]);
249
349
  const [pagination, setPagination] = useState({
@@ -251,6 +351,15 @@ const DataTable = ({
251
351
  pageSize: resultsPerPage,
252
352
  });
253
353
  const [draggingRow, setDraggingRow] = useState<MRT_Row<MRT_RowData> | null>();
354
+ const [isTableContainerScrolledToStart, setIsTableContainerScrolledToStart] =
355
+ useState(true);
356
+ const [isTableContainerScrolledToEnd, setIsTableContainerScrolledToEnd] =
357
+ useState(true);
358
+ const [tableInnerContainerWidth, setTableInnerContainerWidth] =
359
+ useState<string>("100%");
360
+ const tableOuterContainerRef = useRef<HTMLDivElement>(null);
361
+ const tableInnerContainerRef = useRef<HTMLDivElement>(null);
362
+ const tableContentRef = useRef<HTMLTableElement>(null);
254
363
 
255
364
  // Table states
256
365
  const [columnSorting, setColumnSorting] = useState<MRT_SortingState>([]);
@@ -258,8 +367,25 @@ const DataTable = ({
258
367
  useState<MRT_VisibilityState>();
259
368
  const [rowDensity, setRowDensity] =
260
369
  useState<MRT_DensityState>(initialDensity);
370
+ const [rowSelection, setRowSelection] = useState<MRT_RowSelectionState>({});
261
371
  const [search, setSearch] = useState<string>("");
262
372
  const [filters, setFilters] = useState<DataFilter[]>();
373
+ const [initialFilters, setInitialFilters] = useState<DataFilter[]>();
374
+ const [isLoading, setIsLoading] = useState<boolean | undefined>(true);
375
+ const [isEmpty, setIsEmpty] = useState<boolean | undefined>();
376
+ const [errorMessage, setErrorMessage] = useState<string | undefined>(
377
+ errorMessageProp,
378
+ );
379
+
380
+ useScrollIndication({
381
+ tableOuterContainer: tableOuterContainerRef.current,
382
+ tableInnerContainer: tableInnerContainerRef.current,
383
+ setIsTableContainerScrolledToStart: setIsTableContainerScrolledToStart,
384
+ setIsTableContainerScrolledToEnd: setIsTableContainerScrolledToEnd,
385
+ setTableInnerContainerWidth: setTableInnerContainerWidth,
386
+ });
387
+
388
+ const odysseyDesignTokens = useOdysseyDesignTokens();
263
389
 
264
390
  const {
265
391
  dragHandleStyles,
@@ -352,6 +478,29 @@ const DataTable = ({
352
478
  [],
353
479
  );
354
480
 
481
+ const emptyState = useCallback(() => {
482
+ const noResultsInnerContent = noResultsPlaceholder || (
483
+ <DataTableEmptyState
484
+ heading={t("table.noresults.heading")}
485
+ text={t("table.noresults.text")}
486
+ />
487
+ );
488
+
489
+ const emptyStateInnerContent =
490
+ emptyPlaceholder && isEmpty ? emptyPlaceholder : noResultsInnerContent;
491
+
492
+ return (
493
+ <Box sx={{ width: tableInnerContainerWidth }}>
494
+ {emptyStateInnerContent}
495
+ </Box>
496
+ );
497
+ }, [
498
+ tableInnerContainerWidth,
499
+ emptyPlaceholder,
500
+ noResultsPlaceholder,
501
+ isEmpty,
502
+ ]);
503
+
355
504
  const dataTable = useMaterialReactTable({
356
505
  columns: columns,
357
506
  data: data,
@@ -361,6 +510,8 @@ const DataTable = ({
361
510
  sorting: columnSorting,
362
511
  globalFilter: search,
363
512
  columnVisibility,
513
+ isLoading,
514
+ rowSelection,
364
515
  },
365
516
  icons: {
366
517
  ArrowDownwardIcon: ArrowDownIcon,
@@ -428,6 +579,7 @@ const DataTable = ({
428
579
 
429
580
  // Row selection
430
581
  enableRowSelection: hasRowSelection,
582
+ onRowSelectionChange: setRowSelection,
431
583
 
432
584
  // Sorting
433
585
  enableSorting: hasSorting,
@@ -442,77 +594,47 @@ const DataTable = ({
442
594
 
443
595
  // Virtualization
444
596
  enableRowVirtualization:
445
- paginationType === "loadMore" || pagination.pageSize > 50,
597
+ paginationType !== "loadMore" && pagination.pageSize > 50,
446
598
  rowVirtualizerInstanceRef:
447
- useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null),
599
+ useRef<MRT_RowVirtualizer<HTMLDivElement, HTMLTableRowElement>>(null),
448
600
  rowVirtualizerOptions: {
449
601
  overscan: 4,
450
602
  },
451
603
 
452
- // Filters
453
- renderTopToolbar: () => (
454
- <Box sx={{ marginBottom: 5 }}>
455
- <DataFilters
456
- onChangeSearch={hasSearch ? setSearch : undefined}
457
- onChangeFilters={hasFilters ? setFilters : undefined}
458
- hasSearchSubmitButton={hasSearchSubmitButton}
459
- searchDelayTime={searchDelayTime}
460
- filters={hasFilters ? dataTableFilters : undefined}
461
- additionalActions={
462
- <DataTableSettings
463
- hasChangeableDensity={hasChangeableDensity}
464
- rowDensity={rowDensity}
465
- setRowDensity={setRowDensity}
466
- hasColumnVisibility={hasColumnVisibility}
467
- columns={columns}
468
- columnVisibility={columnVisibility}
469
- setColumnVisibility={setColumnVisibility}
470
- />
471
- }
472
- />
473
- </Box>
474
- ),
604
+ // States
605
+ renderEmptyRowsFallback: emptyState,
475
606
 
476
- // Pagination
477
- renderBottomToolbar: hasPagination
478
- ? () => (
479
- <DataTablePagination
480
- paginationType={paginationType}
481
- currentNumberOfResults={data.length}
482
- currentPage={pagination.pageIndex}
483
- isPreviousButtonDisabled={pagination.pageIndex <= 1}
484
- isNextButtonDisabled={false} // TODO: Add logic for disabling next/load more button
485
- onClickPrevious={() =>
486
- setPagination({
487
- pageIndex: pagination.pageIndex - 1,
488
- pageSize: pagination.pageSize,
489
- })
490
- }
491
- onClickNext={() => {
492
- if (paginationType === "loadMore") {
493
- setPagination({
494
- pageSize: pagination.pageSize,
495
- pageIndex: pagination.pageSize + resultsPerPage,
496
- });
497
- } else {
498
- setPagination({
499
- pageSize: pagination.pageSize,
500
- pageIndex: pagination.pageIndex + 1,
501
- });
502
- }
503
- }}
504
- />
505
- )
506
- : undefined,
607
+ // Refs
608
+ muiTableProps: {
609
+ ref: tableContentRef,
610
+ },
611
+
612
+ muiTableContainerProps: {
613
+ ref: tableInnerContainerRef,
614
+ },
507
615
  });
508
616
 
509
617
  // Effects
510
- useEffect(() => {
511
- onChangeRowSelection?.(dataTable.getState().rowSelection);
512
- }, [dataTable.getState().rowSelection, dataTable, onChangeRowSelection]);
618
+ const bulkActionMenuButton = useMemo(
619
+ () => (
620
+ <>
621
+ <MenuButton
622
+ buttonVariant="secondary"
623
+ endIcon={<MoreIcon />}
624
+ isDisabled={Object.keys(rowSelection).length === 0}
625
+ ariaLabel="More actions"
626
+ >
627
+ {bulkActionMenuItems?.(rowSelection)}
628
+ </MenuButton>
629
+ </>
630
+ ),
631
+ [bulkActionMenuItems, rowSelection],
632
+ );
513
633
 
514
634
  useEffect(() => {
515
635
  (async () => {
636
+ setIsLoading(true);
637
+ setErrorMessage(errorMessageProp);
516
638
  try {
517
639
  const incomingData = await getData?.({
518
640
  page: pagination.pageIndex,
@@ -523,13 +645,99 @@ const DataTable = ({
523
645
  });
524
646
  setData(incomingData);
525
647
  } catch (error) {
648
+ setErrorMessage(typeof error === "string" ? error : t("table.error"));
526
649
  } finally {
650
+ setIsLoading(false);
527
651
  }
528
652
  })();
529
- }, [pagination, columnSorting, search, filters, getData]);
653
+ }, [pagination, columnSorting, search, filters, getData, errorMessageProp]);
654
+
655
+ useEffect(() => {
656
+ if (!initialFilters && filters) {
657
+ setInitialFilters(filters);
658
+ }
659
+
660
+ setIsEmpty(
661
+ pagination.pageIndex === currentPage &&
662
+ pagination.pageSize === resultsPerPage &&
663
+ search === "" &&
664
+ filters === initialFilters &&
665
+ data.length === 0,
666
+ );
667
+ }, [
668
+ filters,
669
+ pagination,
670
+ search,
671
+ data,
672
+ currentPage,
673
+ initialFilters,
674
+ resultsPerPage,
675
+ ]);
676
+
677
+ useEffect(() => {
678
+ onChangeRowSelection?.(rowSelection);
679
+ }, [rowSelection, onChangeRowSelection]);
530
680
 
531
681
  // Render the table
532
- return <MaterialReactTable table={dataTable} />;
682
+ return (
683
+ <>
684
+ {(hasSearch ||
685
+ hasFilters ||
686
+ hasChangeableDensity ||
687
+ hasColumnVisibility ||
688
+ bulkActionMenuItems) && (
689
+ <Box sx={{ marginBottom: 5 }}>
690
+ <DataFilters
691
+ onChangeSearch={hasSearch ? setSearch : undefined}
692
+ onChangeFilters={hasFilters ? setFilters : undefined}
693
+ hasSearchSubmitButton={hasSearchSubmitButton}
694
+ searchDelayTime={searchDelayTime}
695
+ filters={hasFilters ? dataTableFilters : undefined}
696
+ isDisabled={isEmpty}
697
+ additionalActions={
698
+ <>
699
+ <DataTableSettings
700
+ hasChangeableDensity={hasChangeableDensity}
701
+ rowDensity={rowDensity}
702
+ setRowDensity={setRowDensity}
703
+ hasColumnVisibility={hasColumnVisibility}
704
+ columns={columns}
705
+ columnVisibility={columnVisibility}
706
+ setColumnVisibility={setColumnVisibility}
707
+ />
708
+ {bulkActionMenuItems && bulkActionMenuButton}
709
+ </>
710
+ }
711
+ />
712
+ </Box>
713
+ )}
714
+
715
+ {errorMessage && (
716
+ <Box sx={{ marginBlockEnd: 2 }}>
717
+ <Callout severity="error" text={errorMessage} />
718
+ </Box>
719
+ )}
720
+
721
+ <ScrollableTableContainer
722
+ odysseyDesignTokens={odysseyDesignTokens}
723
+ isScrollableStart={!isTableContainerScrolledToStart}
724
+ isScrollableEnd={!isTableContainerScrolledToEnd}
725
+ ref={tableOuterContainerRef}
726
+ >
727
+ <MRT_TableContainer table={dataTable} />
728
+ </ScrollableTableContainer>
729
+
730
+ {hasPagination && (
731
+ <DataTablePagination
732
+ pagination={pagination}
733
+ setPagination={setPagination}
734
+ totalRows={totalRows}
735
+ isDisabled={isEmpty}
736
+ variant={paginationType}
737
+ />
738
+ )}
739
+ </>
740
+ );
533
741
  };
534
742
 
535
743
  const MemoizedDataTable = memo(DataTable);
@@ -0,0 +1,62 @@
1
+ /*!
2
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { ReactNode, memo } from "react";
14
+ import { Heading4, Paragraph } from "../Typography";
15
+ import { Box } from "../Box";
16
+ import styled from "@emotion/styled";
17
+ import {
18
+ useOdysseyDesignTokens,
19
+ DesignTokens,
20
+ } from "../OdysseyDesignTokensContext";
21
+
22
+ const EmptyContainer = styled("div", {
23
+ shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
24
+ })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({
25
+ display: "flex",
26
+ flexDirection: "column",
27
+ marginBlock: odysseyDesignTokens.Spacing9,
28
+ padding: odysseyDesignTokens.Spacing5,
29
+ textAlign: "center",
30
+ width: "100%",
31
+ }));
32
+
33
+ export type DataTableEmptyStateProps = {
34
+ heading: string;
35
+ text: string;
36
+ primaryButton?: ReactNode;
37
+ secondaryButton?: ReactNode;
38
+ };
39
+
40
+ const DataTableEmptyState = ({
41
+ heading,
42
+ text,
43
+ primaryButton,
44
+ secondaryButton,
45
+ }: DataTableEmptyStateProps) => {
46
+ const odysseyDesignTokens = useOdysseyDesignTokens();
47
+ return (
48
+ <EmptyContainer odysseyDesignTokens={odysseyDesignTokens}>
49
+ <Heading4>{heading}</Heading4>
50
+ <Paragraph>{text}</Paragraph>
51
+ {(primaryButton || secondaryButton) && (
52
+ <Box sx={{ marginBlockStart: 5 }}>
53
+ {secondaryButton}
54
+ {primaryButton}
55
+ </Box>
56
+ )}
57
+ </EmptyContainer>
58
+ );
59
+ };
60
+
61
+ const MemoizedDataTableEmptyState = memo(DataTableEmptyState);
62
+ export { MemoizedDataTableEmptyState as DataTableEmptyState };