@applica-software-guru/react-admin 1.5.362-alpha1 → 1.5.363

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/style.css.gz CHANGED
Binary file
package/package.json CHANGED
@@ -108,5 +108,5 @@
108
108
  "type": "module",
109
109
  "types": "dist/index.d.ts",
110
110
  "typings": "dist/index.d.ts",
111
- "version": "1.5.362-alpha1"
111
+ "version": "1.5.363"
112
112
  }
@@ -4,7 +4,6 @@ import {
4
4
  ComponentPropType,
5
5
  ListPaginationContextValue,
6
6
  sanitizeListRestProps,
7
- useListContext,
8
7
  useListPaginationContext,
9
8
  useResourceDefinition,
10
9
  useTranslate
@@ -17,8 +16,6 @@ import { PaginationActions, PaginationActionsProps } from './PaginationActions';
17
16
  const Pagination: FC<PaginationProps> = memo((props) => {
18
17
  const { rowsPerPageOptions = DefaultRowsPerPageOptions, actions, limit = null, ...rest } = props;
19
18
  const { isLoading, hasNextPage, page, perPage, total, setPage, setPerPage } = useListPaginationContext(props);
20
- // Recupera i dati della pagina corrente
21
- const { data } = useListContext();
22
19
  const translate = useTranslate();
23
20
  const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
24
21
  const [currentPage, setCurrentPage] = useState(page - 1); // Stato per la UI
@@ -36,12 +33,7 @@ const Pagination: FC<PaginationProps> = memo((props) => {
36
33
  setIsSelectedPage(false);
37
34
  }, [page]);
38
35
 
39
- // Modalità infinito: total === -1
40
36
  const totalPages = useMemo(() => {
41
- if (total === -1) {
42
- // In modalità infinito, non conosciamo il totale
43
- return undefined;
44
- }
45
37
  return total != null ? Math.ceil(total / perPage) : undefined;
46
38
  }, [perPage, total]);
47
39
 
@@ -63,30 +55,12 @@ const Pagination: FC<PaginationProps> = memo((props) => {
63
55
  }
64
56
 
65
57
  event.preventDefault();
66
- // Modalità infinito: total === -1
67
- if (total === -1) {
68
- // Puoi andare indietro se page >= 0
69
- // Puoi andare avanti solo se la pagina attuale ha rowsPerPage record
70
- const canGoForward = data && Array.isArray(data) && data.length === perPage;
71
- if (page < 0) {
72
- throw new Error(
73
- translate('ra.navigation.page_out_of_boundaries', {
74
- page: page + 1
75
- })
76
- );
77
- }
78
- if (page > currentPage && !canGoForward) {
79
- // Non puoi andare avanti se la risposta è minore di rowsPerPage
80
- return;
81
- }
82
- } else {
83
- if (page < 0 || (totalPages !== undefined && page > totalPages - 1)) {
84
- throw new Error(
85
- translate('ra.navigation.page_out_of_boundaries', {
86
- page: page + 1
87
- })
88
- );
89
- }
58
+ if (page < 0 || (totalPages !== undefined && page > totalPages - 1)) {
59
+ throw new Error(
60
+ translate('ra.navigation.page_out_of_boundaries', {
61
+ page: page + 1
62
+ })
63
+ );
90
64
  }
91
65
 
92
66
  const arrowSelected =
@@ -110,7 +84,7 @@ const Pagination: FC<PaginationProps> = memo((props) => {
110
84
  setPage(page + 1);
111
85
  }
112
86
  },
113
- [debouncedPageChange, setPage, translate, totalPages, setIsSelectedPage, total, data, perPage, currentPage]
87
+ [debouncedPageChange, setPage, translate, totalPages, setIsSelectedPage]
114
88
  );
115
89
 
116
90
  const handlePerPageChange = useCallback(
@@ -202,7 +176,7 @@ const Pagination: FC<PaginationProps> = memo((props) => {
202
176
  // @ts-ignore
203
177
  ActionsComponent={ActionsComponent}
204
178
  nextIconButtonProps={{
205
- disabled: total === -1 ? !(data && Array.isArray(data) && data.length === perPage) : !hasNextPage
179
+ disabled: !hasNextPage
206
180
  }}
207
181
  component="span"
208
182
  labelRowsPerPage={translate('ra.navigation.page_rows_per_page')}
@@ -1,22 +1,16 @@
1
1
  import { FC, memo } from 'react';
2
2
  import { styled } from '@mui/material/styles';
3
3
  import { Pagination, PaginationProps } from '@mui/material';
4
- import { useListContext, useTranslate } from 'ra-core';
5
4
  import PropTypes from 'prop-types';
5
+ import { useTranslate } from 'ra-core';
6
6
 
7
7
  const PaginationActions: FC<PaginationActionsProps> = memo((props) => {
8
8
  const { page, rowsPerPage, count, onPageChange, size = 'small', className, ...rest } = props;
9
- const { data } = useListContext();
10
9
  const translate = useTranslate();
11
10
 
12
11
  const nbPages = Math.ceil(count / rowsPerPage) || 1;
13
12
 
14
- // Modalità infinito: total === -1
15
- const isInfinite = count === -1;
16
- // Disabilita avanti se i record sono meno di rowsPerPage
17
- const disableNext = isInfinite ? !(data && Array.isArray(data) && data.length === rowsPerPage) : page + 1 >= nbPages;
18
-
19
- if (nbPages === 1 && !isInfinite) {
13
+ if (nbPages === 1) {
20
14
  return <Root className={className} />;
21
15
  }
22
16
 
@@ -39,22 +33,13 @@ const PaginationActions: FC<PaginationActionsProps> = memo((props) => {
39
33
  <Root className={className}>
40
34
  <Pagination
41
35
  size={size}
42
- count={isInfinite ? page + 2 : nbPages}
36
+ count={nbPages}
37
+ // <TablePagination>, the parent, uses 0-based pagination
38
+ // while <Pagination> uses 1-based pagination
43
39
  page={page + 1}
44
- onChange={(e: any, newPage) => {
45
- // Disabilita avanti in modalità infinito
46
- if (isInfinite && newPage - 1 > page && disableNext) return;
47
- onPageChange(e, newPage - 1);
48
- }}
40
+ onChange={(e: any, page) => onPageChange(e, page - 1)}
49
41
  {...sanitizeRestProps(rest)}
50
42
  getItemAriaLabel={getItemAriaLabel}
51
- renderItem={(item) => {
52
- // Disabilita il bottone avanti in modalità infinito
53
- if (isInfinite && item.type === 'next') {
54
- return { ...item, disabled: disableNext };
55
- }
56
- return item;
57
- }}
58
43
  />
59
44
  </Root>
60
45
  );
@@ -5,19 +5,29 @@ import clsx from 'clsx';
5
5
  import difference from 'lodash/difference';
6
6
  import union from 'lodash/union';
7
7
  import { Identifier, sanitizeListRestProps, useListContext, useTranslate } from 'ra-core';
8
- import { FC, cloneElement, createElement, isValidElement, useCallback, useEffect, useMemo, useRef } from 'react';
8
+ import {
9
+ FC,
10
+ cloneElement,
11
+ createElement,
12
+ isValidElement,
13
+ useCallback,
14
+ useEffect,
15
+ useMemo,
16
+ useRef,
17
+ useState
18
+ } from 'react';
9
19
  import * as React from 'react';
10
20
  import {
11
21
  BulkDeleteButton,
12
22
  DatagridBody,
13
23
  DatagridClasses,
14
- DatagridHeader,
15
24
  DatagridLoading,
16
25
  DatagridRoot,
17
26
  PureDatagridBody,
18
27
  DatagridProps as RaDatagridProps
19
28
  } from 'react-admin';
20
29
  import { Empty } from '@/components/ra-lists/Empty';
30
+ import { DatagridHeader, SortItem } from './DatagridHeader';
21
31
 
22
32
  const defaultBulkActionButtons = <BulkDeleteButton />;
23
33
 
@@ -125,6 +135,29 @@ const Datagrid = React.forwardRef((props, ref) => {
125
135
 
126
136
  const translate = useTranslate();
127
137
  const { sort, data, isLoading, onSelect, onToggleItem, selectedIds, setSort, total } = useListContext(props);
138
+ const [multiSort, setMultiSort] = useState<SortItem[]>([]);
139
+
140
+ const prevMultiSortKeyRef = useRef<string>('');
141
+ const multiSortKey = useMemo(() => multiSort.map((s) => `${s.field}:${s.order}`).join('|'), [multiSort]);
142
+
143
+ useEffect(() => {
144
+ const prevKey = prevMultiSortKeyRef.current;
145
+ if (prevKey === multiSortKey) return;
146
+
147
+ if (multiSort.length > 0) {
148
+ setSort({
149
+ field: multiSort.map((s) => s.field).join(','),
150
+ order: multiSort.map((s) => s.order).join(',') as 'ASC' | 'DESC'
151
+ });
152
+ }
153
+
154
+ prevMultiSortKeyRef.current = multiSortKey;
155
+ }, [multiSortKey, multiSort, setSort]);
156
+
157
+ const onMultiSortChange = useCallback((newMultiSort: SortItem[]) => {
158
+ setMultiSort(newMultiSort);
159
+ }, []);
160
+
128
161
  const hasBulkActions = !!bulkActionButtons !== false;
129
162
  const contextValue = useMemo(() => ({ isRowExpandable, expandSingle }), [isRowExpandable, expandSingle]);
130
163
  const lastSelected = useRef(null);
@@ -216,7 +249,7 @@ const Datagrid = React.forwardRef((props, ref) => {
216
249
  <div className={DatagridClasses.tableWrapper}>
217
250
  <Table ref={ref} className={DatagridClasses.table} size={size} {...sanitizeRestProps(rest)}>
218
251
  {createOrCloneElement(
219
- header,
252
+ <DatagridHeader multiSort={multiSort} setMultiSort={onMultiSortChange} />,
220
253
  {
221
254
  children,
222
255
  sort,
@@ -0,0 +1,179 @@
1
+ import { Checkbox, TableCell, TableHead, TableRow, TableSortLabel, Typography } from '@mui/material';
2
+ import { RaRecord, useListContext, useTranslateLabel } from 'ra-core';
3
+ import React, { ReactElement, ReactNode, useCallback, useMemo } from 'react';
4
+ import { DatagridHeaderProps } from 'react-admin';
5
+
6
+ type SortItem = {
7
+ field: string;
8
+ order: 'ASC' | 'DESC';
9
+ };
10
+
11
+ interface MultiSortDatagridHeaderProps extends Omit<DatagridHeaderProps, 'setSort'> {
12
+ setMultiSort?: (sort: SortItem[]) => void;
13
+ multiSort?: SortItem[];
14
+ }
15
+
16
+ function DatagridHeader(props: MultiSortDatagridHeaderProps): ReactElement {
17
+ const {
18
+ children,
19
+ hasExpand,
20
+ hasBulkActions,
21
+ isRowSelectable,
22
+ onSelect,
23
+ selectedIds = [],
24
+ data = [],
25
+ multiSort = [],
26
+ setMultiSort,
27
+ resource
28
+ } = props;
29
+ const { setSort } = useListContext();
30
+ const translateLabel = useTranslateLabel();
31
+
32
+ const updateSort = useCallback(
33
+ (field: string, isMultiSort: boolean = false) => {
34
+ if (!setMultiSort) {
35
+ const currentSortForField = multiSort.find((s) => s.field === field);
36
+ const newOrder = currentSortForField?.order === 'ASC' ? 'DESC' : 'ASC';
37
+ setSort({ field, order: newOrder });
38
+ return;
39
+ }
40
+
41
+ const existingIndex = multiSort.findIndex((s) => s.field === field);
42
+
43
+ if (!isMultiSort) {
44
+ if (existingIndex >= 0) {
45
+ const currentOrder = multiSort[existingIndex].order;
46
+ if (currentOrder === 'ASC') {
47
+ setMultiSort([{ field, order: 'DESC' }]);
48
+ } else {
49
+ setMultiSort([]);
50
+ }
51
+ } else {
52
+ setMultiSort([{ field, order: 'ASC' }]);
53
+ }
54
+ return;
55
+ }
56
+
57
+ const newMultiSort = [...multiSort];
58
+
59
+ if (existingIndex >= 0) {
60
+ const currentOrder = newMultiSort[existingIndex].order;
61
+ if (currentOrder === 'ASC') {
62
+ newMultiSort[existingIndex] = { field, order: 'DESC' };
63
+ } else {
64
+ newMultiSort.splice(existingIndex, 1);
65
+ }
66
+ } else {
67
+ newMultiSort.push({ field, order: 'ASC' });
68
+ }
69
+
70
+ setMultiSort(newMultiSort);
71
+ },
72
+ [multiSort, setMultiSort, setSort]
73
+ );
74
+
75
+ const handleSelectAll = useCallback(
76
+ (event: React.ChangeEvent<HTMLInputElement>) => {
77
+ if (!onSelect) {
78
+ return;
79
+ }
80
+ if (event.target.checked) {
81
+ const allIds = data
82
+ .filter((record: RaRecord) => (isRowSelectable ? isRowSelectable(record) : true))
83
+ .map((record: RaRecord) => record.id);
84
+ onSelect(allIds);
85
+ } else {
86
+ onSelect([]);
87
+ }
88
+ },
89
+ [data, isRowSelectable, onSelect]
90
+ );
91
+
92
+ const getSortForField = useCallback(
93
+ (field: string): { order: 'ASC' | 'DESC'; index: number } | null => {
94
+ const index = multiSort.findIndex((s) => s.field === field);
95
+ if (index >= 0) {
96
+ return { order: multiSort[index].order, index };
97
+ }
98
+ return null;
99
+ },
100
+ [multiSort]
101
+ );
102
+
103
+ const childrenArray = useMemo(() => React.Children.toArray(children), [children]);
104
+
105
+ const selectableCount = useMemo(
106
+ () => data.filter((record: RaRecord) => (isRowSelectable ? isRowSelectable(record) : true)).length,
107
+ [data, isRowSelectable]
108
+ );
109
+
110
+ const handleSortClick = useCallback(
111
+ (source: string) => (event: React.MouseEvent<unknown>) => {
112
+ updateSort(source, event.ctrlKey || event.metaKey);
113
+ },
114
+ [updateSort]
115
+ );
116
+
117
+ return (
118
+ <TableHead>
119
+ <TableRow>
120
+ {!!hasExpand && <TableCell />}
121
+ {!!hasBulkActions && (
122
+ <TableCell padding="checkbox">
123
+ <Checkbox
124
+ color="primary"
125
+ checked={selectedIds.length > 0 && selectedIds.length === selectableCount}
126
+ onChange={handleSelectAll}
127
+ />
128
+ </TableCell>
129
+ )}
130
+ {childrenArray.length > 0 ? (
131
+ childrenArray.map((child: ReactNode, index) => {
132
+ if (!child || typeof child !== 'object' || !('props' in child)) {
133
+ return <TableCell key={index} />;
134
+ }
135
+
136
+ const source =
137
+ child.props.source ?? (typeof child.props.label === 'string' ? child.props.label : undefined);
138
+ const sortable = child.props.sortable !== false && !!source;
139
+ const sortInfo = sortable ? getSortForField(source) : null;
140
+
141
+ const translated = translateLabel({
142
+ ...child.props,
143
+ source: child.props.source ?? source,
144
+ resource
145
+ });
146
+
147
+ const displayLabel: ReactNode = translated ?? child.props.label ?? null;
148
+
149
+ return (
150
+ <TableCell
151
+ key={child.props.source ?? index}
152
+ align={child.props.textAlign || 'left'}
153
+ sx={{ cursor: sortable ? 'pointer' : 'default' }}
154
+ >
155
+ {sortable ? (
156
+ <TableSortLabel
157
+ active={!!sortInfo}
158
+ direction={(sortInfo?.order.toLowerCase() as 'asc' | 'desc') || 'asc'}
159
+ onClick={handleSortClick(source)}
160
+ >
161
+ <Typography>{displayLabel}</Typography>
162
+ </TableSortLabel>
163
+ ) : (
164
+ displayLabel
165
+ )}
166
+ </TableCell>
167
+ );
168
+ })
169
+ ) : (
170
+ <TableCell />
171
+ )}
172
+ </TableRow>
173
+ </TableHead>
174
+ );
175
+ }
176
+
177
+ export { DatagridHeader };
178
+
179
+ export type { MultiSortDatagridHeaderProps, SortItem };
@@ -19,147 +19,141 @@ interface ListTabsToolbarProps {
19
19
  tabs?: ListTabToolbarConfig[];
20
20
  }
21
21
 
22
- function ListTabsToolbar({ tabs = [] }: ListTabsToolbarProps) {
23
- const { setFilters, filterValues, displayedFilters } = useListContext();
24
- const { setCurrentTabKey } = useListViewContext();
25
- const resource = useResourceContext();
26
- const theme = useTheme();
27
- const [searchParams, setSearchParams] = useSearchParams();
22
+ interface TabFilterState {
23
+ filterValues: Record<string, any>;
24
+ displayedFilters: Record<string, any>;
25
+ }
28
26
 
29
- if (tabs.length > 0 && tabs.some((tab) => !tab.key)) {
30
- throw new Error('ListTabsToolbar: Each tab must have a unique key.');
31
- }
27
+ function useListTabs(tabs: ListTabToolbarConfig[]) {
28
+ const [searchParams] = useSearchParams();
29
+ const resource = useResourceContext();
30
+ const { setCurrentTabKey } = useListViewContext();
31
+ const { filterValues, displayedFilters, setFilters } = useListContext();
32
32
 
33
- const defaultTabIndex = useMemo(() => tabs.findIndex((tab) => tab.default) ?? 0, [tabs]);
34
33
  const resourceKey = useMemo(() => resource.replace(/^entities\//, ''), [resource]);
35
- const tabGroupKey = useMemo(() => `${resourceKey}-tab-group`, [resourceKey]);
36
- const tabFiltersKey = useMemo(() => `${resourceKey}-tab-filters`, [resourceKey]);
34
+ const tabGroupKey = `${resourceKey}-tab-group`;
35
+ const tabFiltersKey = `${resourceKey}-tab-filters`;
37
36
 
38
- const [currentTab, setCurrentTab] = useSessionStorage(tabGroupKey, defaultTabIndex);
39
- const [tabFilterStates, setTabFilterStates] = useSessionStorage(tabFiltersKey, {});
40
- const initialized = useRef(false);
41
-
42
- const saveCurrentTabFilters = useMemo(
43
- () =>
44
- _.debounce((tabKey: string, filters: any, displayed: any) => {
45
- setTabFilterStates((prev: any) => ({
46
- ...prev,
47
- [tabKey]: {
48
- filterValues: { ...filters },
49
- displayedFilters: { ...displayed }
50
- }
51
- }));
52
- }, 500),
53
- [setTabFilterStates]
54
- );
37
+ const defaultTabIndex = useMemo(() => tabs.findIndex((tab) => tab.default) ?? 0, [tabs]);
55
38
 
56
- const restoreTabFilters = useCallback(
57
- (tabKey: string, tabFilter: Record<string, any>, debounce: boolean) => {
58
- const savedState = (tabFilterStates as any)?.[tabKey];
39
+ const [currentTab, setCurrentTab] = useSessionStorage(tabGroupKey, defaultTabIndex) as [number, (v: number) => void];
40
+ const [tabFilterStates, setTabFilterStates] = useSessionStorage(tabFiltersKey, {}) as [
41
+ Record<string, TabFilterState>,
42
+ (v: any) => void
43
+ ];
59
44
 
60
- if (savedState) {
61
- setFilters(savedState.filterValues, savedState.displayedFilters || {}, debounce);
62
- } else {
63
- setFilters(tabFilter, {}, debounce);
64
- }
65
- },
66
- [tabFilterStates, setFilters]
67
- );
45
+ const initialized = useRef(false);
46
+ const switchingTab = useRef(false);
68
47
 
69
- const buildFilters = useCallback(
70
- (index: number, debounce: boolean) => {
71
- const newTabKey = tabs[index].key;
48
+ const handleTabChange = useCallback(
49
+ (index: number) => {
50
+ const tab = tabs[index];
51
+ if (!tab) return;
72
52
 
73
- setCurrentTabKey?.(newTabKey);
74
- restoreTabFilters(newTabKey, tabs[index].filter, debounce);
75
- },
76
- [tabs, setCurrentTabKey, restoreTabFilters]
77
- );
53
+ setCurrentTab(index);
54
+ setCurrentTabKey?.(tab.key);
78
55
 
79
- const updateUrlParam = useCallback(
80
- (index: number) => {
81
- const newParams = new URLSearchParams(searchParams.toString());
82
- newParams.set(tabGroupKey, String(index));
83
- setSearchParams(newParams, { replace: true });
84
- },
85
- [searchParams, setSearchParams, tabGroupKey]
86
- );
56
+ const savedState = tabFilterStates?.[tab.key];
57
+ const nextFilters = savedState?.filterValues ?? tab.filter;
58
+ const nextDisplayed = savedState?.displayedFilters ?? {};
87
59
 
88
- const handleChange = useCallback(
89
- (_: any, newIndex: number) => {
90
- setCurrentTab(newIndex);
91
- buildFilters(newIndex, true);
60
+ if (!_.isEqual(nextFilters, filterValues) || !_.isEqual(nextDisplayed, displayedFilters)) {
61
+ switchingTab.current = true;
62
+ setFilters(nextFilters, nextDisplayed);
63
+ }
92
64
  },
93
- [buildFilters, setCurrentTab]
65
+ [tabs, tabFilterStates, filterValues, displayedFilters, setCurrentTab, setCurrentTabKey, setFilters]
94
66
  );
95
67
 
96
68
  useEffect(() => {
97
- if (initialized.current && tabs.length > 0) {
98
- const currentTabKey = tabs[Number(currentTab)]?.key;
99
- if (currentTabKey) {
100
- saveCurrentTabFilters(currentTabKey, filterValues, displayedFilters);
101
- }
102
- }
103
- }, [filterValues, displayedFilters, currentTab, tabs, saveCurrentTabFilters]);
69
+ if (initialized.current) return;
70
+ initialized.current = true;
104
71
 
105
- useEffect(() => {
106
72
  const param = searchParams.get(tabGroupKey);
107
- if (param !== String(currentTab)) {
108
- updateUrlParam(Number(currentTab));
73
+ const initialIndex = param != null ? Number(param) : Number(currentTab);
74
+ const hasUrlFilter = searchParams.get('filter') !== null;
75
+
76
+ if (hasUrlFilter) {
77
+ setCurrentTab(initialIndex);
78
+ setCurrentTabKey?.(tabs[initialIndex]?.key);
79
+ } else {
80
+ handleTabChange(initialIndex);
109
81
  }
110
- // eslint-disable-next-line react-hooks/exhaustive-deps
111
- }, [filterValues]);
82
+ }, [searchParams, tabGroupKey, currentTab, setCurrentTab, setCurrentTabKey, handleTabChange, tabs]);
112
83
 
113
84
  useEffect(() => {
114
- if (!initialized.current) {
115
- const param = searchParams.get(tabGroupKey);
116
- const initialIndex = param != null ? Number(param) : Number(currentTab);
85
+ if (!initialized.current || switchingTab.current) {
86
+ switchingTab.current = false;
87
+ return;
88
+ }
89
+
90
+ const currentTabKey = tabs[Number(currentTab)]?.key;
91
+ if (!currentTabKey) return;
117
92
 
118
- if (param != null && initialIndex !== Number(currentTab)) {
119
- setCurrentTab(initialIndex);
93
+ setTabFilterStates((prev: Record<string, TabFilterState>) => {
94
+ const prevState = prev?.[currentTabKey];
95
+ if (
96
+ _.isEqual(prevState?.filterValues, filterValues) &&
97
+ _.isEqual(prevState?.displayedFilters, displayedFilters)
98
+ ) {
99
+ return prev;
120
100
  }
101
+ return {
102
+ ...prev,
103
+ [currentTabKey]: { filterValues, displayedFilters }
104
+ };
105
+ });
106
+ }, [filterValues, displayedFilters, currentTab, tabs, setTabFilterStates]);
107
+
108
+ return {
109
+ currentTab,
110
+ handleTabChange: (_: any, val: number) => handleTabChange(val)
111
+ };
112
+ }
121
113
 
122
- buildFilters(initialIndex, false);
123
- initialized.current = true;
124
- }
125
- // eslint-disable-next-line react-hooks/exhaustive-deps
126
- }, []);
114
+ function ListTabsToolbar({ tabs = [] }: ListTabsToolbarProps) {
115
+ const theme = useTheme();
116
+ const { currentTab, handleTabChange } = useListTabs(tabs);
117
+
118
+ if (tabs.length > 0 && tabs.some((tab) => !tab.key)) {
119
+ throw new Error('ListTabsToolbar: Each tab must have a unique key.');
120
+ }
121
+
122
+ if (tabs.length === 0) return null;
127
123
 
128
124
  return (
129
125
  <Box sx={{ width: '100%' }}>
130
- {tabs.length > 0 ? (
131
- <Tabs
132
- value={Number(currentTab)}
133
- onChange={handleChange}
134
- allowScrollButtonsMobile
135
- variant="scrollable"
136
- scrollButtons="auto"
137
- sx={{
138
- width: '100%',
139
- '.MuiTabs-scrollButtons.Mui-disabled': {
140
- opacity: 0.2
141
- }
142
- }}
143
- >
144
- {tabs.map((tab) => (
145
- <Tab
146
- icon={tab.icon}
147
- key={tab.key}
148
- label={tab.label}
149
- sx={{
150
- flex: '0 0 auto',
151
- lineHeight: 1,
152
- whiteSpace: 'nowrap',
153
- overflow: 'hidden',
154
- textOverflow: 'ellipsis',
155
- '& .MuiTab-iconWrapper': {
156
- marginRight: theme.spacing(0.5)
157
- }
158
- }}
159
- />
160
- ))}
161
- </Tabs>
162
- ) : null}
126
+ <Tabs
127
+ value={Number(currentTab)}
128
+ onChange={handleTabChange}
129
+ allowScrollButtonsMobile
130
+ variant="scrollable"
131
+ scrollButtons="auto"
132
+ sx={{
133
+ width: '100%',
134
+ '.MuiTabs-scrollButtons.Mui-disabled': {
135
+ opacity: 0.2
136
+ }
137
+ }}
138
+ >
139
+ {tabs.map((tab) => (
140
+ <Tab
141
+ icon={tab.icon}
142
+ key={tab.key}
143
+ label={tab.label}
144
+ sx={{
145
+ flex: '0 0 auto',
146
+ lineHeight: 1,
147
+ whiteSpace: 'nowrap',
148
+ overflow: 'hidden',
149
+ textOverflow: 'ellipsis',
150
+ '& .MuiTab-iconWrapper': {
151
+ marginRight: theme.spacing(0.5)
152
+ }
153
+ }}
154
+ />
155
+ ))}
156
+ </Tabs>
163
157
  </Box>
164
158
  );
165
159
  }