@etsoo/react 1.4.79 → 1.4.83

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, 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, undefined, NotificationMessageType.Info, { fullScreen: globalApp.smDown, inputs, title: dataComparisonLabel });
61
+ };
62
+ // Destruct
63
+ const { getItemStyle = (index, theme) => ({
64
+ padding: theme.spacing(1),
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
+ 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
+ }
@@ -35,11 +35,11 @@ export interface ViewPageProps<T extends {}> extends Exclude<CommonPageProps, 'c
35
35
  /**
36
36
  * Actions
37
37
  */
38
- actions?: React.ReactNode | ((data: T) => React.ReactNode);
38
+ actions?: React.ReactNode | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
39
39
  /**
40
40
  * Children
41
41
  */
42
- children?: React.ReactNode | ((data: T) => React.ReactNode);
42
+ children?: React.ReactNode | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
43
43
  /**
44
44
  * Fields to display
45
45
  */
@@ -63,13 +63,13 @@ export function ViewPage(props) {
63
63
  // Data
64
64
  const [data, setData] = React.useState();
65
65
  // Load data
66
- const onRefresh = async () => {
66
+ const refresh = async () => {
67
67
  const result = await loadData();
68
68
  if (result == null)
69
69
  return;
70
70
  setData(result);
71
71
  };
72
- return (React.createElement(CommonPage, { paddings: paddings, onRefresh: supportRefresh ? onRefresh : undefined, onUpdate: supportRefresh ? undefined : onRefresh, ...rest, scrollContainer: global }, data == null ? (React.createElement(LinearProgress, null)) : (React.createElement(React.Fragment, null,
72
+ return (React.createElement(CommonPage, { paddings: paddings, onRefresh: supportRefresh ? refresh : undefined, onUpdate: supportRefresh ? undefined : refresh, ...rest, scrollContainer: global }, data == null ? (React.createElement(LinearProgress, null)) : (React.createElement(React.Fragment, null,
73
73
  React.createElement(Grid, { container: true, justifyContent: "left", spacing: paddings, className: "ET-ViewPage", sx: {
74
74
  '.MuiTypography-subtitle2': {
75
75
  fontWeight: 'bold'
@@ -77,7 +77,8 @@ export function ViewPage(props) {
77
77
  } }, fields.map((field, index) => {
78
78
  // Get data
79
79
  const [itemData, itemLabel, gridProps] = getItemField(field, data);
80
- if (itemData == null)
80
+ // Some callback function may return '' instead of undefined
81
+ if (itemData == null || itemData === '')
81
82
  return undefined;
82
83
  // Layout
83
84
  return (React.createElement(Grid, { item: true, ...gridProps, key: index },
@@ -86,6 +87,6 @@ export function ViewPage(props) {
86
87
  ":"),
87
88
  React.createElement(Typography, { variant: "subtitle2" }, itemData)));
88
89
  })),
89
- actions != null && (React.createElement(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: paddings, paddingBottom: paddings, gap: paddings }, Utils.getResult(actions, data))),
90
- Utils.getResult(children, data)))));
90
+ actions != null && (React.createElement(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: paddings, paddingBottom: paddings, gap: paddings }, Utils.getResult(actions, data, refresh))),
91
+ Utils.getResult(children, data, refresh)))));
91
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.4.79",
3
+ "version": "1.4.83",
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.32",
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,188 @@
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
+ 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
+ // Label
82
+ const dataComparisonLabel = getLabel('dataComparison');
83
+
84
+ // Show data comparison
85
+ const showDataComparison = (data: AuditLineUpdateData) => {
86
+ if (typeof globalApp === 'undefined') return;
87
+
88
+ const keys = new Set([
89
+ ...Object.keys(data.oldData),
90
+ ...Object.keys(data.newData)
91
+ ]);
92
+
93
+ const rows = Array.from(keys).map((field) => ({
94
+ field,
95
+ oldValue: data.oldData[field],
96
+ newValue: data.newData[field]
97
+ }));
98
+
99
+ const inputs = (
100
+ <Table>
101
+ <TableHead>
102
+ <TableRow>
103
+ <TableCell>{getLabel('field')}</TableCell>
104
+ <TableCell align="right">
105
+ {getLabel('oldValue')}
106
+ </TableCell>
107
+ <TableCell align="right">
108
+ {getLabel('newValue')}
109
+ </TableCell>
110
+ </TableRow>
111
+ </TableHead>
112
+ <TableBody>
113
+ {rows.map((row) => (
114
+ <TableRow key={row.field}>
115
+ <TableCell>{getLabel(row.field)}</TableCell>
116
+ <TableCell align="right">
117
+ {formatValue(row.oldValue)}
118
+ </TableCell>
119
+ <TableCell align="right">
120
+ {formatValue(row.newValue)}
121
+ </TableCell>
122
+ </TableRow>
123
+ ))}
124
+ </TableBody>
125
+ </Table>
126
+ );
127
+
128
+ globalApp.notifier.alert(
129
+ undefined,
130
+ undefined,
131
+ NotificationMessageType.Info,
132
+ { fullScreen: globalApp.smDown, inputs, title: dataComparisonLabel }
133
+ );
134
+ };
135
+
136
+ // Destruct
137
+ const {
138
+ getItemStyle = (index, theme) => ({
139
+ padding: theme.spacing(1),
140
+ background:
141
+ index % 2 === 0
142
+ ? theme.palette.grey[100]
143
+ : theme.palette.grey[50]
144
+ }),
145
+ itemRenderer = (data) => {
146
+ return (
147
+ <React.Fragment>
148
+ {data.changes != null && (
149
+ <Button
150
+ variant="outlined"
151
+ size="small"
152
+ onClick={() => showDataComparison(data.changes!)}
153
+ sx={{
154
+ marginLeft: theme.spacing(1),
155
+ float: 'right'
156
+ }}
157
+ >
158
+ {dataComparisonLabel}
159
+ </Button>
160
+ )}
161
+ {formatDate(data.creation) +
162
+ ', [' +
163
+ getLabel(data.action) +
164
+ '], ' +
165
+ data.user}
166
+ </React.Fragment>
167
+ );
168
+ },
169
+ headerTitle = (
170
+ <React.Fragment>
171
+ {getLabel('audits')}
172
+ <Divider />
173
+ </React.Fragment>
174
+ ),
175
+ ...rest
176
+ } = props;
177
+
178
+ // Layout
179
+ return (
180
+ <ListMoreDisplay headerTitle={headerTitle} {...rest}>
181
+ {(data, index) => (
182
+ <div key={data.id} style={getItemStyle(index, theme)}>
183
+ {itemRenderer(data, index)}
184
+ </div>
185
+ )}
186
+ </ListMoreDisplay>
187
+ );
188
+ }
@@ -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
+ }
@@ -60,12 +60,16 @@ export interface ViewPageProps<T extends {}>
60
60
  /**
61
61
  * Actions
62
62
  */
63
- actions?: React.ReactNode | ((data: T) => React.ReactNode);
63
+ actions?:
64
+ | React.ReactNode
65
+ | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
64
66
 
65
67
  /**
66
68
  * Children
67
69
  */
68
- children?: React.ReactNode | ((data: T) => React.ReactNode);
70
+ children?:
71
+ | React.ReactNode
72
+ | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
69
73
 
70
74
  /**
71
75
  * Fields to display
@@ -164,7 +168,7 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
164
168
  const [data, setData] = React.useState<T>();
165
169
 
166
170
  // Load data
167
- const onRefresh = async () => {
171
+ const refresh = async () => {
168
172
  const result = await loadData();
169
173
  if (result == null) return;
170
174
  setData(result);
@@ -173,8 +177,8 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
173
177
  return (
174
178
  <CommonPage
175
179
  paddings={paddings}
176
- onRefresh={supportRefresh ? onRefresh : undefined}
177
- onUpdate={supportRefresh ? undefined : onRefresh}
180
+ onRefresh={supportRefresh ? refresh : undefined}
181
+ onUpdate={supportRefresh ? undefined : refresh}
178
182
  {...rest}
179
183
  scrollContainer={global}
180
184
  >
@@ -198,7 +202,9 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
198
202
  const [itemData, itemLabel, gridProps] =
199
203
  getItemField(field, data);
200
204
 
201
- if (itemData == null) return undefined;
205
+ // Some callback function may return '' instead of undefined
206
+ if (itemData == null || itemData === '')
207
+ return undefined;
202
208
 
203
209
  // Layout
204
210
  return (
@@ -227,10 +233,10 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
227
233
  paddingBottom={paddings}
228
234
  gap={paddings}
229
235
  >
230
- {Utils.getResult(actions, data)}
236
+ {Utils.getResult(actions, data, refresh)}
231
237
  </Stack>
232
238
  )}
233
- {Utils.getResult(children, data)}
239
+ {Utils.getResult(children, data, refresh)}
234
240
  </React.Fragment>
235
241
  )}
236
242
  </CommonPage>