@etsoo/react 1.4.80 → 1.4.84

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.
@@ -30,6 +30,14 @@ export interface IReactApp<S extends IAppSettings, D extends IUser, P extends IP
30
30
  * User state
31
31
  */
32
32
  readonly userState: UserState<D>;
33
+ /**
34
+ * Is screen size down 'sm'
35
+ */
36
+ smDown?: boolean;
37
+ /**
38
+ * Is screen size up 'md'
39
+ */
40
+ mdUp?: boolean;
33
41
  /**
34
42
  * Set page data
35
43
  * @param data Page data
package/lib/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export * from './mu/pages/ViewPage';
30
30
  export * from './mu/texts/DateText';
31
31
  export * from './mu/texts/MoneyText';
32
32
  export * from './mu/texts/NumberText';
33
+ export * from './mu/AuditDisplay';
33
34
  export * from './mu/ButtonLink';
34
35
  export * from './mu/ComboBox';
35
36
  export * from './mu/CountdownButton';
@@ -47,6 +48,7 @@ export * from './mu/IconButtonLink';
47
48
  export * from './mu/InputField';
48
49
  export * from './mu/ItemList';
49
50
  export * from './mu/ListItemRightIcon';
51
+ export * from './mu/ListMoreDisplay';
50
52
  export * from './mu/LoadingButton';
51
53
  export * from './mu/MaskInput';
52
54
  export * from './mu/MobileListItemRenderer';
package/lib/index.js CHANGED
@@ -33,6 +33,7 @@ export * from './mu/pages/ViewPage';
33
33
  export * from './mu/texts/DateText';
34
34
  export * from './mu/texts/MoneyText';
35
35
  export * from './mu/texts/NumberText';
36
+ export * from './mu/AuditDisplay';
36
37
  export * from './mu/ButtonLink';
37
38
  export * from './mu/ComboBox';
38
39
  export * from './mu/CountdownButton';
@@ -50,6 +51,7 @@ export * from './mu/IconButtonLink';
50
51
  export * from './mu/InputField';
51
52
  export * from './mu/ItemList';
52
53
  export * from './mu/ListItemRightIcon';
54
+ export * from './mu/ListMoreDisplay';
53
55
  export * from './mu/LoadingButton';
54
56
  export * from './mu/MaskInput';
55
57
  export * from './mu/MobileListItemRenderer';
@@ -0,0 +1,39 @@
1
+ import { Theme } from '@mui/material';
2
+ import React, { CSSProperties } from 'react';
3
+ import { ListMoreDisplayProps } from './ListMoreDisplay';
4
+ /**
5
+ * Audit line update data model
6
+ */
7
+ export interface AuditLineUpdateData {
8
+ oldData: Record<string, unknown>;
9
+ newData: Record<string, unknown>;
10
+ }
11
+ /**
12
+ * Audit line data model
13
+ */
14
+ export interface AuditLine {
15
+ id: number;
16
+ creation: Date;
17
+ user: string;
18
+ action: string;
19
+ changes?: AuditLineUpdateData;
20
+ }
21
+ /**
22
+ * Audit display props
23
+ */
24
+ export interface AuditDisplayProps extends Omit<ListMoreDisplayProps<AuditLine>, 'children'> {
25
+ /**
26
+ * Get list item style callback
27
+ */
28
+ getItemStyle?: (index: number, theme: Theme) => CSSProperties;
29
+ /**
30
+ * Item/line renderer
31
+ */
32
+ itemRenderer?: (data: AuditLine, index: number) => React.ReactNode;
33
+ }
34
+ /**
35
+ * Audit display
36
+ * @param props Props
37
+ * @returns Component
38
+ */
39
+ export declare function AuditDisplay(props: AuditDisplayProps): JSX.Element;
@@ -0,0 +1,84 @@
1
+ import { Utils } from '@etsoo/shared';
2
+ import { Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Typography, useTheme } from '@mui/material';
3
+ import React from 'react';
4
+ import { globalApp, NotificationMessageType } from '..';
5
+ import { ListMoreDisplay } from './ListMoreDisplay';
6
+ // Get label
7
+ const getLabel = (key) => {
8
+ var _a;
9
+ if (typeof globalApp === 'undefined')
10
+ return key;
11
+ return (_a = globalApp.get(Utils.formatInitial(key))) !== null && _a !== void 0 ? _a : key;
12
+ };
13
+ // Format date
14
+ const formatDate = (date) => {
15
+ if (typeof globalApp === 'undefined')
16
+ return date.toUTCString();
17
+ return globalApp.formatDate(date, 'ds');
18
+ };
19
+ // Format value
20
+ const formatValue = (value) => {
21
+ if (value == null)
22
+ return '';
23
+ if (value instanceof Date && typeof globalApp !== 'undefined')
24
+ return globalApp.formatDate(value, 'ds');
25
+ return `${value}`;
26
+ };
27
+ /**
28
+ * Audit display
29
+ * @param props Props
30
+ * @returns Component
31
+ */
32
+ export function AuditDisplay(props) {
33
+ // Theme
34
+ const theme = useTheme();
35
+ // Label
36
+ const dataComparisonLabel = getLabel('dataComparison');
37
+ // Show data comparison
38
+ const showDataComparison = (data) => {
39
+ if (typeof globalApp === 'undefined')
40
+ return;
41
+ const keys = new Set([
42
+ ...Object.keys(data.oldData),
43
+ ...Object.keys(data.newData)
44
+ ]);
45
+ const rows = Array.from(keys).map((field) => ({
46
+ field,
47
+ oldValue: data.oldData[field],
48
+ newValue: data.newData[field]
49
+ }));
50
+ const inputs = (React.createElement(Table, null,
51
+ React.createElement(TableHead, null,
52
+ React.createElement(TableRow, null,
53
+ React.createElement(TableCell, null, getLabel('field')),
54
+ React.createElement(TableCell, { align: "right" }, getLabel('oldValue')),
55
+ React.createElement(TableCell, { align: "right" }, getLabel('newValue')))),
56
+ React.createElement(TableBody, null, rows.map((row) => (React.createElement(TableRow, { key: row.field },
57
+ React.createElement(TableCell, null, getLabel(row.field)),
58
+ React.createElement(TableCell, { align: "right" }, formatValue(row.oldValue)),
59
+ React.createElement(TableCell, { align: "right" }, formatValue(row.newValue))))))));
60
+ globalApp.notifier.alert([undefined, dataComparisonLabel], undefined, NotificationMessageType.Info, { fullScreen: globalApp.smDown, inputs });
61
+ };
62
+ // Destruct
63
+ const { getItemStyle = (index, theme) => ({
64
+ padding: [theme.spacing(1.5), theme.spacing(1)].join(' '),
65
+ background: index % 2 === 0
66
+ ? theme.palette.grey[100]
67
+ : theme.palette.grey[50]
68
+ }), itemRenderer = (data) => {
69
+ return (React.createElement(React.Fragment, null,
70
+ data.changes != null && (React.createElement(Button, { variant: "outlined", size: "small", onClick: () => showDataComparison(data.changes), sx: {
71
+ marginLeft: theme.spacing(1),
72
+ float: 'right'
73
+ } }, dataComparisonLabel)),
74
+ React.createElement(Typography, null, formatDate(data.creation) +
75
+ ', [' +
76
+ getLabel(data.action) +
77
+ '], ' +
78
+ data.user)));
79
+ }, headerTitle = (React.createElement(React.Fragment, null,
80
+ getLabel('audits'),
81
+ React.createElement(Divider, null))), ...rest } = props;
82
+ // Layout
83
+ return (React.createElement(ListMoreDisplay, { headerTitle: headerTitle, ...rest }, (data, index) => (React.createElement("div", { key: data.id, style: getItemStyle(index, theme) }, itemRenderer(data, index)))));
84
+ }
@@ -0,0 +1,26 @@
1
+ import { CardProps } from '@mui/material';
2
+ import React from 'react';
3
+ import { GridLoader } from '../components/GridLoader';
4
+ /**
5
+ * ListMoreDisplay props
6
+ */
7
+ export interface ListMoreDisplayProps<T> extends CardProps, GridLoader<T> {
8
+ /**
9
+ * Children to display the list
10
+ */
11
+ children: (data: T, index: number) => React.ReactNode;
12
+ /**
13
+ * More button label
14
+ */
15
+ moreLabel?: string;
16
+ /**
17
+ * Header title
18
+ */
19
+ headerTitle?: React.ReactNode;
20
+ }
21
+ /**
22
+ * ListMoreDisplay
23
+ * @param props Props
24
+ * @returns Component
25
+ */
26
+ export declare function ListMoreDisplay<T extends {}>(props: ListMoreDisplayProps<T>): JSX.Element;
@@ -0,0 +1,85 @@
1
+ import { Card, CardActions, CardContent, CardHeader, CircularProgress } from '@mui/material';
2
+ import React from 'react';
3
+ import { globalApp, LoadingButton } from '..';
4
+ /**
5
+ * ListMoreDisplay
6
+ * @param props Props
7
+ * @returns Component
8
+ */
9
+ export function ListMoreDisplay(props) {
10
+ // Destruct
11
+ const { autoLoad = true, children, defaultOrderBy, headerTitle, loadBatchSize, loadData, moreLabel = typeof globalApp === 'undefined'
12
+ ? undefined
13
+ : globalApp.get('more') + '...', threshold, ...rest } = props;
14
+ // Refs
15
+ const refs = React.useRef({
16
+ autoLoad,
17
+ currentPage: 0,
18
+ hasNextPage: true,
19
+ isNextPageLoading: false,
20
+ orderBy: defaultOrderBy,
21
+ batchSize: 10,
22
+ loadedItems: 0,
23
+ selectedItems: []
24
+ });
25
+ const ref = refs.current;
26
+ // States
27
+ const [states, setStates] = React.useReducer((currentStates, newStates) => {
28
+ return { ...currentStates, ...newStates };
29
+ }, { completed: false });
30
+ // Load data
31
+ const loadDataLocal = async () => {
32
+ // Prevent multiple loadings
33
+ if (!ref.hasNextPage || ref.isNextPageLoading)
34
+ return;
35
+ // Update state
36
+ ref.isNextPageLoading = true;
37
+ // Parameters
38
+ const { currentPage, batchSize, orderBy, orderByAsc, data } = ref;
39
+ const loadProps = {
40
+ currentPage,
41
+ batchSize,
42
+ orderBy,
43
+ orderByAsc,
44
+ data
45
+ };
46
+ const items = await loadData(loadProps);
47
+ if (items == null || ref.isMounted === false) {
48
+ return;
49
+ }
50
+ ref.isMounted = true;
51
+ const newItems = items.length;
52
+ const hasNextPage = newItems >= batchSize;
53
+ ref.lastLoadedItems = newItems;
54
+ ref.isNextPageLoading = false;
55
+ ref.hasNextPage = hasNextPage;
56
+ // Update rows
57
+ if (states.items == null)
58
+ setStates({ items, completed: !hasNextPage });
59
+ else
60
+ setStates({
61
+ items: [...states.items, ...items],
62
+ completed: !hasNextPage
63
+ });
64
+ };
65
+ React.useEffect(() => {
66
+ if (autoLoad)
67
+ loadDataLocal();
68
+ }, [autoLoad]);
69
+ React.useEffect(() => {
70
+ return () => {
71
+ ref.isMounted = false;
72
+ };
73
+ }, []);
74
+ // Loading
75
+ if (states.items == null)
76
+ return React.createElement(CircularProgress, { size: 20 });
77
+ return (React.createElement(Card, { ...rest },
78
+ React.createElement(CardHeader, { title: headerTitle }),
79
+ React.createElement(CardContent, { sx: {
80
+ paddingTop: 0,
81
+ paddingBottom: states.completed ? 0 : 'inherit'
82
+ } }, states.items.map((item, index) => children(item, index))),
83
+ !states.completed && (React.createElement(CardActions, { sx: { justifyContent: 'flex-end' } },
84
+ React.createElement(LoadingButton, { onClick: async () => await loadDataLocal() }, moreLabel)))));
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.4.80",
3
+ "version": "1.4.84",
4
4
  "description": "TypeScript ReactJs framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -50,11 +50,11 @@
50
50
  "@emotion/react": "^11.7.1",
51
51
  "@emotion/style": "^0.8.0",
52
52
  "@emotion/styled": "^11.6.0",
53
- "@etsoo/appscript": "^1.2.29",
54
- "@etsoo/notificationbase": "^1.1.0",
53
+ "@etsoo/appscript": "^1.2.33",
54
+ "@etsoo/notificationbase": "^1.1.1",
55
55
  "@etsoo/shared": "^1.1.9",
56
56
  "@mui/icons-material": "^5.3.1",
57
- "@mui/material": "^5.3.1",
57
+ "@mui/material": "^5.4.0",
58
58
  "@reach/router": "^1.3.4",
59
59
  "@types/pica": "^5.1.3",
60
60
  "@types/pulltorefreshjs": "^0.1.5",
@@ -72,7 +72,7 @@
72
72
  "react-beautiful-dnd": "^13.1.0",
73
73
  "react-dom": "^17.0.2",
74
74
  "react-draggable": "^4.4.4",
75
- "react-imask": "^6.2.2",
75
+ "react-imask": "^6.3.0",
76
76
  "react-window": "^1.8.6"
77
77
  },
78
78
  "devDependencies": {
@@ -83,8 +83,8 @@
83
83
  "@babel/runtime-corejs3": "^7.16.8",
84
84
  "@types/jest": "^27.4.0",
85
85
  "@types/react-test-renderer": "^17.0.1",
86
- "@typescript-eslint/eslint-plugin": "^5.10.1",
87
- "@typescript-eslint/parser": "^5.10.1",
86
+ "@typescript-eslint/eslint-plugin": "^5.10.2",
87
+ "@typescript-eslint/parser": "^5.10.2",
88
88
  "eslint": "^8.8.0",
89
89
  "eslint-config-airbnb-base": "^15.0.0",
90
90
  "eslint-plugin-import": "^2.25.4",
@@ -96,6 +96,16 @@ export interface IReactApp<
96
96
  */
97
97
  readonly userState: UserState<D>;
98
98
 
99
+ /**
100
+ * Is screen size down 'sm'
101
+ */
102
+ smDown?: boolean;
103
+
104
+ /**
105
+ * Is screen size up 'md'
106
+ */
107
+ mdUp?: boolean;
108
+
99
109
  /**
100
110
  * Set page data
101
111
  * @param data Page data
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export * from './mu/texts/DateText';
37
37
  export * from './mu/texts/MoneyText';
38
38
  export * from './mu/texts/NumberText';
39
39
 
40
+ export * from './mu/AuditDisplay';
40
41
  export * from './mu/ButtonLink';
41
42
  export * from './mu/ComboBox';
42
43
  export * from './mu/CountdownButton';
@@ -54,6 +55,7 @@ export * from './mu/IconButtonLink';
54
55
  export * from './mu/InputField';
55
56
  export * from './mu/ItemList';
56
57
  export * from './mu/ListItemRightIcon';
58
+ export * from './mu/ListMoreDisplay';
57
59
  export * from './mu/LoadingButton';
58
60
  export * from './mu/MaskInput';
59
61
  export * from './mu/MobileListItemRenderer';
@@ -0,0 +1,191 @@
1
+ import { Utils } from '@etsoo/shared';
2
+ import {
3
+ Button,
4
+ Divider,
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableRow,
10
+ Theme,
11
+ Typography,
12
+ useTheme
13
+ } from '@mui/material';
14
+ import React, { CSSProperties } from 'react';
15
+ import { globalApp, NotificationMessageType } from '..';
16
+ import { ListMoreDisplay, ListMoreDisplayProps } from './ListMoreDisplay';
17
+
18
+ /**
19
+ * Audit line update data model
20
+ */
21
+ export interface AuditLineUpdateData {
22
+ oldData: Record<string, unknown>;
23
+ newData: Record<string, unknown>;
24
+ }
25
+
26
+ /**
27
+ * Audit line data model
28
+ */
29
+ export interface AuditLine {
30
+ id: number;
31
+ creation: Date;
32
+ user: string;
33
+ action: string;
34
+ changes?: AuditLineUpdateData;
35
+ }
36
+
37
+ /**
38
+ * Audit display props
39
+ */
40
+ export interface AuditDisplayProps
41
+ extends Omit<ListMoreDisplayProps<AuditLine>, 'children'> {
42
+ /**
43
+ * Get list item style callback
44
+ */
45
+ getItemStyle?: (index: number, theme: Theme) => CSSProperties;
46
+
47
+ /**
48
+ * Item/line renderer
49
+ */
50
+ itemRenderer?: (data: AuditLine, index: number) => React.ReactNode;
51
+ }
52
+
53
+ // Get label
54
+ const getLabel = (key: string) => {
55
+ if (typeof globalApp === 'undefined') return key;
56
+ return globalApp.get(Utils.formatInitial(key)) ?? key;
57
+ };
58
+
59
+ // Format date
60
+ const formatDate = (date: Date) => {
61
+ if (typeof globalApp === 'undefined') return date.toUTCString();
62
+ return globalApp.formatDate(date, 'ds');
63
+ };
64
+
65
+ // Format value
66
+ const formatValue = (value: unknown) => {
67
+ if (value == null) return '';
68
+ if (value instanceof Date && typeof globalApp !== 'undefined')
69
+ return globalApp.formatDate(value, 'ds');
70
+ return `${value}`;
71
+ };
72
+
73
+ /**
74
+ * Audit display
75
+ * @param props Props
76
+ * @returns Component
77
+ */
78
+ export function AuditDisplay(props: AuditDisplayProps) {
79
+ // Theme
80
+ const theme = useTheme();
81
+
82
+ // Label
83
+ const dataComparisonLabel = getLabel('dataComparison');
84
+
85
+ // Show data comparison
86
+ const showDataComparison = (data: AuditLineUpdateData) => {
87
+ if (typeof globalApp === 'undefined') return;
88
+
89
+ const keys = new Set([
90
+ ...Object.keys(data.oldData),
91
+ ...Object.keys(data.newData)
92
+ ]);
93
+
94
+ const rows = Array.from(keys).map((field) => ({
95
+ field,
96
+ oldValue: data.oldData[field],
97
+ newValue: data.newData[field]
98
+ }));
99
+
100
+ const inputs = (
101
+ <Table>
102
+ <TableHead>
103
+ <TableRow>
104
+ <TableCell>{getLabel('field')}</TableCell>
105
+ <TableCell align="right">
106
+ {getLabel('oldValue')}
107
+ </TableCell>
108
+ <TableCell align="right">
109
+ {getLabel('newValue')}
110
+ </TableCell>
111
+ </TableRow>
112
+ </TableHead>
113
+ <TableBody>
114
+ {rows.map((row) => (
115
+ <TableRow key={row.field}>
116
+ <TableCell>{getLabel(row.field)}</TableCell>
117
+ <TableCell align="right">
118
+ {formatValue(row.oldValue)}
119
+ </TableCell>
120
+ <TableCell align="right">
121
+ {formatValue(row.newValue)}
122
+ </TableCell>
123
+ </TableRow>
124
+ ))}
125
+ </TableBody>
126
+ </Table>
127
+ );
128
+
129
+ globalApp.notifier.alert(
130
+ [undefined, dataComparisonLabel],
131
+ undefined,
132
+ NotificationMessageType.Info,
133
+ { fullScreen: globalApp.smDown, inputs }
134
+ );
135
+ };
136
+
137
+ // Destruct
138
+ const {
139
+ getItemStyle = (index, theme) => ({
140
+ padding: [theme.spacing(1.5), theme.spacing(1)].join(' '),
141
+ background:
142
+ index % 2 === 0
143
+ ? theme.palette.grey[100]
144
+ : theme.palette.grey[50]
145
+ }),
146
+ itemRenderer = (data) => {
147
+ return (
148
+ <React.Fragment>
149
+ {data.changes != null && (
150
+ <Button
151
+ variant="outlined"
152
+ size="small"
153
+ onClick={() => showDataComparison(data.changes!)}
154
+ sx={{
155
+ marginLeft: theme.spacing(1),
156
+ float: 'right'
157
+ }}
158
+ >
159
+ {dataComparisonLabel}
160
+ </Button>
161
+ )}
162
+ <Typography>
163
+ {formatDate(data.creation) +
164
+ ', [' +
165
+ getLabel(data.action) +
166
+ '], ' +
167
+ data.user}
168
+ </Typography>
169
+ </React.Fragment>
170
+ );
171
+ },
172
+ headerTitle = (
173
+ <React.Fragment>
174
+ {getLabel('audits')}
175
+ <Divider />
176
+ </React.Fragment>
177
+ ),
178
+ ...rest
179
+ } = props;
180
+
181
+ // Layout
182
+ return (
183
+ <ListMoreDisplay headerTitle={headerTitle} {...rest}>
184
+ {(data, index) => (
185
+ <div key={data.id} style={getItemStyle(index, theme)}>
186
+ {itemRenderer(data, index)}
187
+ </div>
188
+ )}
189
+ </ListMoreDisplay>
190
+ );
191
+ }
@@ -0,0 +1,157 @@
1
+ import {
2
+ Card,
3
+ CardActions,
4
+ CardContent,
5
+ CardHeader,
6
+ CardProps,
7
+ CircularProgress
8
+ } from '@mui/material';
9
+ import React from 'react';
10
+ import { globalApp, LoadingButton } from '..';
11
+ import {
12
+ GridLoadDataProps,
13
+ GridLoader,
14
+ GridLoaderStates
15
+ } from '../components/GridLoader';
16
+
17
+ /**
18
+ * ListMoreDisplay props
19
+ */
20
+ export interface ListMoreDisplayProps<T> extends CardProps, GridLoader<T> {
21
+ /**
22
+ * Children to display the list
23
+ */
24
+ children: (data: T, index: number) => React.ReactNode;
25
+
26
+ /**
27
+ * More button label
28
+ */
29
+ moreLabel?: string;
30
+
31
+ /**
32
+ * Header title
33
+ */
34
+ headerTitle?: React.ReactNode;
35
+ }
36
+
37
+ type states<T> = {
38
+ items?: T[];
39
+ completed: boolean;
40
+ };
41
+
42
+ /**
43
+ * ListMoreDisplay
44
+ * @param props Props
45
+ * @returns Component
46
+ */
47
+ export function ListMoreDisplay<T extends {}>(props: ListMoreDisplayProps<T>) {
48
+ // Destruct
49
+ const {
50
+ autoLoad = true,
51
+ children,
52
+ defaultOrderBy,
53
+ headerTitle,
54
+ loadBatchSize,
55
+ loadData,
56
+ moreLabel = typeof globalApp === 'undefined'
57
+ ? undefined
58
+ : globalApp.get('more') + '...',
59
+ threshold,
60
+ ...rest
61
+ } = props;
62
+
63
+ // Refs
64
+ const refs = React.useRef<GridLoaderStates<T>>({
65
+ autoLoad,
66
+ currentPage: 0,
67
+ hasNextPage: true,
68
+ isNextPageLoading: false,
69
+ orderBy: defaultOrderBy,
70
+ batchSize: 10,
71
+ loadedItems: 0,
72
+ selectedItems: []
73
+ });
74
+ const ref = refs.current;
75
+
76
+ // States
77
+ const [states, setStates] = React.useReducer(
78
+ (currentStates: states<T>, newStates: Partial<states<T>>) => {
79
+ return { ...currentStates, ...newStates };
80
+ },
81
+ { completed: false }
82
+ );
83
+
84
+ // Load data
85
+ const loadDataLocal = async () => {
86
+ // Prevent multiple loadings
87
+ if (!ref.hasNextPage || ref.isNextPageLoading) return;
88
+
89
+ // Update state
90
+ ref.isNextPageLoading = true;
91
+
92
+ // Parameters
93
+ const { currentPage, batchSize, orderBy, orderByAsc, data } = ref;
94
+
95
+ const loadProps: GridLoadDataProps = {
96
+ currentPage,
97
+ batchSize,
98
+ orderBy,
99
+ orderByAsc,
100
+ data
101
+ };
102
+
103
+ const items = await loadData(loadProps);
104
+ if (items == null || ref.isMounted === false) {
105
+ return;
106
+ }
107
+ ref.isMounted = true;
108
+
109
+ const newItems = items.length;
110
+ const hasNextPage = newItems >= batchSize;
111
+ ref.lastLoadedItems = newItems;
112
+ ref.isNextPageLoading = false;
113
+ ref.hasNextPage = hasNextPage;
114
+
115
+ // Update rows
116
+ if (states.items == null) setStates({ items, completed: !hasNextPage });
117
+ else
118
+ setStates({
119
+ items: [...states.items, ...items],
120
+ completed: !hasNextPage
121
+ });
122
+ };
123
+
124
+ React.useEffect(() => {
125
+ if (autoLoad) loadDataLocal();
126
+ }, [autoLoad]);
127
+
128
+ React.useEffect(() => {
129
+ return () => {
130
+ ref.isMounted = false;
131
+ };
132
+ }, []);
133
+
134
+ // Loading
135
+ if (states.items == null) return <CircularProgress size={20} />;
136
+
137
+ return (
138
+ <Card {...rest}>
139
+ <CardHeader title={headerTitle}></CardHeader>
140
+ <CardContent
141
+ sx={{
142
+ paddingTop: 0,
143
+ paddingBottom: states.completed ? 0 : 'inherit'
144
+ }}
145
+ >
146
+ {states.items.map((item, index) => children(item, index))}
147
+ </CardContent>
148
+ {!states.completed && (
149
+ <CardActions sx={{ justifyContent: 'flex-end' }}>
150
+ <LoadingButton onClick={async () => await loadDataLocal()}>
151
+ {moreLabel}
152
+ </LoadingButton>
153
+ </CardActions>
154
+ )}
155
+ </Card>
156
+ );
157
+ }