@etsoo/materialui 1.0.1
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/.eslintignore +3 -0
- package/.eslintrc.json +38 -0
- package/.gitattributes +2 -0
- package/.github/workflows/main.yml +48 -0
- package/.prettierignore +5 -0
- package/.prettierrc +6 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/__tests__/ComboBox.tsx +30 -0
- package/__tests__/MUGlobalTests.tsx +58 -0
- package/__tests__/NotifierMUTests.tsx +217 -0
- package/__tests__/SelectEx.tsx +26 -0
- package/__tests__/tsconfig.json +19 -0
- package/babel.config.json +11 -0
- package/lib/AuditDisplay.d.ts +33 -0
- package/lib/AuditDisplay.js +52 -0
- package/lib/AutocompleteExtendedProps.d.ts +64 -0
- package/lib/AutocompleteExtendedProps.js +1 -0
- package/lib/BackButton.d.ts +13 -0
- package/lib/BackButton.js +33 -0
- package/lib/BridgeCloseButton.d.ts +23 -0
- package/lib/BridgeCloseButton.js +32 -0
- package/lib/ButtonLink.d.ts +17 -0
- package/lib/ButtonLink.js +19 -0
- package/lib/ComboBox.d.ts +38 -0
- package/lib/ComboBox.js +108 -0
- package/lib/CountdownButton.d.ts +23 -0
- package/lib/CountdownButton.js +81 -0
- package/lib/CustomFabProps.d.ts +27 -0
- package/lib/CustomFabProps.js +1 -0
- package/lib/DataGridEx.d.ts +94 -0
- package/lib/DataGridEx.js +329 -0
- package/lib/DataGridRenderers.d.ts +22 -0
- package/lib/DataGridRenderers.js +99 -0
- package/lib/DialogButton.d.ts +54 -0
- package/lib/DialogButton.js +45 -0
- package/lib/DnDList.d.ts +87 -0
- package/lib/DnDList.js +153 -0
- package/lib/DraggablePaperComponent.d.ts +8 -0
- package/lib/DraggablePaperComponent.js +12 -0
- package/lib/EmailInput.d.ts +11 -0
- package/lib/EmailInput.js +15 -0
- package/lib/FabBox.d.ts +21 -0
- package/lib/FabBox.js +31 -0
- package/lib/FlexBox.d.ts +14 -0
- package/lib/FlexBox.js +18 -0
- package/lib/GridDataFormat.d.ts +10 -0
- package/lib/GridDataFormat.js +43 -0
- package/lib/IconButtonLink.d.ts +17 -0
- package/lib/IconButtonLink.js +16 -0
- package/lib/InputField.d.ts +21 -0
- package/lib/InputField.js +39 -0
- package/lib/ItemList.d.ts +56 -0
- package/lib/ItemList.js +69 -0
- package/lib/ListItemRightIcon.d.ts +4 -0
- package/lib/ListItemRightIcon.js +8 -0
- package/lib/ListMoreDisplay.d.ts +35 -0
- package/lib/ListMoreDisplay.js +99 -0
- package/lib/LoadingButton.d.ts +16 -0
- package/lib/LoadingButton.js +41 -0
- package/lib/MUGlobal.d.ts +102 -0
- package/lib/MUGlobal.js +184 -0
- package/lib/MaskInput.d.ts +34 -0
- package/lib/MaskInput.js +43 -0
- package/lib/MobileListItemRenderer.d.ts +17 -0
- package/lib/MobileListItemRenderer.js +35 -0
- package/lib/MoreFab.d.ts +45 -0
- package/lib/MoreFab.js +95 -0
- package/lib/NotifierMU.d.ts +47 -0
- package/lib/NotifierMU.js +387 -0
- package/lib/NotifierPromptProps.d.ts +22 -0
- package/lib/NotifierPromptProps.js +1 -0
- package/lib/OptionGroup.d.ts +58 -0
- package/lib/OptionGroup.js +81 -0
- package/lib/PList.d.ts +15 -0
- package/lib/PList.js +12 -0
- package/lib/ProgressCount.d.ts +44 -0
- package/lib/ProgressCount.js +79 -0
- package/lib/PullToRefreshUI.d.ts +9 -0
- package/lib/PullToRefreshUI.js +18 -0
- package/lib/RLink.d.ts +14 -0
- package/lib/RLink.js +37 -0
- package/lib/ResponsibleContainer.d.ts +87 -0
- package/lib/ResponsibleContainer.js +156 -0
- package/lib/ScrollTopFab.d.ts +7 -0
- package/lib/ScrollTopFab.js +25 -0
- package/lib/ScrollerListEx.d.ts +81 -0
- package/lib/ScrollerListEx.js +167 -0
- package/lib/SearchBar.d.ts +29 -0
- package/lib/SearchBar.js +260 -0
- package/lib/SearchField.d.ts +21 -0
- package/lib/SearchField.js +39 -0
- package/lib/SearchOptionGroup.d.ts +9 -0
- package/lib/SearchOptionGroup.js +14 -0
- package/lib/SelectBool.d.ts +13 -0
- package/lib/SelectBool.js +22 -0
- package/lib/SelectEx.d.ts +50 -0
- package/lib/SelectEx.js +156 -0
- package/lib/ShowDataComparison.d.ts +20 -0
- package/lib/ShowDataComparison.js +58 -0
- package/lib/Switch.d.ts +29 -0
- package/lib/Switch.js +34 -0
- package/lib/SwitchAnt.d.ts +25 -0
- package/lib/SwitchAnt.js +40 -0
- package/lib/TabBox.d.ts +54 -0
- package/lib/TabBox.js +31 -0
- package/lib/TableEx.d.ts +65 -0
- package/lib/TableEx.js +270 -0
- package/lib/TextFieldEx.d.ts +101 -0
- package/lib/TextFieldEx.js +126 -0
- package/lib/Tiplist.d.ts +18 -0
- package/lib/Tiplist.js +157 -0
- package/lib/TooltipClick.d.ts +15 -0
- package/lib/TooltipClick.js +40 -0
- package/lib/UserAvatar.d.ts +24 -0
- package/lib/UserAvatar.js +25 -0
- package/lib/UserAvatarEditor.d.ts +53 -0
- package/lib/UserAvatarEditor.js +129 -0
- package/lib/app/CommonApp.d.ts +38 -0
- package/lib/app/CommonApp.js +149 -0
- package/lib/app/IServiceAppSettings.d.ts +11 -0
- package/lib/app/IServiceAppSettings.js +1 -0
- package/lib/app/IServicePage.d.ts +6 -0
- package/lib/app/IServicePage.js +1 -0
- package/lib/app/IServiceUser.d.ts +14 -0
- package/lib/app/IServiceUser.js +1 -0
- package/lib/app/ISmartERPUser.d.ts +14 -0
- package/lib/app/ISmartERPUser.js +1 -0
- package/lib/app/Labels.d.ts +65 -0
- package/lib/app/Labels.js +62 -0
- package/lib/app/ReactApp.d.ts +195 -0
- package/lib/app/ReactApp.js +296 -0
- package/lib/app/ServiceApp.d.ts +78 -0
- package/lib/app/ServiceApp.js +244 -0
- package/lib/index.d.ts +74 -0
- package/lib/index.js +74 -0
- package/lib/pages/CommonPage.d.ts +11 -0
- package/lib/pages/CommonPage.js +60 -0
- package/lib/pages/CommonPageProps.d.ts +59 -0
- package/lib/pages/CommonPageProps.js +1 -0
- package/lib/pages/DataGridPage.d.ts +9 -0
- package/lib/pages/DataGridPage.js +79 -0
- package/lib/pages/DataGridPageProps.d.ts +17 -0
- package/lib/pages/DataGridPageProps.js +1 -0
- package/lib/pages/EditPage.d.ts +33 -0
- package/lib/pages/EditPage.js +29 -0
- package/lib/pages/FixedListPage.d.ts +15 -0
- package/lib/pages/FixedListPage.js +70 -0
- package/lib/pages/ListPage.d.ts +9 -0
- package/lib/pages/ListPage.js +50 -0
- package/lib/pages/ListPageProps.d.ts +7 -0
- package/lib/pages/ListPageProps.js +1 -0
- package/lib/pages/ResponsivePage.d.ts +9 -0
- package/lib/pages/ResponsivePage.js +45 -0
- package/lib/pages/ResponsivePageProps.d.ts +39 -0
- package/lib/pages/ResponsivePageProps.js +1 -0
- package/lib/pages/SearchPageProps.d.ts +30 -0
- package/lib/pages/SearchPageProps.js +1 -0
- package/lib/pages/TablePage.d.ts +9 -0
- package/lib/pages/TablePage.js +69 -0
- package/lib/pages/TablePageProps.d.ts +7 -0
- package/lib/pages/TablePageProps.js +1 -0
- package/lib/pages/ViewPage.d.ts +66 -0
- package/lib/pages/ViewPage.js +105 -0
- package/lib/texts/DateText.d.ts +34 -0
- package/lib/texts/DateText.js +25 -0
- package/lib/texts/MoneyText.d.ts +21 -0
- package/lib/texts/MoneyText.js +14 -0
- package/lib/texts/NumberText.d.ts +25 -0
- package/lib/texts/NumberText.js +14 -0
- package/package.json +97 -0
- package/src/AuditDisplay.tsx +114 -0
- package/src/AutocompleteExtendedProps.ts +83 -0
- package/src/BackButton.tsx +55 -0
- package/src/BridgeCloseButton.tsx +69 -0
- package/src/ButtonLink.tsx +32 -0
- package/src/ComboBox.tsx +251 -0
- package/src/CountdownButton.tsx +119 -0
- package/src/CustomFabProps.ts +32 -0
- package/src/DataGridEx.tsx +713 -0
- package/src/DataGridRenderers.tsx +140 -0
- package/src/DialogButton.tsx +163 -0
- package/src/DnDList.tsx +344 -0
- package/src/DraggablePaperComponent.tsx +19 -0
- package/src/EmailInput.tsx +24 -0
- package/src/FabBox.tsx +51 -0
- package/src/FlexBox.tsx +20 -0
- package/src/GridDataFormat.tsx +77 -0
- package/src/IconButtonLink.tsx +29 -0
- package/src/InputField.tsx +82 -0
- package/src/ItemList.tsx +204 -0
- package/src/ListItemRightIcon.tsx +9 -0
- package/src/ListMoreDisplay.tsx +205 -0
- package/src/LoadingButton.tsx +75 -0
- package/src/MUGlobal.ts +220 -0
- package/src/MaskInput.tsx +107 -0
- package/src/MobileListItemRenderer.tsx +79 -0
- package/src/MoreFab.tsx +211 -0
- package/src/NotifierMU.tsx +654 -0
- package/src/NotifierPromptProps.ts +24 -0
- package/src/OptionGroup.tsx +223 -0
- package/src/PList.tsx +27 -0
- package/src/ProgressCount.tsx +166 -0
- package/src/PullToRefreshUI.tsx +21 -0
- package/src/RLink.tsx +64 -0
- package/src/ResponsibleContainer.tsx +394 -0
- package/src/ScrollTopFab.tsx +34 -0
- package/src/ScrollerListEx.tsx +387 -0
- package/src/SearchBar.tsx +396 -0
- package/src/SearchField.tsx +82 -0
- package/src/SearchOptionGroup.tsx +31 -0
- package/src/SelectBool.tsx +33 -0
- package/src/SelectEx.tsx +290 -0
- package/src/ShowDataComparison.tsx +106 -0
- package/src/Switch.tsx +94 -0
- package/src/SwitchAnt.tsx +95 -0
- package/src/TabBox.tsx +118 -0
- package/src/TableEx.tsx +558 -0
- package/src/TextFieldEx.tsx +249 -0
- package/src/Tiplist.tsx +303 -0
- package/src/TooltipClick.tsx +84 -0
- package/src/UserAvatar.tsx +64 -0
- package/src/UserAvatarEditor.tsx +287 -0
- package/src/app/CommonApp.ts +223 -0
- package/src/app/IServiceAppSettings.ts +13 -0
- package/src/app/IServicePage.ts +6 -0
- package/src/app/IServiceUser.ts +17 -0
- package/src/app/ISmartERPUser.ts +16 -0
- package/src/app/Labels.ts +77 -0
- package/src/app/ReactApp.ts +504 -0
- package/src/app/ServiceApp.ts +352 -0
- package/src/index.ts +77 -0
- package/src/pages/CommonPage.tsx +128 -0
- package/src/pages/CommonPageProps.ts +70 -0
- package/src/pages/DataGridPage.tsx +140 -0
- package/src/pages/DataGridPageProps.ts +24 -0
- package/src/pages/EditPage.tsx +114 -0
- package/src/pages/FixedListPage.tsx +141 -0
- package/src/pages/ListPage.tsx +90 -0
- package/src/pages/ListPageProps.ts +12 -0
- package/src/pages/ResponsivePage.tsx +68 -0
- package/src/pages/ResponsivePageProps.ts +57 -0
- package/src/pages/SearchPageProps.ts +39 -0
- package/src/pages/TablePage.tsx +126 -0
- package/src/pages/TablePageProps.ts +12 -0
- package/src/pages/ViewPage.tsx +282 -0
- package/src/texts/DateText.tsx +74 -0
- package/src/texts/MoneyText.tsx +49 -0
- package/src/texts/NumberText.tsx +40 -0
- package/tsconfig.json +19 -0
package/lib/Tiplist.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ReactUtils, useDelayedExecutor } from '@etsoo/react';
|
|
2
|
+
import { DataTypes } from '@etsoo/shared';
|
|
3
|
+
import { Autocomplete } from '@mui/material';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { InputField } from './InputField';
|
|
6
|
+
import { SearchField } from './SearchField';
|
|
7
|
+
/**
|
|
8
|
+
* Tiplist
|
|
9
|
+
* @param props Props
|
|
10
|
+
* @returns Component
|
|
11
|
+
*/
|
|
12
|
+
export function Tiplist(props) {
|
|
13
|
+
// Destruct
|
|
14
|
+
const { search = false, idField = 'id', idValue, inputAutoComplete = 'off', inputError, inputHelperText, inputMargin, inputOnChange, inputRequired, inputVariant, label, loadData, defaultValue, value, name, readOnly, onChange, openOnFocus = true, sx = { minWidth: '180px' }, ...rest } = props;
|
|
15
|
+
// Value input ref
|
|
16
|
+
const inputRef = React.createRef();
|
|
17
|
+
// Local value
|
|
18
|
+
let localValue = value !== null && value !== void 0 ? value : defaultValue;
|
|
19
|
+
// One time calculation for input's default value (uncontrolled)
|
|
20
|
+
const localIdValue = idValue !== null && idValue !== void 0 ? idValue : DataTypes.getValue(localValue, idField);
|
|
21
|
+
// Changable states
|
|
22
|
+
const [states, stateUpdate] = React.useReducer((currentState, newState) => {
|
|
23
|
+
return { ...currentState, ...newState };
|
|
24
|
+
}, {
|
|
25
|
+
// Loading unknown
|
|
26
|
+
open: false,
|
|
27
|
+
options: [],
|
|
28
|
+
value: null
|
|
29
|
+
});
|
|
30
|
+
// Input value
|
|
31
|
+
const inputValue = React.useMemo(() => states.value && states.value[idField], [states.value]);
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
if (localValue != null)
|
|
34
|
+
stateUpdate({ value: localValue });
|
|
35
|
+
}, [localValue]);
|
|
36
|
+
// State
|
|
37
|
+
const [state] = React.useState({});
|
|
38
|
+
const isMounted = React.useRef(true);
|
|
39
|
+
// Add readOnly
|
|
40
|
+
const addReadOnly = (params) => {
|
|
41
|
+
if (readOnly != null) {
|
|
42
|
+
Object.assign(params, { readOnly });
|
|
43
|
+
}
|
|
44
|
+
// https://stackoverflow.com/questions/15738259/disabling-chrome-autofill
|
|
45
|
+
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html
|
|
46
|
+
Object.assign(params.inputProps, { autoComplete: inputAutoComplete });
|
|
47
|
+
return params;
|
|
48
|
+
};
|
|
49
|
+
// Change handler
|
|
50
|
+
const changeHandle = (event) => {
|
|
51
|
+
// Stop processing with auto trigger event
|
|
52
|
+
if (event.nativeEvent.cancelable && !event.nativeEvent.composed) {
|
|
53
|
+
stateUpdate({ options: [] });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Stop bubble
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
// Call with delay
|
|
59
|
+
delayed.call(undefined, event.currentTarget.value);
|
|
60
|
+
};
|
|
61
|
+
// Directly load data
|
|
62
|
+
const loadDataDirect = (keyword, id) => {
|
|
63
|
+
// Reset options
|
|
64
|
+
// setOptions([]);
|
|
65
|
+
if (id == null) {
|
|
66
|
+
// Reset real value
|
|
67
|
+
const input = inputRef.current;
|
|
68
|
+
if (input && input.value !== '') {
|
|
69
|
+
// Different value, trigger change event
|
|
70
|
+
ReactUtils.triggerChange(input, '', false);
|
|
71
|
+
}
|
|
72
|
+
if (states.options.length > 0) {
|
|
73
|
+
// Reset options
|
|
74
|
+
stateUpdate({ options: [] });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Loading indicator
|
|
78
|
+
if (!states.loading)
|
|
79
|
+
stateUpdate({ loading: true });
|
|
80
|
+
// Load list
|
|
81
|
+
loadData(keyword, id).then((options) => {
|
|
82
|
+
if (!isMounted.current)
|
|
83
|
+
return;
|
|
84
|
+
// Indicates loading completed
|
|
85
|
+
stateUpdate({
|
|
86
|
+
loading: false,
|
|
87
|
+
...(options != null && { options })
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const delayed = useDelayedExecutor(loadDataDirect, 480);
|
|
92
|
+
const setInputValue = (value) => {
|
|
93
|
+
var _a;
|
|
94
|
+
stateUpdate({ value });
|
|
95
|
+
// Input value
|
|
96
|
+
const input = inputRef.current;
|
|
97
|
+
if (input) {
|
|
98
|
+
// Update value
|
|
99
|
+
const newValue = (_a = DataTypes.getStringValue(value, idField)) !== null && _a !== void 0 ? _a : '';
|
|
100
|
+
if (newValue !== input.value) {
|
|
101
|
+
// Different value, trigger change event
|
|
102
|
+
ReactUtils.triggerChange(input, newValue, false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (localIdValue != null && localIdValue !== '') {
|
|
107
|
+
if (state.idLoaded) {
|
|
108
|
+
// Set default
|
|
109
|
+
if (!state.idSet && states.options.length == 1) {
|
|
110
|
+
stateUpdate({ value: states.options[0] });
|
|
111
|
+
state.idSet = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Load id data
|
|
116
|
+
loadDataDirect(undefined, localIdValue);
|
|
117
|
+
state.idLoaded = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
return () => {
|
|
122
|
+
isMounted.current = false;
|
|
123
|
+
delayed.clear();
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
// Layout
|
|
127
|
+
return (React.createElement("div", null,
|
|
128
|
+
React.createElement("input", { ref: inputRef, "data-reset": "true", type: "text", style: { display: 'none' }, name: name, value: `${inputValue !== null && inputValue !== void 0 ? inputValue : ''}`, readOnly: true, onChange: inputOnChange }),
|
|
129
|
+
React.createElement(Autocomplete, { filterOptions: (options, _state) => options, value: states.value, options: states.options, onChange: (event, value, reason, details) => {
|
|
130
|
+
// Set value
|
|
131
|
+
setInputValue(value);
|
|
132
|
+
// Custom
|
|
133
|
+
if (onChange != null)
|
|
134
|
+
onChange(event, value, reason, details);
|
|
135
|
+
// For clear case
|
|
136
|
+
if (reason === 'clear') {
|
|
137
|
+
stateUpdate({ options: [] });
|
|
138
|
+
loadDataDirect();
|
|
139
|
+
}
|
|
140
|
+
}, open: states.open, openOnFocus: openOnFocus, onOpen: () => {
|
|
141
|
+
// Should load
|
|
142
|
+
const loading = states.loading
|
|
143
|
+
? true
|
|
144
|
+
: states.options.length === 0;
|
|
145
|
+
stateUpdate({ open: true, loading });
|
|
146
|
+
// If not loading
|
|
147
|
+
if (loading)
|
|
148
|
+
loadDataDirect(undefined, states.value == null
|
|
149
|
+
? undefined
|
|
150
|
+
: states.value[idField]);
|
|
151
|
+
}, onClose: () => {
|
|
152
|
+
stateUpdate({
|
|
153
|
+
open: false,
|
|
154
|
+
...(!states.value && { options: [] })
|
|
155
|
+
});
|
|
156
|
+
}, loading: states.loading, sx: sx, renderInput: (params) => search ? (React.createElement(SearchField, { onChange: changeHandle, ...params, readOnly: readOnly, label: label, name: name + 'Input', margin: inputMargin, variant: inputVariant, required: inputRequired, autoComplete: inputAutoComplete, error: inputError, helperText: inputHelperText })) : (React.createElement(InputField, { onChange: changeHandle, ...addReadOnly(params), label: label, name: name + 'Input', margin: inputMargin, variant: inputVariant, required: inputRequired, autoComplete: inputAutoComplete, error: inputError, helperText: inputHelperText })), isOptionEqualToValue: (option, value) => option[idField] === value[idField], ...rest })));
|
|
157
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TooltipProps } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Tooltip with click visibility props
|
|
5
|
+
*/
|
|
6
|
+
export interface TooltipClickProps extends Omit<TooltipProps, 'children' | 'open' | 'disableFocusListener' | 'disableTouchListener'> {
|
|
7
|
+
children: (openTooltip: (newTitle?: string) => void) => React.ReactElement<any, any>;
|
|
8
|
+
disableHoverListener?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Tooltip with click visibility
|
|
12
|
+
* @param props Props
|
|
13
|
+
* @returns Component
|
|
14
|
+
*/
|
|
15
|
+
export declare function TooltipClick(props: TooltipClickProps): JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useDelayedExecutor } from '@etsoo/react';
|
|
2
|
+
import { ClickAwayListener, Tooltip } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Tooltip with click visibility
|
|
6
|
+
* @param props Props
|
|
7
|
+
* @returns Component
|
|
8
|
+
*/
|
|
9
|
+
export function TooltipClick(props) {
|
|
10
|
+
// Destruct
|
|
11
|
+
// leaveDelay set to 5 seconds to hide the tooltip automatically
|
|
12
|
+
const { children, disableHoverListener = true, leaveDelay = 5000, onClose, title, ...rest } = props;
|
|
13
|
+
// State
|
|
14
|
+
const [localTitle, setTitle] = React.useState(title);
|
|
15
|
+
const [open, setOpen] = React.useState(false);
|
|
16
|
+
const delayed = leaveDelay > 0
|
|
17
|
+
? useDelayedExecutor(() => setOpen(false), leaveDelay)
|
|
18
|
+
: undefined;
|
|
19
|
+
// Callback for open the tooltip
|
|
20
|
+
const openTooltip = (newTitle) => {
|
|
21
|
+
setOpen(true);
|
|
22
|
+
if (newTitle)
|
|
23
|
+
setTitle(newTitle);
|
|
24
|
+
delayed === null || delayed === void 0 ? void 0 : delayed.call();
|
|
25
|
+
};
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
delayed === null || delayed === void 0 ? void 0 : delayed.clear();
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
// Layout
|
|
32
|
+
return (React.createElement(ClickAwayListener, { onClickAway: () => setOpen(false) },
|
|
33
|
+
React.createElement(Tooltip, { PopperProps: {
|
|
34
|
+
disablePortal: true
|
|
35
|
+
}, onClose: (event) => {
|
|
36
|
+
setOpen(false);
|
|
37
|
+
if (onClose)
|
|
38
|
+
onClose(event);
|
|
39
|
+
}, title: localTitle, open: open, disableFocusListener: true, disableTouchListener: true, disableHoverListener: disableHoverListener, onMouseOver: disableHoverListener ? undefined : () => setOpen(true), ...rest }, children(openTooltip))));
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
/**
|
|
3
|
+
* User avatar props
|
|
4
|
+
*/
|
|
5
|
+
export interface UserAvatarProps {
|
|
6
|
+
/**
|
|
7
|
+
* Photo src
|
|
8
|
+
*/
|
|
9
|
+
src?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Format title
|
|
12
|
+
*/
|
|
13
|
+
formatTitle?: (title?: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Title of the user
|
|
16
|
+
*/
|
|
17
|
+
title?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* User avatar
|
|
21
|
+
* @param props Props
|
|
22
|
+
* @returns Component
|
|
23
|
+
*/
|
|
24
|
+
export declare function UserAvatar(props: UserAvatarProps): JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Avatar } from '@mui/material';
|
|
3
|
+
import { BusinessUtils } from '@etsoo/appscript';
|
|
4
|
+
import { globalApp } from './app/ReactApp';
|
|
5
|
+
/**
|
|
6
|
+
* User avatar
|
|
7
|
+
* @param props Props
|
|
8
|
+
* @returns Component
|
|
9
|
+
*/
|
|
10
|
+
export function UserAvatar(props) {
|
|
11
|
+
// Destruct
|
|
12
|
+
const { src, title, formatTitle = (title) => {
|
|
13
|
+
return BusinessUtils.formatAvatarTitle(title, 3, typeof globalApp === 'undefined'
|
|
14
|
+
? 'ME'
|
|
15
|
+
: globalApp.get('me'));
|
|
16
|
+
} } = props;
|
|
17
|
+
// Format
|
|
18
|
+
const fTitle = formatTitle(title);
|
|
19
|
+
const count = fTitle.length;
|
|
20
|
+
return (React.createElement(Avatar, { title: title, src: src, sx: {
|
|
21
|
+
width: 48,
|
|
22
|
+
height: 32,
|
|
23
|
+
fontSize: count <= 2 ? '15px' : '12px'
|
|
24
|
+
} }, fTitle));
|
|
25
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
/**
|
|
3
|
+
* User avatar editor to Blob helper
|
|
4
|
+
*/
|
|
5
|
+
export interface UserAvatarEditorToBlob {
|
|
6
|
+
(canvas: HTMLCanvasElement, mimeType?: string, quality?: number): Promise<Blob>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* User avatar editor on done handler
|
|
10
|
+
*/
|
|
11
|
+
export interface UserAvatarEditorOnDoneHandler {
|
|
12
|
+
(canvas: HTMLCanvasElement, toBlob: UserAvatarEditorToBlob): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* User avatar editor props
|
|
16
|
+
*/
|
|
17
|
+
export interface UserAvatarEditorProps {
|
|
18
|
+
/**
|
|
19
|
+
* Cropping border size
|
|
20
|
+
*/
|
|
21
|
+
border?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Image source
|
|
24
|
+
*/
|
|
25
|
+
image?: string | File;
|
|
26
|
+
/**
|
|
27
|
+
* Max width to save
|
|
28
|
+
*/
|
|
29
|
+
maxWidth?: number;
|
|
30
|
+
/**
|
|
31
|
+
* On done handler
|
|
32
|
+
*/
|
|
33
|
+
onDone: UserAvatarEditorOnDoneHandler;
|
|
34
|
+
/**
|
|
35
|
+
* Return scaled result?
|
|
36
|
+
*/
|
|
37
|
+
scaledResult?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Width of the editor
|
|
40
|
+
*/
|
|
41
|
+
width?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Height of the editor
|
|
44
|
+
*/
|
|
45
|
+
height?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* User avatar editor
|
|
49
|
+
* https://github.com/mosch/react-avatar-editor
|
|
50
|
+
* @param props Props
|
|
51
|
+
* @returns Component
|
|
52
|
+
*/
|
|
53
|
+
export declare function UserAvatarEditor(props: UserAvatarEditorProps): JSX.Element;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Button, ButtonGroup, Slider, Stack } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import AvatarEditor from 'react-avatar-editor';
|
|
4
|
+
import RotateLeftIcon from '@mui/icons-material/RotateLeft';
|
|
5
|
+
import RotateRightIcon from '@mui/icons-material/RotateRight';
|
|
6
|
+
import ClearAllIcon from '@mui/icons-material/ClearAll';
|
|
7
|
+
import ComputerIcon from '@mui/icons-material/Computer';
|
|
8
|
+
import DoneIcon from '@mui/icons-material/Done';
|
|
9
|
+
import pica from 'pica';
|
|
10
|
+
import { Labels } from './app/Labels';
|
|
11
|
+
const defaultState = {
|
|
12
|
+
scale: 1,
|
|
13
|
+
rotate: 0
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* User avatar editor
|
|
17
|
+
* https://github.com/mosch/react-avatar-editor
|
|
18
|
+
* @param props Props
|
|
19
|
+
* @returns Component
|
|
20
|
+
*/
|
|
21
|
+
export function UserAvatarEditor(props) {
|
|
22
|
+
// Destruct
|
|
23
|
+
const { border = 30, image, maxWidth, onDone, scaledResult = false, width = 200, height = 200 } = props;
|
|
24
|
+
// Container width
|
|
25
|
+
const containerWidth = width + 2 * border + 44 + 4;
|
|
26
|
+
// Calculated max width
|
|
27
|
+
const maxWidthCalculated = maxWidth == null || maxWidth < 200 ? 3 * width : maxWidth;
|
|
28
|
+
// Labels
|
|
29
|
+
const labels = Labels.UserAvatarEditor;
|
|
30
|
+
// Ref
|
|
31
|
+
const ref = React.createRef();
|
|
32
|
+
// Button ref
|
|
33
|
+
const buttonRef = React.createRef();
|
|
34
|
+
// Preview image state
|
|
35
|
+
const [previewImage, setPreviewImage] = React.useState(image);
|
|
36
|
+
// Is ready state
|
|
37
|
+
const [ready, setReady] = React.useState(false);
|
|
38
|
+
// Editor states
|
|
39
|
+
const [editorState, setEditorState] = React.useState(defaultState);
|
|
40
|
+
// Handle zoom
|
|
41
|
+
const handleZoom = (_event, value, _activeThumb) => {
|
|
42
|
+
const scale = typeof value === 'number' ? value : value[0];
|
|
43
|
+
const newState = { ...editorState, scale };
|
|
44
|
+
setEditorState(newState);
|
|
45
|
+
};
|
|
46
|
+
// Handle image load
|
|
47
|
+
const handleLoad = () => {
|
|
48
|
+
setReady(true);
|
|
49
|
+
};
|
|
50
|
+
// Handle file change
|
|
51
|
+
const handleFileChange = (event) => {
|
|
52
|
+
var _a;
|
|
53
|
+
const files = event.target.files;
|
|
54
|
+
if (files == null || files.length == 0)
|
|
55
|
+
return;
|
|
56
|
+
// Reset all settings
|
|
57
|
+
handleReset();
|
|
58
|
+
// Set new preview image
|
|
59
|
+
setPreviewImage(files[0]);
|
|
60
|
+
// Set ready state
|
|
61
|
+
setReady(false);
|
|
62
|
+
// Make the submit button visible
|
|
63
|
+
(_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView(false);
|
|
64
|
+
};
|
|
65
|
+
// Handle reset
|
|
66
|
+
const handleReset = () => {
|
|
67
|
+
setEditorState({ ...defaultState });
|
|
68
|
+
};
|
|
69
|
+
// Handle rotate
|
|
70
|
+
const handleRotate = (r) => {
|
|
71
|
+
let rotate = editorState.rotate + r;
|
|
72
|
+
if (rotate >= 360 || rotate <= -360)
|
|
73
|
+
rotate = 0;
|
|
74
|
+
const newState = { ...editorState, rotate };
|
|
75
|
+
setEditorState(newState);
|
|
76
|
+
};
|
|
77
|
+
// Handle done
|
|
78
|
+
const handleDone = () => {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
// Data
|
|
81
|
+
var data = scaledResult
|
|
82
|
+
? (_a = ref.current) === null || _a === void 0 ? void 0 : _a.getImageScaledToCanvas()
|
|
83
|
+
: (_b = ref.current) === null || _b === void 0 ? void 0 : _b.getImage();
|
|
84
|
+
if (data == null)
|
|
85
|
+
return;
|
|
86
|
+
// pica
|
|
87
|
+
const picaInstance = pica();
|
|
88
|
+
// toBlob helper
|
|
89
|
+
// Convenience method, similar to canvas.toBlob(), but with promise interface & polyfill for old browsers.
|
|
90
|
+
const toBlob = (canvas, mimeType = 'image/jpeg', quality = 1) => {
|
|
91
|
+
return picaInstance.toBlob(canvas, mimeType, quality);
|
|
92
|
+
};
|
|
93
|
+
if (data.width > maxWidthCalculated) {
|
|
94
|
+
// Target height
|
|
95
|
+
const heightCalculated = (height * maxWidthCalculated) / width;
|
|
96
|
+
// Target
|
|
97
|
+
const to = document.createElement('canvas');
|
|
98
|
+
to.width = maxWidthCalculated;
|
|
99
|
+
to.height = heightCalculated;
|
|
100
|
+
// Large photo, resize it
|
|
101
|
+
// https://github.com/nodeca/pica
|
|
102
|
+
picaInstance
|
|
103
|
+
.resize(data, to, {
|
|
104
|
+
unsharpAmount: 160,
|
|
105
|
+
unsharpRadius: 0.6,
|
|
106
|
+
unsharpThreshold: 1
|
|
107
|
+
})
|
|
108
|
+
.then((result) => onDone(result, toBlob));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
onDone(data, toBlob);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return (React.createElement(Stack, { direction: "column", spacing: 0.5, width: containerWidth },
|
|
115
|
+
React.createElement(Button, { variant: "outlined", size: "medium", component: "label", startIcon: React.createElement(ComputerIcon, null), fullWidth: true },
|
|
116
|
+
labels.upload,
|
|
117
|
+
React.createElement("input", { id: "fileInput", type: "file", accept: "image/png, image/jpeg", multiple: false, hidden: true, onChange: handleFileChange })),
|
|
118
|
+
React.createElement(Stack, { direction: "row", spacing: 0.5 },
|
|
119
|
+
React.createElement(AvatarEditor, { ref: ref, border: border, width: width, height: height, onLoadSuccess: handleLoad, image: previewImage !== null && previewImage !== void 0 ? previewImage : '', scale: editorState.scale, rotate: editorState.rotate }),
|
|
120
|
+
React.createElement(ButtonGroup, { size: "small", orientation: "vertical", disabled: !ready },
|
|
121
|
+
React.createElement(Button, { onClick: () => handleRotate(90), title: labels.rotateRight },
|
|
122
|
+
React.createElement(RotateRightIcon, null)),
|
|
123
|
+
React.createElement(Button, { onClick: () => handleRotate(-90), title: labels.rotateLeft },
|
|
124
|
+
React.createElement(RotateLeftIcon, null)),
|
|
125
|
+
React.createElement(Button, { onClick: handleReset, title: labels.reset },
|
|
126
|
+
React.createElement(ClearAllIcon, null)))),
|
|
127
|
+
React.createElement(Slider, { title: labels.zoom, disabled: !ready, min: 1, max: 5, step: 0.01, value: editorState.scale, onChange: handleZoom }),
|
|
128
|
+
React.createElement(Button, { ref: buttonRef, variant: "contained", startIcon: React.createElement(DoneIcon, null), disabled: !ready, onClick: handleDone }, labels.done)));
|
|
129
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IAppSettings, IUser, RefreshTokenProps } from '@etsoo/appscript';
|
|
2
|
+
import { IPageData, RefreshTokenRQ } from '@etsoo/react';
|
|
3
|
+
import { ReactApp } from './ReactApp';
|
|
4
|
+
/**
|
|
5
|
+
* Common independent application
|
|
6
|
+
* 通用独立程序
|
|
7
|
+
*/
|
|
8
|
+
export declare abstract class CommonApp<U extends IUser = IUser, P extends IPageData = IPageData, S extends IAppSettings = IAppSettings> extends ReactApp<S, U, P> {
|
|
9
|
+
/**
|
|
10
|
+
* Override persistedFields
|
|
11
|
+
*/
|
|
12
|
+
protected get persistedFields(): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Init call update fields in local storage
|
|
15
|
+
* @returns Fields
|
|
16
|
+
*/
|
|
17
|
+
protected initCallEncryptedUpdateFields(): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Do user login
|
|
20
|
+
* @param data User data
|
|
21
|
+
* @param refreshToken Refresh token
|
|
22
|
+
* @param keep Keep login
|
|
23
|
+
* @returns Success data
|
|
24
|
+
*/
|
|
25
|
+
protected doUserLogin(data: U, refreshToken: string, keep: boolean): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Refresh token
|
|
28
|
+
* @param props Props
|
|
29
|
+
*/
|
|
30
|
+
refreshToken<D extends object = RefreshTokenRQ>(props?: RefreshTokenProps<D>): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Try login
|
|
33
|
+
* @param data Additional data
|
|
34
|
+
* @param showLoading Show loading bar or not
|
|
35
|
+
* @returns Result
|
|
36
|
+
*/
|
|
37
|
+
tryLogin<D extends object = RefreshTokenRQ>(data?: D, showLoading?: boolean): Promise<boolean>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { CoreConstants } from '@etsoo/react';
|
|
2
|
+
import { ReactApp } from './ReactApp';
|
|
3
|
+
/**
|
|
4
|
+
* Common independent application
|
|
5
|
+
* 通用独立程序
|
|
6
|
+
*/
|
|
7
|
+
export class CommonApp extends ReactApp {
|
|
8
|
+
/**
|
|
9
|
+
* Override persistedFields
|
|
10
|
+
*/
|
|
11
|
+
get persistedFields() {
|
|
12
|
+
return [...super.persistedFields, CoreConstants.FieldUserIdSaved];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Init call update fields in local storage
|
|
16
|
+
* @returns Fields
|
|
17
|
+
*/
|
|
18
|
+
initCallEncryptedUpdateFields() {
|
|
19
|
+
const fields = super.initCallEncryptedUpdateFields();
|
|
20
|
+
fields.push(CoreConstants.FieldUserIdSaved);
|
|
21
|
+
return fields;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Do user login
|
|
25
|
+
* @param data User data
|
|
26
|
+
* @param refreshToken Refresh token
|
|
27
|
+
* @param keep Keep login
|
|
28
|
+
* @returns Success data
|
|
29
|
+
*/
|
|
30
|
+
doUserLogin(data, refreshToken, keep) {
|
|
31
|
+
this.userLogin(data, refreshToken, keep);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Refresh token
|
|
36
|
+
* @param props Props
|
|
37
|
+
*/
|
|
38
|
+
async refreshToken(props) {
|
|
39
|
+
// Destruct
|
|
40
|
+
const { callback, data, relogin = false, showLoading = false } = props !== null && props !== void 0 ? props : {};
|
|
41
|
+
// Token
|
|
42
|
+
const token = this.getCacheToken();
|
|
43
|
+
if (token == null || token === '') {
|
|
44
|
+
if (callback)
|
|
45
|
+
callback(false);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Reqest data
|
|
49
|
+
const rq = {
|
|
50
|
+
deviceId: this.deviceId,
|
|
51
|
+
timezone: this.getTimeZone(),
|
|
52
|
+
...data
|
|
53
|
+
};
|
|
54
|
+
// Payload
|
|
55
|
+
const payload = {
|
|
56
|
+
// No loading bar needed to avoid screen flicks
|
|
57
|
+
showLoading,
|
|
58
|
+
config: { headers: { [CoreConstants.TokenHeaderRefresh]: token } },
|
|
59
|
+
onError: (error) => {
|
|
60
|
+
if (callback)
|
|
61
|
+
callback(error);
|
|
62
|
+
// Prevent further processing
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Success callback
|
|
67
|
+
const success = (result, failCallback) => {
|
|
68
|
+
// Token
|
|
69
|
+
const refreshToken = this.getResponseToken(payload.response);
|
|
70
|
+
if (refreshToken == null || result.data == null) {
|
|
71
|
+
if (failCallback)
|
|
72
|
+
failCallback(this.get('noData'));
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// Keep
|
|
76
|
+
const keep = this.storage.getData(CoreConstants.FieldLoginKeep, false);
|
|
77
|
+
// User login
|
|
78
|
+
var successData = this.doUserLogin(result.data, refreshToken, keep);
|
|
79
|
+
// Callback
|
|
80
|
+
if (failCallback)
|
|
81
|
+
failCallback(true, successData);
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
// Call API
|
|
85
|
+
const result = await this.api.put('Auth/RefreshToken', rq, payload);
|
|
86
|
+
if (result == null)
|
|
87
|
+
return false;
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
if (result.type === 'TokenExpired' && relogin) {
|
|
90
|
+
// Try login
|
|
91
|
+
// Dialog to receive password
|
|
92
|
+
var labels = this.getLabels('reloginTip', 'login');
|
|
93
|
+
this.notifier.prompt(labels.reloginTip, async (pwd) => {
|
|
94
|
+
if (pwd == null) {
|
|
95
|
+
this.toLoginPage();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Set password for the action
|
|
99
|
+
rq.pwd = this.encrypt(this.hash(pwd));
|
|
100
|
+
// Submit again
|
|
101
|
+
const result = await this.api.put('Auth/RefreshToken', rq, payload);
|
|
102
|
+
if (result == null)
|
|
103
|
+
return;
|
|
104
|
+
if (result.ok) {
|
|
105
|
+
success(result, (loginResult) => {
|
|
106
|
+
if (loginResult === true) {
|
|
107
|
+
if (callback)
|
|
108
|
+
callback(true);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const message = this.formatRefreshTokenResult(loginResult);
|
|
112
|
+
if (message)
|
|
113
|
+
this.notifier.alert(message);
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Popup message
|
|
118
|
+
this.alertResult(result);
|
|
119
|
+
return false;
|
|
120
|
+
}, labels.login, { type: 'password' });
|
|
121
|
+
// Fake truth to avoid reloading
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (callback)
|
|
125
|
+
callback(result);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return success(result, callback);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Try login
|
|
132
|
+
* @param data Additional data
|
|
133
|
+
* @param showLoading Show loading bar or not
|
|
134
|
+
* @returns Result
|
|
135
|
+
*/
|
|
136
|
+
async tryLogin(data, showLoading) {
|
|
137
|
+
// Reset user state
|
|
138
|
+
const result = await super.tryLogin(data);
|
|
139
|
+
if (!result)
|
|
140
|
+
return false;
|
|
141
|
+
// Refresh token
|
|
142
|
+
return await this.refreshToken({
|
|
143
|
+
callback: (result) => this.doRefreshTokenResult(result),
|
|
144
|
+
data,
|
|
145
|
+
showLoading,
|
|
146
|
+
relogin: true
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IAppSettings } from '@etsoo/appscript';
|
|
2
|
+
import { DataTypes } from '@etsoo/shared';
|
|
3
|
+
/**
|
|
4
|
+
* Service app settings interface
|
|
5
|
+
*/
|
|
6
|
+
export interface IServiceAppSettings<S extends DataTypes.IdType = number> extends IAppSettings {
|
|
7
|
+
/**
|
|
8
|
+
* Service id
|
|
9
|
+
*/
|
|
10
|
+
readonly serviceId: S;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IActionResult, IUser } from '@etsoo/appscript';
|
|
2
|
+
/**
|
|
3
|
+
* Service user interface
|
|
4
|
+
*/
|
|
5
|
+
export interface IServiceUser extends IUser {
|
|
6
|
+
/**
|
|
7
|
+
* Service device id
|
|
8
|
+
*/
|
|
9
|
+
serviceDeviceId: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Service user login result
|
|
13
|
+
*/
|
|
14
|
+
export declare type ServiceLoginResult<U extends IServiceUser = IServiceUser> = IActionResult<U>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|