@applica-software-guru/react-admin 1.5.363 → 1.5.365

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/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.363"
111
+ "version": "1.5.365"
112
112
  }
@@ -18,7 +18,7 @@ const Pagination: FC<PaginationProps> = memo((props) => {
18
18
  const { isLoading, hasNextPage, page, perPage, total, setPage, setPerPage } = useListPaginationContext(props);
19
19
  const translate = useTranslate();
20
20
  const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
21
- const [currentPage, setCurrentPage] = useState(page - 1); // Stato per la UI
21
+ const [currentPage, setCurrentPage] = useState(page - 1);
22
22
  const { hasCreate } = useResourceDefinition(props);
23
23
  const [isSelectedPage, setIsSelectedPage] = useState(false);
24
24
  const theme = useTheme();
@@ -173,10 +173,13 @@ const Pagination: FC<PaginationProps> = memo((props) => {
173
173
  page={currentPage}
174
174
  onPageChange={handlePageChange}
175
175
  onRowsPerPageChange={handlePerPageChange}
176
- // @ts-ignore
177
- ActionsComponent={ActionsComponent}
178
- nextIconButtonProps={{
179
- disabled: !hasNextPage
176
+ ActionsComponent={ActionsComponent as any}
177
+ slotProps={{
178
+ actions: {
179
+ nextButton: {
180
+ disabled: !hasNextPage
181
+ }
182
+ }
180
183
  }}
181
184
  component="span"
182
185
  labelRowsPerPage={translate('ra.navigation.page_rows_per_page')}
@@ -0,0 +1,104 @@
1
+ import * as React from 'react';
2
+ import { useCallback } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import DownloadIcon from '@mui/icons-material/GetApp';
5
+ import {
6
+ Exporter,
7
+ FilterPayload,
8
+ fetchRelatedRecords,
9
+ useDataProvider,
10
+ useListContext,
11
+ useNotify,
12
+ useResourceContext
13
+ } from 'ra-core';
14
+ import { Button, ButtonProps } from './Button';
15
+
16
+ /**
17
+ * Custom ExportButton that exports data.
18
+ *
19
+ *
20
+ * @example
21
+ * import { ExportButton } from '@applica-software-guru/react-admin';
22
+ * import { TopToolbar } from 'react-admin';
23
+ *
24
+ * const ListActions = () => (
25
+ * <TopToolbar>
26
+ * <ExportButton />
27
+ * </TopToolbar>
28
+ * );
29
+ */
30
+ function _ExportButton(props: ExportButtonProps) {
31
+ const {
32
+ maxResults = 1000,
33
+ onClick,
34
+ label = 'ra.action.export',
35
+ icon = defaultIcon,
36
+ exporter: customExporter,
37
+ meta,
38
+ sort,
39
+ ...rest
40
+ } = props;
41
+ const { filter, filterValues, exporter: exportContext, total } = useListContext(props);
42
+ const resource = useResourceContext(props);
43
+ const exporter = customExporter || exportContext;
44
+ const dataProvider = useDataProvider();
45
+ const notify = useNotify();
46
+
47
+ const handleClick = useCallback(
48
+ (event: React.MouseEvent) => {
49
+ dataProvider
50
+ .getList(resource, {
51
+ sort: { field: '_id', order: 'ASC' },
52
+ filter: filter ? { ...filterValues, ...filter } : filterValues,
53
+ pagination: { page: 1, perPage: maxResults },
54
+ meta
55
+ })
56
+ .then(({ data }) => exporter && exporter(data, fetchRelatedRecords(dataProvider), dataProvider, resource))
57
+ .catch((error) => {
58
+ console.error(error);
59
+ notify('ra.notification.http_error', { type: 'error' });
60
+ });
61
+ if (typeof onClick === 'function') {
62
+ onClick(event as any);
63
+ }
64
+ },
65
+ [dataProvider, exporter, filter, filterValues, maxResults, notify, onClick, resource, meta]
66
+ );
67
+
68
+ return (
69
+ <Button onClick={handleClick} label={label} disabled={total === 0} {...rest}>
70
+ {icon}
71
+ </Button>
72
+ );
73
+ }
74
+
75
+ interface Props {
76
+ exporter?: Exporter;
77
+ filterValues?: FilterPayload;
78
+ icon?: JSX.Element;
79
+ label?: string;
80
+ maxResults?: number;
81
+ onClick?: (e: Event) => void;
82
+ resource?: string;
83
+ meta?: any;
84
+ sort?: any;
85
+ }
86
+
87
+ type ExportButtonProps = Props & ButtonProps;
88
+
89
+ _ExportButton.propTypes = {
90
+ exporter: PropTypes.func,
91
+ filterValues: PropTypes.object,
92
+ label: PropTypes.string,
93
+ maxResults: PropTypes.number,
94
+ resource: PropTypes.string,
95
+ icon: PropTypes.element,
96
+ meta: PropTypes.any
97
+ };
98
+
99
+ const defaultIcon = <DownloadIcon />;
100
+
101
+ const ExportButton = React.memo(_ExportButton);
102
+
103
+ export { ExportButton };
104
+ export type { ExportButtonProps };
@@ -4,5 +4,6 @@ export * from './CreateButton';
4
4
  export * from './CreateInDialogButton';
5
5
  export * from './DeleteWithConfirmButton';
6
6
  export * from './EditInDialogButton';
7
+ export * from './ExportButton';
7
8
  export * from './GoogleLoginButton';
8
9
  export * from './ImpersonateUserButton';
@@ -5,17 +5,8 @@ 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 {
9
- FC,
10
- cloneElement,
11
- createElement,
12
- isValidElement,
13
- useCallback,
14
- useEffect,
15
- useMemo,
16
- useRef,
17
- useState
18
- } from 'react';
8
+ import { FC, cloneElement, createElement, isValidElement, useCallback, useEffect, useMemo, useRef } from 'react';
9
+ import { useLocation, useNavigate } from 'react-router-dom';
19
10
  import * as React from 'react';
20
11
  import {
21
12
  BulkDeleteButton,
@@ -134,29 +125,54 @@ const Datagrid = React.forwardRef((props, ref) => {
134
125
  } = props;
135
126
 
136
127
  const translate = useTranslate();
137
- const { sort, data, isLoading, onSelect, onToggleItem, selectedIds, setSort, total } = useListContext(props);
138
- const [multiSort, setMultiSort] = useState<SortItem[]>([]);
128
+ const listContext = useListContext(props);
129
+ const { sort, data, isLoading, onSelect, onToggleItem, selectedIds, setSort, total } = listContext;
130
+ const navigate = useNavigate();
131
+ const location = useLocation();
139
132
 
140
- const prevMultiSortKeyRef = useRef<string>('');
141
- const multiSortKey = useMemo(() => multiSort.map((s) => `${s.field}:${s.order}`).join('|'), [multiSort]);
133
+ const clickedFields = useRef<Set<string>>(new Set());
142
134
 
143
- useEffect(() => {
144
- const prevKey = prevMultiSortKeyRef.current;
145
- if (prevKey === multiSortKey) return;
135
+ function handleHeaderSortClick(field: string) {
136
+ if (field) clickedFields.current.add(field);
137
+ }
146
138
 
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
- });
139
+ const multiSort = useMemo<SortItem[]>(() => {
140
+ if (!sort || !sort.field) return [];
141
+ if (sort.field === 'id' && !clickedFields.current.has('id')) {
142
+ return [];
152
143
  }
144
+ const fields = sort.field.split(',');
145
+ const orders = (sort.order || '').split(',');
146
+ return fields.map((field, i) => ({
147
+ field,
148
+ order: (orders[i] || 'ASC') as 'ASC' | 'DESC'
149
+ }));
150
+ }, [sort]);
151
+
152
+ const onMultiSortChange = useCallback(
153
+ (newMultiSort: SortItem[]) => {
154
+ const newField = newMultiSort.map((s) => s.field).join(',');
155
+ const newOrder = newMultiSort.length > 0 ? newMultiSort.map((s) => s.order).join(',') : 'ASC';
156
+
157
+ if (newMultiSort.length > 1) {
158
+ const searchParams = new URLSearchParams(
159
+ location.search.startsWith('?') ? location.search.slice(1) : location.search
160
+ );
153
161
 
154
- prevMultiSortKeyRef.current = multiSortKey;
155
- }, [multiSortKey, multiSort, setSort]);
162
+ searchParams.set('sort', newField);
163
+ searchParams.set('order', newOrder);
156
164
 
157
- const onMultiSortChange = useCallback((newMultiSort: SortItem[]) => {
158
- setMultiSort(newMultiSort);
159
- }, []);
165
+ const url = searchParams.toString();
166
+ navigate(`${location.pathname}${url ? `?${url}` : ''}`, { replace: true });
167
+ } else {
168
+ setSort({
169
+ field: newField,
170
+ order: newOrder as 'ASC' | 'DESC'
171
+ });
172
+ }
173
+ },
174
+ [location, navigate, setSort]
175
+ );
160
176
 
161
177
  const hasBulkActions = !!bulkActionButtons !== false;
162
178
  const contextValue = useMemo(() => ({ isRowExpandable, expandSingle }), [isRowExpandable, expandSingle]);
@@ -260,7 +276,8 @@ const Datagrid = React.forwardRef((props, ref) => {
260
276
  onSelect,
261
277
  resource,
262
278
  selectedIds,
263
- setSort
279
+ setSort,
280
+ onSortClick: handleHeaderSortClick
264
281
  },
265
282
  children
266
283
  )}
@@ -9,8 +9,9 @@ type SortItem = {
9
9
  };
10
10
 
11
11
  interface MultiSortDatagridHeaderProps extends Omit<DatagridHeaderProps, 'setSort'> {
12
- setMultiSort?: (sort: SortItem[]) => void;
13
12
  multiSort?: SortItem[];
13
+ onSortClick?: (field: string) => void;
14
+ setMultiSort?: (sort: SortItem[]) => void;
14
15
  }
15
16
 
16
17
  function DatagridHeader(props: MultiSortDatagridHeaderProps): ReactElement {
@@ -24,6 +25,7 @@ function DatagridHeader(props: MultiSortDatagridHeaderProps): ReactElement {
24
25
  data = [],
25
26
  multiSort = [],
26
27
  setMultiSort,
28
+ onSortClick,
27
29
  resource
28
30
  } = props;
29
31
  const { setSort } = useListContext();
@@ -31,9 +33,10 @@ function DatagridHeader(props: MultiSortDatagridHeaderProps): ReactElement {
31
33
 
32
34
  const updateSort = useCallback(
33
35
  (field: string, isMultiSort: boolean = false) => {
36
+ if (onSortClick) onSortClick(field);
34
37
  if (!setMultiSort) {
35
- const currentSortForField = multiSort.find((s) => s.field === field);
36
- const newOrder = currentSortForField?.order === 'ASC' ? 'DESC' : 'ASC';
38
+ const currentSort = multiSort.find((s) => s.field === field);
39
+ const newOrder = currentSort?.order === 'ASC' ? 'DESC' : 'ASC';
37
40
  setSort({ field, order: newOrder });
38
41
  return;
39
42
  }
@@ -69,7 +72,7 @@ function DatagridHeader(props: MultiSortDatagridHeaderProps): ReactElement {
69
72
 
70
73
  setMultiSort(newMultiSort);
71
74
  },
72
- [multiSort, setMultiSort, setSort]
75
+ [multiSort, setMultiSort, setSort, onSortClick]
73
76
  );
74
77
 
75
78
  const handleSelectAll = useCallback(
@@ -134,7 +137,9 @@ function DatagridHeader(props: MultiSortDatagridHeaderProps): ReactElement {
134
137
  }
135
138
 
136
139
  const source =
137
- child.props.source ?? (typeof child.props.label === 'string' ? child.props.label : undefined);
140
+ child.props.sortBy ??
141
+ child.props.source ??
142
+ (typeof child.props.label === 'string' ? child.props.label : undefined);
138
143
  const sortable = child.props.sortable !== false && !!source;
139
144
  const sortInfo = sortable ? getSortForField(source) : null;
140
145
 
@@ -8,18 +8,31 @@ import { RaRecord, useListContext } from 'ra-core';
8
8
  import { ElementType, ReactElement, ReactNode, cloneElement } from 'react';
9
9
  import * as React from 'react';
10
10
  import {
11
- ListActions as DefaultActions,
11
+ CreateButton,
12
12
  Pagination as DefaultPagination,
13
+ FilterButton,
13
14
  SavedQueriesListClasses,
14
- Title
15
+ Title,
16
+ TopToolbar
15
17
  } from 'react-admin';
18
+ import { ExportButton } from '@/components/ra-buttons/ExportButton';
16
19
  import { ListToolbar } from './ListToolbar';
17
20
  import { FilterSidebar } from './FilterSidebar';
18
21
  import { MainCard } from '@/components/MainCard';
19
22
  import { Empty } from '@/components/ra-lists/Empty';
20
23
  import { type ListTabToolbarConfig } from './ListTabsToolbar';
21
24
 
22
- const defaultActions = <DefaultActions />;
25
+ function DefaultListActions() {
26
+ return (
27
+ <TopToolbar>
28
+ <FilterButton />
29
+ <CreateButton />
30
+ <ExportButton />
31
+ </TopToolbar>
32
+ );
33
+ }
34
+
35
+ const defaultActions = <DefaultListActions />;
23
36
  const defaultPagination = <DefaultPagination />;
24
37
  const defaultEmpty = <Empty />;
25
38
  const DefaultComponent = Card;