@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.
- package/lib/app/ReactApp.d.ts +8 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/mu/AuditDisplay.d.ts +39 -0
- package/lib/mu/AuditDisplay.js +77 -0
- package/lib/mu/ComboBox.js +4 -0
- package/lib/mu/ListMoreDisplay.d.ts +26 -0
- package/lib/mu/ListMoreDisplay.js +85 -0
- package/lib/mu/SelectBool.js +2 -1
- package/lib/mu/SelectEx.js +23 -8
- package/lib/mu/pages/ViewPage.d.ts +2 -2
- package/lib/mu/pages/ViewPage.js +6 -5
- package/package.json +8 -8
- package/src/app/ReactApp.ts +10 -0
- package/src/index.ts +2 -0
- package/src/mu/AuditDisplay.tsx +176 -0
- package/src/mu/ComboBox.tsx +4 -0
- package/src/mu/ListMoreDisplay.tsx +157 -0
- package/src/mu/SelectBool.tsx +2 -1
- package/src/mu/SelectEx.tsx +26 -12
- package/src/mu/pages/ViewPage.tsx +14 -8
package/lib/app/ReactApp.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/lib/mu/ComboBox.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/mu/SelectBool.js
CHANGED
|
@@ -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
|
-
|
|
19
|
+
Utils.addBlankItem(options);
|
|
19
20
|
// Layout
|
|
20
21
|
return React.createElement(SelectEx, { options: options, search: search, ...rest });
|
|
21
22
|
}
|
package/lib/mu/SelectEx.js
CHANGED
|
@@ -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) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
134
|
+
const id = getId(option);
|
|
118
135
|
// Option label
|
|
119
|
-
const label =
|
|
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
|
*/
|
package/lib/mu/pages/ViewPage.js
CHANGED
|
@@ -63,13 +63,13 @@ export function ViewPage(props) {
|
|
|
63
63
|
// Data
|
|
64
64
|
const [data, setData] = React.useState();
|
|
65
65
|
// Load data
|
|
66
|
-
const
|
|
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 ?
|
|
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
|
-
|
|
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.
|
|
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.
|
|
53
|
+
"@etsoo/appscript": "^1.2.31",
|
|
54
54
|
"@etsoo/notificationbase": "^1.1.0",
|
|
55
|
-
"@etsoo/shared": "^1.1.
|
|
55
|
+
"@etsoo/shared": "^1.1.9",
|
|
56
56
|
"@mui/icons-material": "^5.3.1",
|
|
57
|
-
"@mui/material": "^5.
|
|
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.
|
|
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.
|
|
87
|
-
"@typescript-eslint/parser": "^5.10.
|
|
88
|
-
"eslint": "^8.
|
|
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",
|
package/src/app/ReactApp.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/mu/ComboBox.tsx
CHANGED
|
@@ -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
|
+
}
|
package/src/mu/SelectBool.tsx
CHANGED
|
@@ -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)
|
|
28
|
+
if (search) Utils.addBlankItem(options);
|
|
28
29
|
|
|
29
30
|
// Layout
|
|
30
31
|
return <SelectEx options={options} search={search} {...rest} />;
|
package/src/mu/SelectEx.tsx
CHANGED
|
@@ -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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
229
|
+
{localOptions.map((option) => {
|
|
213
230
|
// Option id
|
|
214
|
-
const id = option
|
|
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?:
|
|
63
|
+
actions?:
|
|
64
|
+
| React.ReactNode
|
|
65
|
+
| ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
68
|
* Children
|
|
67
69
|
*/
|
|
68
|
-
children?:
|
|
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
|
|
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 ?
|
|
177
|
-
onUpdate={supportRefresh ? undefined :
|
|
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
|
-
|
|
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>
|