@etsoo/react 1.4.80 → 1.4.81

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,77 @@
1
+ import { Utils } from '@etsoo/shared';
2
+ import { Button, Table, TableBody, TableCell, TableHead, TableRow, 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
+ // Show data comparison
36
+ const showDataComparison = (data) => {
37
+ if (typeof globalApp === 'undefined')
38
+ return;
39
+ const keys = new Set([
40
+ ...Object.keys(data.oldData),
41
+ ...Object.keys(data.newData)
42
+ ]);
43
+ const rows = Array.from(keys).map((field) => ({
44
+ field,
45
+ oldValue: data.oldData[field],
46
+ newValue: data.newData[field]
47
+ }));
48
+ const inputs = (React.createElement(Table, null,
49
+ React.createElement(TableHead, null,
50
+ React.createElement(TableRow, null,
51
+ React.createElement(TableCell, null, getLabel('field')),
52
+ React.createElement(TableCell, { align: "right" }, getLabel('oldValue')),
53
+ React.createElement(TableCell, { align: "right" }, getLabel('newValue')))),
54
+ React.createElement(TableBody, null, rows.map((row) => (React.createElement(TableRow, { key: row.field },
55
+ React.createElement(TableCell, null, getLabel(row.field)),
56
+ React.createElement(TableCell, { align: "right" }, formatValue(row.oldValue)),
57
+ React.createElement(TableCell, { align: "right" }, formatValue(row.newValue))))))));
58
+ globalApp.notifier.alert(undefined, undefined, NotificationMessageType.Info, { fullScreen: globalApp.smDown, inputs });
59
+ };
60
+ // Destruct
61
+ const { getItemStyle = (index, theme) => ({
62
+ padding: theme.spacing(1),
63
+ background: index % 2 === 0
64
+ ? theme.palette.grey[100]
65
+ : theme.palette.grey[50]
66
+ }), itemRenderer = (data) => {
67
+ return (React.createElement(React.Fragment, null,
68
+ formatDate(data.creation) +
69
+ ', [' +
70
+ getLabel(data.action) +
71
+ '], ' +
72
+ data.user,
73
+ data.changes != null && (React.createElement(Button, { variant: "outlined", size: "small", onClick: () => showDataComparison(data.changes), sx: { marginLeft: theme.spacing(1) } }, getLabel('dataComparison')))));
74
+ }, ...rest } = props;
75
+ // Layout
76
+ return (React.createElement(ListMoreDisplay, { ...rest }, (data, index) => (React.createElement("div", { key: data.id, style: getItemStyle(index, theme) }, itemRenderer(data, index)))));
77
+ }
@@ -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.81",
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",
53
+ "@etsoo/appscript": "^1.2.31",
54
54
  "@etsoo/notificationbase": "^1.1.0",
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,176 @@
1
+ import { Utils } from '@etsoo/shared';
2
+ import { HelpOutline } from '@mui/icons-material';
3
+ import {
4
+ Button,
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableRow,
10
+ Theme,
11
+ useTheme
12
+ } from '@mui/material';
13
+ import React, { CSSProperties } from 'react';
14
+ import { globalApp, NotificationMessageType } from '..';
15
+ import { ListMoreDisplay, ListMoreDisplayProps } from './ListMoreDisplay';
16
+
17
+ /**
18
+ * Audit line update data model
19
+ */
20
+ export interface AuditLineUpdateData {
21
+ oldData: Record<string, unknown>;
22
+ newData: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * Audit line data model
27
+ */
28
+ export interface AuditLine {
29
+ id: number;
30
+ creation: Date;
31
+ user: string;
32
+ action: string;
33
+ changes?: AuditLineUpdateData;
34
+ }
35
+
36
+ /**
37
+ * Audit display props
38
+ */
39
+ export interface AuditDisplayProps
40
+ extends Omit<ListMoreDisplayProps<AuditLine>, 'children'> {
41
+ /**
42
+ * Get list item style callback
43
+ */
44
+ getItemStyle?: (index: number, theme: Theme) => CSSProperties;
45
+
46
+ /**
47
+ * Item/line renderer
48
+ */
49
+ itemRenderer?: (data: AuditLine, index: number) => React.ReactNode;
50
+ }
51
+
52
+ // Get label
53
+ const getLabel = (key: string) => {
54
+ if (typeof globalApp === 'undefined') return key;
55
+ return globalApp.get(Utils.formatInitial(key)) ?? key;
56
+ };
57
+
58
+ // Format date
59
+ const formatDate = (date: Date) => {
60
+ if (typeof globalApp === 'undefined') return date.toUTCString();
61
+ return globalApp.formatDate(date, 'ds');
62
+ };
63
+
64
+ // Format value
65
+ const formatValue = (value: unknown) => {
66
+ if (value == null) return '';
67
+ if (value instanceof Date && typeof globalApp !== 'undefined')
68
+ return globalApp.formatDate(value, 'ds');
69
+ return `${value}`;
70
+ };
71
+
72
+ /**
73
+ * Audit display
74
+ * @param props Props
75
+ * @returns Component
76
+ */
77
+ export function AuditDisplay(props: AuditDisplayProps) {
78
+ // Theme
79
+ const theme = useTheme();
80
+
81
+ // Show data comparison
82
+ const showDataComparison = (data: AuditLineUpdateData) => {
83
+ if (typeof globalApp === 'undefined') return;
84
+
85
+ const keys = new Set([
86
+ ...Object.keys(data.oldData),
87
+ ...Object.keys(data.newData)
88
+ ]);
89
+
90
+ const rows = Array.from(keys).map((field) => ({
91
+ field,
92
+ oldValue: data.oldData[field],
93
+ newValue: data.newData[field]
94
+ }));
95
+
96
+ const inputs = (
97
+ <Table>
98
+ <TableHead>
99
+ <TableRow>
100
+ <TableCell>{getLabel('field')}</TableCell>
101
+ <TableCell align="right">
102
+ {getLabel('oldValue')}
103
+ </TableCell>
104
+ <TableCell align="right">
105
+ {getLabel('newValue')}
106
+ </TableCell>
107
+ </TableRow>
108
+ </TableHead>
109
+ <TableBody>
110
+ {rows.map((row) => (
111
+ <TableRow key={row.field}>
112
+ <TableCell>{getLabel(row.field)}</TableCell>
113
+ <TableCell align="right">
114
+ {formatValue(row.oldValue)}
115
+ </TableCell>
116
+ <TableCell align="right">
117
+ {formatValue(row.newValue)}
118
+ </TableCell>
119
+ </TableRow>
120
+ ))}
121
+ </TableBody>
122
+ </Table>
123
+ );
124
+
125
+ globalApp.notifier.alert(
126
+ undefined,
127
+ undefined,
128
+ NotificationMessageType.Info,
129
+ { fullScreen: globalApp.smDown, inputs }
130
+ );
131
+ };
132
+
133
+ // Destruct
134
+ const {
135
+ getItemStyle = (index, theme) => ({
136
+ padding: theme.spacing(1),
137
+ background:
138
+ index % 2 === 0
139
+ ? theme.palette.grey[100]
140
+ : theme.palette.grey[50]
141
+ }),
142
+ itemRenderer = (data) => {
143
+ return (
144
+ <React.Fragment>
145
+ {formatDate(data.creation) +
146
+ ', [' +
147
+ getLabel(data.action) +
148
+ '], ' +
149
+ data.user}
150
+ {data.changes != null && (
151
+ <Button
152
+ variant="outlined"
153
+ size="small"
154
+ onClick={() => showDataComparison(data.changes!)}
155
+ sx={{ marginLeft: theme.spacing(1) }}
156
+ >
157
+ {getLabel('dataComparison')}
158
+ </Button>
159
+ )}
160
+ </React.Fragment>
161
+ );
162
+ },
163
+ ...rest
164
+ } = props;
165
+
166
+ // Layout
167
+ return (
168
+ <ListMoreDisplay {...rest}>
169
+ {(data, index) => (
170
+ <div key={data.id} style={getItemStyle(index, theme)}>
171
+ {itemRenderer(data, index)}
172
+ </div>
173
+ )}
174
+ </ListMoreDisplay>
175
+ );
176
+ }
@@ -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
+ }