@etsoo/react 1.4.76 → 1.4.80
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/Utils.d.ts +7 -0
- package/lib/app/Utils.js +26 -0
- package/lib/components/GridColumn.d.ts +1 -1
- package/lib/mu/ComboBox.js +4 -0
- package/lib/mu/DataGridEx.d.ts +9 -0
- package/lib/mu/DataGridEx.js +2 -2
- package/lib/mu/MUGlobal.d.ts +8 -0
- package/lib/mu/MUGlobal.js +3 -0
- package/lib/mu/ResponsibleContainer.d.ts +4 -0
- package/lib/mu/ResponsibleContainer.js +6 -3
- package/lib/mu/ScrollerListEx.d.ts +9 -0
- package/lib/mu/ScrollerListEx.js +5 -3
- package/lib/mu/SelectBool.js +2 -1
- package/lib/mu/SelectEx.js +23 -8
- package/lib/mu/pages/ResponsivePageProps.d.ts +4 -0
- package/lib/mu/pages/ViewPage.d.ts +2 -2
- package/lib/mu/pages/ViewPage.js +6 -5
- package/package.json +4 -4
- package/src/app/Utils.ts +32 -0
- package/src/components/GridColumn.ts +1 -1
- package/src/mu/ComboBox.tsx +4 -0
- package/src/mu/DataGridEx.tsx +19 -0
- package/src/mu/MUGlobal.ts +11 -0
- package/src/mu/ResponsibleContainer.tsx +15 -0
- package/src/mu/ScrollerListEx.tsx +31 -1
- package/src/mu/SelectBool.tsx +2 -1
- package/src/mu/SelectEx.tsx +26 -12
- package/src/mu/pages/ResponsivePageProps.ts +5 -0
- package/src/mu/pages/ViewPage.tsx +14 -8
package/lib/app/Utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
/**
|
|
2
3
|
* React utils
|
|
3
4
|
*/
|
|
@@ -8,6 +9,12 @@ export declare namespace Utils {
|
|
|
8
9
|
* @returns Formatted value
|
|
9
10
|
*/
|
|
10
11
|
function formatInputValue(value: unknown): string | number | any[] | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Is safe click
|
|
14
|
+
* @param event Mouse event
|
|
15
|
+
* @returns Result
|
|
16
|
+
*/
|
|
17
|
+
function isSafeClick(event: React.MouseEvent<HTMLElement>): boolean;
|
|
11
18
|
/**
|
|
12
19
|
* Trigger input change event
|
|
13
20
|
* @param input Form input
|
package/lib/app/Utils.js
CHANGED
|
@@ -20,6 +20,32 @@ export var Utils;
|
|
|
20
20
|
return String(value);
|
|
21
21
|
}
|
|
22
22
|
Utils.formatInputValue = formatInputValue;
|
|
23
|
+
/**
|
|
24
|
+
* Is safe click
|
|
25
|
+
* @param event Mouse event
|
|
26
|
+
* @returns Result
|
|
27
|
+
*/
|
|
28
|
+
function isSafeClick(event) {
|
|
29
|
+
// No target
|
|
30
|
+
// HTMLElement <= Element, SVGElement <= Element
|
|
31
|
+
if (!(event.target instanceof Element))
|
|
32
|
+
return true;
|
|
33
|
+
// Outside of the currentTarget
|
|
34
|
+
let target = event.target;
|
|
35
|
+
if (!event.currentTarget.contains(target))
|
|
36
|
+
return false;
|
|
37
|
+
while (target != null && target != event.currentTarget) {
|
|
38
|
+
const nodeName = target.nodeName.toUpperCase();
|
|
39
|
+
if (nodeName === 'INPUT' ||
|
|
40
|
+
nodeName === 'BUTTON' ||
|
|
41
|
+
nodeName === 'A' ||
|
|
42
|
+
target.hasAttribute('onClick'))
|
|
43
|
+
return false;
|
|
44
|
+
target = target.parentElement;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
Utils.isSafeClick = isSafeClick;
|
|
23
49
|
/**
|
|
24
50
|
* Trigger input change event
|
|
25
51
|
* @param input Form input
|
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
|
}
|
package/lib/mu/DataGridEx.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { GridColumn } from '../components/GridColumn';
|
|
3
3
|
import { GridLoaderStates } from '../components/GridLoader';
|
|
4
4
|
import { ScrollerGridProps } from '../components/ScrollerGrid';
|
|
5
|
+
import { MouseEventWithDataHandler } from './MUGlobal';
|
|
5
6
|
/**
|
|
6
7
|
* Footer item renderer props
|
|
7
8
|
*/
|
|
@@ -55,6 +56,14 @@ export interface DataGridExProps<T extends Record<string, any>> extends Omit<Scr
|
|
|
55
56
|
* Hover color
|
|
56
57
|
*/
|
|
57
58
|
hoverColor?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Double click handler
|
|
61
|
+
*/
|
|
62
|
+
onDoubleClick?: MouseEventWithDataHandler<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Click handler
|
|
65
|
+
*/
|
|
66
|
+
onClick?: MouseEventWithDataHandler<T>;
|
|
58
67
|
/**
|
|
59
68
|
* Selectable to support hover over and out effect and row clickable
|
|
60
69
|
* @default true
|
package/lib/mu/DataGridEx.js
CHANGED
|
@@ -126,7 +126,7 @@ export function DataGridEx(props) {
|
|
|
126
126
|
})));
|
|
127
127
|
}
|
|
128
128
|
// Destruct
|
|
129
|
-
const { alternatingColors = [theme.palette.grey[100], undefined], borderRowsCount, bottomHeight = 53, checkable = false, className, columns, defaultOrderBy, height, headerHeight = 56, headerRenderer = defaultHeaderRenderer, footerRenderer = defaultFooterRenderer, footerItemRenderer = DataGridRenderers.defaultFooterItemRenderer, hideFooter = false, hoverColor = '#f6f9fb', idField = 'id', mRef = React.createRef(), selectable = true, selectedColor = '#edf4fb', width, ...rest } = props;
|
|
129
|
+
const { alternatingColors = [theme.palette.grey[100], undefined], borderRowsCount, bottomHeight = 53, checkable = false, className, columns, defaultOrderBy, height, headerHeight = 56, headerRenderer = defaultHeaderRenderer, footerRenderer = defaultFooterRenderer, footerItemRenderer = DataGridRenderers.defaultFooterItemRenderer, hideFooter = false, hoverColor = '#f6f9fb', idField = 'id', mRef = React.createRef(), onClick, onDoubleClick, selectable = true, selectedColor = '#edf4fb', width, ...rest } = props;
|
|
130
130
|
if (checkable) {
|
|
131
131
|
const cbColumn = {
|
|
132
132
|
field: 'selected',
|
|
@@ -261,7 +261,7 @@ export function DataGridEx(props) {
|
|
|
261
261
|
cellProps,
|
|
262
262
|
renderProps
|
|
263
263
|
});
|
|
264
|
-
return (React.createElement("div", { className: rowClass, style: style, "data-row": rowIndex, "data-column": columnIndex, onMouseDown: selectable && !checkable ? handleMouseDown : undefined, onMouseOver: selectable ? handleMouseOver : undefined, onMouseOut: selectable ? handleMouseOut : undefined },
|
|
264
|
+
return (React.createElement("div", { className: rowClass, style: style, "data-row": rowIndex, "data-column": columnIndex, onMouseDown: selectable && !checkable ? handleMouseDown : undefined, onMouseOver: selectable ? handleMouseOver : undefined, onMouseOut: selectable ? handleMouseOut : undefined, onClick: (event) => onClick && data != null && onClick(event, data), onDoubleClick: (event) => onDoubleClick && data != null && onDoubleClick(event, data) },
|
|
265
265
|
React.createElement(Box, { ...cellProps, onMouseEnter: handleMouseEnter }, child)));
|
|
266
266
|
};
|
|
267
267
|
// Column width calculator
|
package/lib/mu/MUGlobal.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
1
2
|
import { Theme } from '@mui/material';
|
|
3
|
+
/**
|
|
4
|
+
* Mouse event handler with data
|
|
5
|
+
*/
|
|
6
|
+
export declare type MouseEventWithDataHandler<T> = (event: React.MouseEvent<HTMLDivElement>, data: T) => void;
|
|
7
|
+
/**
|
|
8
|
+
* MUGlobal for global configurations
|
|
9
|
+
*/
|
|
2
10
|
export declare class MUGlobal {
|
|
3
11
|
/**
|
|
4
12
|
* Search field shrink
|
package/lib/mu/MUGlobal.js
CHANGED
|
@@ -72,6 +72,10 @@ export interface ResponsibleContainerProps<T extends {}, F extends DataTypes.Bas
|
|
|
72
72
|
* Pull to refresh data
|
|
73
73
|
*/
|
|
74
74
|
pullToRefresh?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Quick action for double click or click under mobile
|
|
77
|
+
*/
|
|
78
|
+
quickAction?: (data: T) => void;
|
|
75
79
|
/**
|
|
76
80
|
* Size ready to read miliseconds span
|
|
77
81
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box, Stack } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Labels } from '../app/Labels';
|
|
4
|
+
import { Utils } from '../app/Utils';
|
|
4
5
|
import { GridDataGet } from '../components/GridLoader';
|
|
5
6
|
import useCombinedRefs from '../uses/useCombinedRefs';
|
|
6
7
|
import { useDimensions } from '../uses/useDimensions';
|
|
@@ -24,7 +25,7 @@ function defaultContainerBoxSx(paddings, hasField, _dataGrid) {
|
|
|
24
25
|
*/
|
|
25
26
|
export function ResponsibleContainer(props) {
|
|
26
27
|
// Destruct
|
|
27
|
-
const { adjustHeight, columns, containerBoxSx = defaultContainerBoxSx, dataGridMinWidth = Math.max(576, DataGridExCalColumns(columns).total), elementReady, fields, fieldTemplate, height, loadData, mRef, paddings = MUGlobal.pagePaddings, pullToRefresh = true, sizeReadyMiliseconds = 0, ...rest } = props;
|
|
28
|
+
const { adjustHeight, columns, containerBoxSx = defaultContainerBoxSx, dataGridMinWidth = Math.max(576, DataGridExCalColumns(columns).total), elementReady, fields, fieldTemplate, height, loadData, mRef, paddings = MUGlobal.pagePaddings, pullToRefresh = true, quickAction, sizeReadyMiliseconds = 0, ...rest } = props;
|
|
28
29
|
// Labels
|
|
29
30
|
const labels = Labels.CommonPage;
|
|
30
31
|
// Refs
|
|
@@ -106,7 +107,7 @@ export function ResponsibleContainer(props) {
|
|
|
106
107
|
delete rest.itemRenderer;
|
|
107
108
|
return [
|
|
108
109
|
React.createElement(Box, { className: "DataGridBox" },
|
|
109
|
-
React.createElement(DataGridEx, { autoLoad: !hasFields, height: heightLocal, width: rect.width, loadData: localLoadData, mRef: mRefs, outerRef: (element) => {
|
|
110
|
+
React.createElement(DataGridEx, { autoLoad: !hasFields, height: heightLocal, width: rect.width, loadData: localLoadData, mRef: mRefs, onDoubleClick: (_, data) => quickAction && quickAction(data), outerRef: (element) => {
|
|
110
111
|
if (element != null && elementReady)
|
|
111
112
|
elementReady(element, true);
|
|
112
113
|
}, columns: columns, ...rest })),
|
|
@@ -124,7 +125,9 @@ export function ResponsibleContainer(props) {
|
|
|
124
125
|
delete rest.selectable;
|
|
125
126
|
return [
|
|
126
127
|
React.createElement(Box, { className: "ListBox", sx: { height: heightLocal } },
|
|
127
|
-
React.createElement(ScrollerListEx, { autoLoad: !hasFields, height: heightLocal, loadData: localLoadData, mRef: mRefs,
|
|
128
|
+
React.createElement(ScrollerListEx, { autoLoad: !hasFields, height: heightLocal, loadData: localLoadData, mRef: mRefs, onClick: (event, data) => quickAction &&
|
|
129
|
+
Utils.isSafeClick(event) &&
|
|
130
|
+
quickAction(data), oRef: (element) => {
|
|
128
131
|
if (element != null && elementReady)
|
|
129
132
|
elementReady(element, false);
|
|
130
133
|
}, ...rest })),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ListChildComponentProps } from 'react-window';
|
|
3
3
|
import { ScrollerListProps } from '../components/ScrollerList';
|
|
4
|
+
import { MouseEventWithDataHandler } from './MUGlobal';
|
|
4
5
|
/**
|
|
5
6
|
* Extended ScrollerList inner item renderer props
|
|
6
7
|
*/
|
|
@@ -53,6 +54,14 @@ export interface ScrollerListExProps<T> extends Omit<ScrollerListProps<T>, 'item
|
|
|
53
54
|
* Item size, a function indicates its a variable size list
|
|
54
55
|
*/
|
|
55
56
|
itemSize: ScrollerListExItemSize;
|
|
57
|
+
/**
|
|
58
|
+
* Double click handler
|
|
59
|
+
*/
|
|
60
|
+
onDoubleClick?: MouseEventWithDataHandler<T>;
|
|
61
|
+
/**
|
|
62
|
+
* Click handler
|
|
63
|
+
*/
|
|
64
|
+
onClick?: MouseEventWithDataHandler<T>;
|
|
56
65
|
/**
|
|
57
66
|
* On items select change
|
|
58
67
|
*/
|
package/lib/mu/ScrollerListEx.js
CHANGED
|
@@ -66,7 +66,7 @@ const defaultMargin = (margin, isNarrow) => {
|
|
|
66
66
|
};
|
|
67
67
|
};
|
|
68
68
|
// Default itemRenderer
|
|
69
|
-
function defaultItemRenderer({ index, innerItemRenderer, data, onMouseDown, selected, style, itemHeight, space, margins }) {
|
|
69
|
+
function defaultItemRenderer({ index, innerItemRenderer, data, onMouseDown, selected, style, itemHeight, onClick, onDoubleClick, space, margins }) {
|
|
70
70
|
// Child
|
|
71
71
|
const child = innerItemRenderer({
|
|
72
72
|
index,
|
|
@@ -81,7 +81,7 @@ function defaultItemRenderer({ index, innerItemRenderer, data, onMouseDown, sele
|
|
|
81
81
|
if (selected)
|
|
82
82
|
rowClass += ` ${selectedClassName}`;
|
|
83
83
|
// Layout
|
|
84
|
-
return (React.createElement("div", { className: rowClass, style: style, onMouseDown: (event) => onMouseDown(event.currentTarget, data) }, child));
|
|
84
|
+
return (React.createElement("div", { className: rowClass, style: style, onMouseDown: (event) => onMouseDown(event.currentTarget, data), onClick: (event) => onClick && onClick(event, data), onDoubleClick: (event) => onDoubleClick && onDoubleClick(event, data) }, child));
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
87
|
* Extended ScrollerList
|
|
@@ -118,12 +118,14 @@ export function ScrollerListEx(props) {
|
|
|
118
118
|
itemHeight,
|
|
119
119
|
innerItemRenderer,
|
|
120
120
|
onMouseDown,
|
|
121
|
+
onClick,
|
|
122
|
+
onDoubleClick,
|
|
121
123
|
space,
|
|
122
124
|
margins,
|
|
123
125
|
selected: isSelected(itemProps.data),
|
|
124
126
|
...itemProps
|
|
125
127
|
});
|
|
126
|
-
}, onSelectChange, selectedColor = '#edf4fb', ...rest } = props;
|
|
128
|
+
}, onClick, onDoubleClick, onSelectChange, selectedColor = '#edf4fb', ...rest } = props;
|
|
127
129
|
// Theme
|
|
128
130
|
const theme = useTheme();
|
|
129
131
|
// Cache calculation
|
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.80",
|
|
4
4
|
"description": "TypeScript ReactJs framework",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -50,9 +50,9 @@
|
|
|
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.29",
|
|
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
57
|
"@mui/material": "^5.3.1",
|
|
58
58
|
"@reach/router": "^1.3.4",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"@types/react-test-renderer": "^17.0.1",
|
|
86
86
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
|
87
87
|
"@typescript-eslint/parser": "^5.10.1",
|
|
88
|
-
"eslint": "^8.
|
|
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/Utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* React utils
|
|
3
5
|
*/
|
|
@@ -19,6 +21,36 @@ export namespace Utils {
|
|
|
19
21
|
return String(value);
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Is safe click
|
|
26
|
+
* @param event Mouse event
|
|
27
|
+
* @returns Result
|
|
28
|
+
*/
|
|
29
|
+
export function isSafeClick(event: React.MouseEvent<HTMLElement>) {
|
|
30
|
+
// No target
|
|
31
|
+
// HTMLElement <= Element, SVGElement <= Element
|
|
32
|
+
if (!(event.target instanceof Element)) return true;
|
|
33
|
+
|
|
34
|
+
// Outside of the currentTarget
|
|
35
|
+
let target: Element | null = event.target;
|
|
36
|
+
if (!event.currentTarget.contains(target)) return false;
|
|
37
|
+
|
|
38
|
+
while (target != null && target != event.currentTarget) {
|
|
39
|
+
const nodeName = target.nodeName.toUpperCase();
|
|
40
|
+
if (
|
|
41
|
+
nodeName === 'INPUT' ||
|
|
42
|
+
nodeName === 'BUTTON' ||
|
|
43
|
+
nodeName === 'A' ||
|
|
44
|
+
target.hasAttribute('onClick')
|
|
45
|
+
)
|
|
46
|
+
return false;
|
|
47
|
+
|
|
48
|
+
target = target.parentElement;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
22
54
|
/**
|
|
23
55
|
* Trigger input change event
|
|
24
56
|
* @param input Form input
|
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
|
}
|
package/src/mu/DataGridEx.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../components/ScrollerGrid';
|
|
26
26
|
import useCombinedRefs from '../uses/useCombinedRefs';
|
|
27
27
|
import { DataGridRenderers } from './DataGridRenderers';
|
|
28
|
+
import { MouseEventWithDataHandler } from './MUGlobal';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Footer item renderer props
|
|
@@ -98,6 +99,16 @@ export interface DataGridExProps<T extends Record<string, any>>
|
|
|
98
99
|
*/
|
|
99
100
|
hoverColor?: string;
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Double click handler
|
|
104
|
+
*/
|
|
105
|
+
onDoubleClick?: MouseEventWithDataHandler<T>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Click handler
|
|
109
|
+
*/
|
|
110
|
+
onClick?: MouseEventWithDataHandler<T>;
|
|
111
|
+
|
|
101
112
|
/**
|
|
102
113
|
* Selectable to support hover over and out effect and row clickable
|
|
103
114
|
* @default true
|
|
@@ -351,6 +362,8 @@ export function DataGridEx<T extends Record<string, any>>(
|
|
|
351
362
|
hoverColor = '#f6f9fb',
|
|
352
363
|
idField = 'id',
|
|
353
364
|
mRef = React.createRef(),
|
|
365
|
+
onClick,
|
|
366
|
+
onDoubleClick,
|
|
354
367
|
selectable = true,
|
|
355
368
|
selectedColor = '#edf4fb',
|
|
356
369
|
width,
|
|
@@ -567,6 +580,12 @@ export function DataGridEx<T extends Record<string, any>>(
|
|
|
567
580
|
}
|
|
568
581
|
onMouseOver={selectable ? handleMouseOver : undefined}
|
|
569
582
|
onMouseOut={selectable ? handleMouseOut : undefined}
|
|
583
|
+
onClick={(event) =>
|
|
584
|
+
onClick && data != null && onClick(event, data)
|
|
585
|
+
}
|
|
586
|
+
onDoubleClick={(event) =>
|
|
587
|
+
onDoubleClick && data != null && onDoubleClick(event, data)
|
|
588
|
+
}
|
|
570
589
|
>
|
|
571
590
|
<Box {...cellProps} onMouseEnter={handleMouseEnter}>
|
|
572
591
|
{child}
|
package/src/mu/MUGlobal.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { NumberUtils } from '@etsoo/shared';
|
|
2
2
|
import { Breakpoint, Theme } from '@mui/material';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Mouse event handler with data
|
|
6
|
+
*/
|
|
7
|
+
export type MouseEventWithDataHandler<T> = (
|
|
8
|
+
event: React.MouseEvent<HTMLDivElement>,
|
|
9
|
+
data: T
|
|
10
|
+
) => void;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MUGlobal for global configurations
|
|
14
|
+
*/
|
|
4
15
|
export class MUGlobal {
|
|
5
16
|
/**
|
|
6
17
|
* Search field shrink
|
|
@@ -3,6 +3,7 @@ import { Box, Stack, SxProps, Theme } from '@mui/material';
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { ListChildComponentProps } from 'react-window';
|
|
5
5
|
import { Labels } from '../app/Labels';
|
|
6
|
+
import { Utils } from '../app/Utils';
|
|
6
7
|
import { GridColumn } from '../components/GridColumn';
|
|
7
8
|
import {
|
|
8
9
|
GridDataGet,
|
|
@@ -125,6 +126,11 @@ export interface ResponsibleContainerProps<
|
|
|
125
126
|
*/
|
|
126
127
|
pullToRefresh?: boolean;
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Quick action for double click or click under mobile
|
|
131
|
+
*/
|
|
132
|
+
quickAction?: (data: T) => void;
|
|
133
|
+
|
|
128
134
|
/**
|
|
129
135
|
* Size ready to read miliseconds span
|
|
130
136
|
*/
|
|
@@ -173,6 +179,7 @@ export function ResponsibleContainer<
|
|
|
173
179
|
mRef,
|
|
174
180
|
paddings = MUGlobal.pagePaddings,
|
|
175
181
|
pullToRefresh = true,
|
|
182
|
+
quickAction,
|
|
176
183
|
sizeReadyMiliseconds = 0,
|
|
177
184
|
...rest
|
|
178
185
|
} = props;
|
|
@@ -285,6 +292,9 @@ export function ResponsibleContainer<
|
|
|
285
292
|
width={rect.width}
|
|
286
293
|
loadData={localLoadData}
|
|
287
294
|
mRef={mRefs}
|
|
295
|
+
onDoubleClick={(_, data) =>
|
|
296
|
+
quickAction && quickAction(data)
|
|
297
|
+
}
|
|
288
298
|
outerRef={(element?: HTMLDivElement) => {
|
|
289
299
|
if (element != null && elementReady)
|
|
290
300
|
elementReady(element, true);
|
|
@@ -314,6 +324,11 @@ export function ResponsibleContainer<
|
|
|
314
324
|
height={heightLocal}
|
|
315
325
|
loadData={localLoadData}
|
|
316
326
|
mRef={mRefs}
|
|
327
|
+
onClick={(event, data) =>
|
|
328
|
+
quickAction &&
|
|
329
|
+
Utils.isSafeClick(event) &&
|
|
330
|
+
quickAction(data)
|
|
331
|
+
}
|
|
317
332
|
oRef={(element) => {
|
|
318
333
|
if (element != null && elementReady)
|
|
319
334
|
elementReady(element, false);
|
|
@@ -4,7 +4,7 @@ import { useTheme } from '@mui/material';
|
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import { ListChildComponentProps } from 'react-window';
|
|
6
6
|
import { ScrollerList, ScrollerListProps } from '../components/ScrollerList';
|
|
7
|
-
import { MUGlobal } from './MUGlobal';
|
|
7
|
+
import { MouseEventWithDataHandler, MUGlobal } from './MUGlobal';
|
|
8
8
|
|
|
9
9
|
// Scroll bar size
|
|
10
10
|
const scrollbarSize = 16;
|
|
@@ -146,6 +146,16 @@ export interface ScrollerListExProps<T>
|
|
|
146
146
|
*/
|
|
147
147
|
itemSize: ScrollerListExItemSize;
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Double click handler
|
|
151
|
+
*/
|
|
152
|
+
onDoubleClick?: MouseEventWithDataHandler<T>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Click handler
|
|
156
|
+
*/
|
|
157
|
+
onClick?: MouseEventWithDataHandler<T>;
|
|
158
|
+
|
|
149
159
|
/**
|
|
150
160
|
* On items select change
|
|
151
161
|
*/
|
|
@@ -175,6 +185,16 @@ interface defaultItemRendererProps<T> extends ListChildComponentProps<T> {
|
|
|
175
185
|
*/
|
|
176
186
|
itemHeight: number;
|
|
177
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Double click handler
|
|
190
|
+
*/
|
|
191
|
+
onDoubleClick?: MouseEventWithDataHandler<T>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Click handler
|
|
195
|
+
*/
|
|
196
|
+
onClick?: MouseEventWithDataHandler<T>;
|
|
197
|
+
|
|
178
198
|
/**
|
|
179
199
|
* Item space
|
|
180
200
|
*/
|
|
@@ -200,6 +220,8 @@ function defaultItemRenderer<T>({
|
|
|
200
220
|
selected,
|
|
201
221
|
style,
|
|
202
222
|
itemHeight,
|
|
223
|
+
onClick,
|
|
224
|
+
onDoubleClick,
|
|
203
225
|
space,
|
|
204
226
|
margins
|
|
205
227
|
}: defaultItemRendererProps<T>) {
|
|
@@ -223,6 +245,10 @@ function defaultItemRenderer<T>({
|
|
|
223
245
|
className={rowClass}
|
|
224
246
|
style={style}
|
|
225
247
|
onMouseDown={(event) => onMouseDown(event.currentTarget, data)}
|
|
248
|
+
onClick={(event) => onClick && onClick(event, data)}
|
|
249
|
+
onDoubleClick={(event) =>
|
|
250
|
+
onDoubleClick && onDoubleClick(event, data)
|
|
251
|
+
}
|
|
226
252
|
>
|
|
227
253
|
{child}
|
|
228
254
|
</div>
|
|
@@ -282,12 +308,16 @@ export function ScrollerListEx<T extends Record<string, unknown>>(
|
|
|
282
308
|
itemHeight,
|
|
283
309
|
innerItemRenderer,
|
|
284
310
|
onMouseDown,
|
|
311
|
+
onClick,
|
|
312
|
+
onDoubleClick,
|
|
285
313
|
space,
|
|
286
314
|
margins,
|
|
287
315
|
selected: isSelected(itemProps.data),
|
|
288
316
|
...itemProps
|
|
289
317
|
});
|
|
290
318
|
},
|
|
319
|
+
onClick,
|
|
320
|
+
onDoubleClick,
|
|
291
321
|
onSelectChange,
|
|
292
322
|
selectedColor = '#edf4fb',
|
|
293
323
|
...rest
|
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>
|