@etsoo/react 1.4.77 → 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
+ }
@@ -2,6 +2,7 @@ import { Keyboard } from '@etsoo/shared';
2
2
  import { Autocomplete } from '@mui/material';
3
3
  import React from 'react';
4
4
  import { Utils } from '../app/Utils';
5
+ import { Utils as SharedUtils } from '@etsoo/shared';
5
6
  import { InputField } from './InputField';
6
7
  import { SearchField } from './SearchField';
7
8
  /**
@@ -81,6 +82,9 @@ export function ComboBox(props) {
81
82
  return;
82
83
  if (onLoadData)
83
84
  onLoadData(result);
85
+ if (search) {
86
+ SharedUtils.addBlankItem(result, idField, labelField);
87
+ }
84
88
  setOptions(result);
85
89
  });
86
90
  }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import { Utils } from '@etsoo/shared';
1
2
  import React from 'react';
2
3
  import { globalApp } from '..';
3
4
  import { SelectEx } from './SelectEx';
@@ -15,7 +16,7 @@ export function SelectBool(props) {
15
16
  { id: 'true', label: globalApp.get('yes') }
16
17
  ];
17
18
  if (search)
18
- options.unshift({ id: '', label: '---' });
19
+ Utils.addBlankItem(options);
19
20
  // Layout
20
21
  return React.createElement(SelectEx, { options: options, search: search, ...rest });
21
22
  }
@@ -3,6 +3,7 @@ import { Checkbox, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInpu
3
3
  import React from 'react';
4
4
  import { MUGlobal } from './MUGlobal';
5
5
  import { ListItemRightIcon } from './ListItemRightIcon';
6
+ import { Utils } from '@etsoo/shared';
6
7
  /**
7
8
  * Extended select component
8
9
  * @param props Props
@@ -68,6 +69,16 @@ export function SelectEx(props) {
68
69
  }
69
70
  }
70
71
  };
72
+ // Get option id
73
+ const getId = (option) => {
74
+ return Reflect.get(option, idField);
75
+ };
76
+ // Get option label
77
+ const getLabel = (option) => {
78
+ return typeof labelField === 'function'
79
+ ? labelField(option)
80
+ : Reflect.get(option, labelField);
81
+ };
71
82
  // Refs
72
83
  const divRef = React.useRef();
73
84
  // When layout ready
@@ -86,6 +97,9 @@ export function SelectEx(props) {
86
97
  return;
87
98
  if (onLoadData)
88
99
  onLoadData(result);
100
+ if (search) {
101
+ Utils.addBlankItem(result, idField, labelField);
102
+ }
89
103
  setOptions(result);
90
104
  });
91
105
  }
@@ -107,18 +121,19 @@ export function SelectEx(props) {
107
121
  }, renderValue: (selected) => {
108
122
  // The text shows up
109
123
  return localOptions
110
- .filter((option) => Array.isArray(selected)
111
- ? selected.indexOf(option.id) !== -1
112
- : selected === option.id)
113
- .map((option) => option.label)
124
+ .filter((option) => {
125
+ const id = getId(option);
126
+ return Array.isArray(selected)
127
+ ? selected.indexOf(id) !== -1
128
+ : selected === id;
129
+ })
130
+ .map((option) => getLabel(option))
114
131
  .join(', ');
115
132
  }, sx: { minWidth: '150px' }, ...rest }, localOptions.map((option) => {
116
133
  // Option id
117
- const id = option[idField];
134
+ const id = getId(option);
118
135
  // Option label
119
- const label = typeof labelField === 'function'
120
- ? labelField(option)
121
- : option[labelField];
136
+ const label = getLabel(option);
122
137
  // Option
123
138
  return (React.createElement(MenuItem, { key: id, value: id, onClick: (event) => {
124
139
  if (onItemClick) {
@@ -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.77",
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.28",
53
+ "@etsoo/appscript": "^1.2.31",
54
54
  "@etsoo/notificationbase": "^1.1.0",
55
- "@etsoo/shared": "^1.1.6",
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,9 +83,9 @@
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",
88
- "eslint": "^8.7.0",
86
+ "@typescript-eslint/eslint-plugin": "^5.10.2",
87
+ "@typescript-eslint/parser": "^5.10.2",
88
+ "eslint": "^8.8.0",
89
89
  "eslint-config-airbnb-base": "^15.0.0",
90
90
  "eslint-plugin-import": "^2.25.4",
91
91
  "eslint-plugin-react": "^7.28.0",
@@ -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
+ }
@@ -3,6 +3,7 @@ import { Keyboard } from '@etsoo/shared';
3
3
  import { Autocomplete, AutocompleteRenderInputParams } from '@mui/material';
4
4
  import React from 'react';
5
5
  import { Utils } from '../app/Utils';
6
+ import { Utils as SharedUtils } from '@etsoo/shared';
6
7
  import { AutocompleteExtendedProps } from './AutocompleteExtendedProps';
7
8
  import { InputField } from './InputField';
8
9
  import { SearchField } from './SearchField';
@@ -155,6 +156,9 @@ export function ComboBox<T extends {} = IdLabelDto>(props: ComboBoxProps<T>) {
155
156
  loadData().then((result) => {
156
157
  if (result == null || !isMounted.current) return;
157
158
  if (onLoadData) onLoadData(result);
159
+ if (search) {
160
+ SharedUtils.addBlankItem(result, idField, labelField);
161
+ }
158
162
  setOptions(result);
159
163
  });
160
164
  }
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import { IdLabelDto } from '@etsoo/appscript';
2
+ import { Utils } from '@etsoo/shared';
2
3
  import React from 'react';
3
4
  import { globalApp } from '..';
4
5
  import { SelectEx, SelectExProps } from './SelectEx';
@@ -24,7 +25,7 @@ export function SelectBool(props: SelectBoolProps) {
24
25
  { id: 'true', label: globalApp.get('yes')! }
25
26
  ];
26
27
 
27
- if (search) options.unshift({ id: '', label: '---' });
28
+ if (search) Utils.addBlankItem(options);
28
29
 
29
30
  // Layout
30
31
  return <SelectEx options={options} search={search} {...rest} />;
@@ -14,6 +14,7 @@ import React from 'react';
14
14
  import { MUGlobal } from './MUGlobal';
15
15
  import { IdLabelDto } from '@etsoo/appscript';
16
16
  import { ListItemRightIcon } from './ListItemRightIcon';
17
+ import { Utils } from '@etsoo/shared';
17
18
 
18
19
  /**
19
20
  * Extended select component props
@@ -143,6 +144,18 @@ export function SelectEx<T extends {} = IdLabelDto>(props: SelectExProps<T>) {
143
144
  }
144
145
  };
145
146
 
147
+ // Get option id
148
+ const getId = (option: T) => {
149
+ return Reflect.get(option, idField);
150
+ };
151
+
152
+ // Get option label
153
+ const getLabel = (option: T) => {
154
+ return typeof labelField === 'function'
155
+ ? labelField(option)
156
+ : Reflect.get(option, labelField);
157
+ };
158
+
146
159
  // Refs
147
160
  const divRef = React.useRef<HTMLDivElement>();
148
161
 
@@ -159,6 +172,9 @@ export function SelectEx<T extends {} = IdLabelDto>(props: SelectExProps<T>) {
159
172
  loadData().then((result) => {
160
173
  if (result == null || !isMounted.current) return;
161
174
  if (onLoadData) onLoadData(result);
175
+ if (search) {
176
+ Utils.addBlankItem(result, idField, labelField);
177
+ }
162
178
  setOptions(result);
163
179
  });
164
180
  }
@@ -198,26 +214,24 @@ export function SelectEx<T extends {} = IdLabelDto>(props: SelectExProps<T>) {
198
214
  renderValue={(selected) => {
199
215
  // The text shows up
200
216
  return localOptions
201
- .filter((option: any) =>
202
- Array.isArray(selected)
203
- ? selected.indexOf(option.id) !== -1
204
- : selected === option.id
205
- )
206
- .map((option: any) => option.label)
217
+ .filter((option) => {
218
+ const id = getId(option);
219
+ return Array.isArray(selected)
220
+ ? selected.indexOf(id) !== -1
221
+ : selected === id;
222
+ })
223
+ .map((option) => getLabel(option))
207
224
  .join(', ');
208
225
  }}
209
226
  sx={{ minWidth: '150px' }}
210
227
  {...rest}
211
228
  >
212
- {localOptions.map((option: any) => {
229
+ {localOptions.map((option) => {
213
230
  // Option id
214
- const id = option[idField];
231
+ const id = getId(option);
215
232
 
216
233
  // Option label
217
- const label =
218
- typeof labelField === 'function'
219
- ? labelField(option)
220
- : option[labelField];
234
+ const label = getLabel(option);
221
235
 
222
236
  // Option
223
237
  return (
@@ -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>