@onehat/ui 0.4.58 → 0.4.60
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/package.json
CHANGED
|
@@ -44,6 +44,12 @@ import AngleLeft from '../Icons/AngleLeft.js';
|
|
|
44
44
|
import Eye from '../Icons/Eye.js';
|
|
45
45
|
import Rotate from '../Icons/Rotate.js';
|
|
46
46
|
import Pencil from '../Icons/Pencil.js';
|
|
47
|
+
import Plus from '../Icons/Plus.js';
|
|
48
|
+
import FloppyDiskRegular from '../Icons/FloppyDiskRegular.js';
|
|
49
|
+
import Trash from '../Icons/Trash.js';
|
|
50
|
+
import Xmark from '../Icons/Xmark.js';
|
|
51
|
+
import Check from '../Icons/Check.js';
|
|
52
|
+
|
|
47
53
|
import Footer from '../Layout/Footer.js';
|
|
48
54
|
import Label from '../Form/Label.js';
|
|
49
55
|
import _ from 'lodash';
|
|
@@ -1216,6 +1222,7 @@ function Form(props) {
|
|
|
1216
1222
|
{...testProps('deleteBtn')}
|
|
1217
1223
|
key="deleteBtn"
|
|
1218
1224
|
onPress={onDelete}
|
|
1225
|
+
icon={Trash}
|
|
1219
1226
|
className={`
|
|
1220
1227
|
bg-warning-500
|
|
1221
1228
|
hover:bg-warning-700
|
|
@@ -1239,6 +1246,7 @@ function Form(props) {
|
|
|
1239
1246
|
{...testProps('cancelBtn')}
|
|
1240
1247
|
key="cancelBtn"
|
|
1241
1248
|
variant={editorType === EDITOR_TYPE__INLINE ? 'solid' : 'outline'}
|
|
1249
|
+
icon={Xmark}
|
|
1242
1250
|
onPress={onCancel}
|
|
1243
1251
|
className="text-white"
|
|
1244
1252
|
text="Cancel"
|
|
@@ -1249,6 +1257,7 @@ function Form(props) {
|
|
|
1249
1257
|
{...testProps('closeBtn')}
|
|
1250
1258
|
key="closeBtn"
|
|
1251
1259
|
variant={editorType === EDITOR_TYPE__INLINE ? 'solid' : 'outline'}
|
|
1260
|
+
icon={Xmark}
|
|
1252
1261
|
onPress={onClose}
|
|
1253
1262
|
className="text-white"
|
|
1254
1263
|
text="Close"
|
|
@@ -1259,6 +1268,7 @@ function Form(props) {
|
|
|
1259
1268
|
{...testProps('saveBtn')}
|
|
1260
1269
|
key="saveBtn"
|
|
1261
1270
|
onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
|
|
1271
|
+
icon={getEditorMode() === EDITOR_MODE__ADD ? Plus : FloppyDiskRegular}
|
|
1262
1272
|
isDisabled={isSaveDisabled}
|
|
1263
1273
|
className="text-white"
|
|
1264
1274
|
text={getEditorMode() === EDITOR_MODE__ADD ? 'Add' : 'Save'}
|
|
@@ -1268,6 +1278,7 @@ function Form(props) {
|
|
|
1268
1278
|
<Button
|
|
1269
1279
|
{...testProps('submitBtn')}
|
|
1270
1280
|
key="submitBtn"
|
|
1281
|
+
icon={Check}
|
|
1271
1282
|
onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
|
|
1272
1283
|
isDisabled={isSubmitDisabled}
|
|
1273
1284
|
className="text-white"
|
|
@@ -1283,6 +1294,7 @@ function Form(props) {
|
|
|
1283
1294
|
{...testProps('additionalFooterBtn-' + props.key)}
|
|
1284
1295
|
{...props}
|
|
1285
1296
|
onPress={(e) => handleSubmit(props.onPress, onSubmitError)(e)}
|
|
1297
|
+
icon={props.icon || null}
|
|
1286
1298
|
text={props.text}
|
|
1287
1299
|
isDisabled={isDisabled}
|
|
1288
1300
|
/>;
|
|
@@ -424,9 +424,9 @@ function GridComponent(props) {
|
|
|
424
424
|
} else {
|
|
425
425
|
let canDoEdit = false,
|
|
426
426
|
canDoView = false;
|
|
427
|
-
if (onEdit && canUser && canUser(EDIT) && canRecordBeEdited
|
|
427
|
+
if (onEdit && canUser && canUser(EDIT) && (!canRecordBeEdited || canRecordBeEdited(selection))) {
|
|
428
428
|
canDoEdit = true;
|
|
429
|
-
}
|
|
429
|
+
} else
|
|
430
430
|
if (onView && canUser && canUser(VIEW)) {
|
|
431
431
|
canDoView = true;
|
|
432
432
|
}
|
|
@@ -502,7 +502,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
502
502
|
setIsSaving(true);
|
|
503
503
|
let success = true;
|
|
504
504
|
const tempListener = (msg, data) => {
|
|
505
|
-
success =
|
|
505
|
+
success = false;
|
|
506
506
|
};
|
|
507
507
|
|
|
508
508
|
Repository.on('error', tempListener); // add a temporary listener for the error event
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { useState, } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
|
+
ScrollView,
|
|
4
5
|
Text,
|
|
5
6
|
VStack,
|
|
6
7
|
} from '@project-components/Gluestack';
|
|
8
|
+
import { useSelector, useDispatch, } from 'react-redux';
|
|
9
|
+
import { PROGRESS_COMPLETED } from '../../Constants/Progress.js';
|
|
10
|
+
import useForceUpdate from '../../Hooks/useForceUpdate.js';
|
|
7
11
|
import isJson from '../../Functions/isJson.js';
|
|
8
12
|
import Form from '../Form/Form.js';
|
|
9
13
|
import Button from '../Buttons/Button.js';
|
|
@@ -12,19 +16,31 @@ import withAlert from '../Hoc/withAlert.js';
|
|
|
12
16
|
import ChevronLeft from '../Icons/ChevronLeft.js';
|
|
13
17
|
import ChevronRight from '../Icons/ChevronRight.js';
|
|
14
18
|
import Play from '../Icons/Play.js';
|
|
19
|
+
import EllipsisHorizontal from '../Icons/EllipsisHorizontal.js';
|
|
15
20
|
import Stop from '../Icons/Stop.js';
|
|
16
21
|
import TabBar from '../Tab/TabBar.js';
|
|
17
22
|
import Panel from '../Panel/Panel.js';
|
|
18
23
|
import Toolbar from '../Toolbar/Toolbar.js';
|
|
19
24
|
import _ from 'lodash';
|
|
20
25
|
|
|
26
|
+
// NOTE: This component assumes you have an AppSlice, that has
|
|
27
|
+
// an 'operationsInProgress' state var and a 'setOperationsInProgress' action.
|
|
21
28
|
|
|
22
29
|
function AsyncOperation(props) {
|
|
30
|
+
|
|
31
|
+
if (!props.Repository || !props.action) {
|
|
32
|
+
throw Error('AsyncOperation: Repository and action are required!');
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
const {
|
|
24
36
|
action,
|
|
25
37
|
Repository,
|
|
26
38
|
formItems = [],
|
|
27
39
|
formStartingValues = {},
|
|
40
|
+
getProgressUpdates = false,
|
|
41
|
+
parseProgress, // optional fn, accepts 'response' as arg and returns progress string
|
|
42
|
+
progressStuckThreshold = null, // e.g. 3, if left blank, doesn't check for stuck state
|
|
43
|
+
updateInterval = 10000, // ms
|
|
28
44
|
|
|
29
45
|
// withComponent
|
|
30
46
|
self,
|
|
@@ -32,25 +48,24 @@ function AsyncOperation(props) {
|
|
|
32
48
|
// withAlert
|
|
33
49
|
alert,
|
|
34
50
|
} = props,
|
|
51
|
+
dispatch = useDispatch(),
|
|
35
52
|
initiate = async () => {
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
alert('AsyncOperation: Repository and action are required!');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
54
|
+
clearProgress();
|
|
42
55
|
setFooter(getFooter('processing'));
|
|
56
|
+
setIsInProgress(true);
|
|
43
57
|
|
|
44
58
|
const
|
|
45
59
|
method = Repository.methods.edit,
|
|
46
60
|
uri = Repository.getModel() + '/' + action,
|
|
47
|
-
formValues = self
|
|
61
|
+
formValues = self?.children?.form?.formGetValues() || {},
|
|
48
62
|
result = await Repository._send(method, uri, formValues);
|
|
49
|
-
|
|
63
|
+
|
|
64
|
+
setFormValues(formValues);
|
|
65
|
+
|
|
50
66
|
const response = Repository._processServerResponse(result);
|
|
51
67
|
if (!response.success) {
|
|
52
|
-
|
|
53
|
-
reset();
|
|
68
|
+
resetToInitialState();
|
|
54
69
|
return;
|
|
55
70
|
}
|
|
56
71
|
|
|
@@ -75,7 +90,7 @@ function AsyncOperation(props) {
|
|
|
75
90
|
case 'initiate':
|
|
76
91
|
return <Toolbar>
|
|
77
92
|
<Button
|
|
78
|
-
text="
|
|
93
|
+
text="Start"
|
|
79
94
|
rightIcon={ChevronRight}
|
|
80
95
|
onPress={() => initiate()}
|
|
81
96
|
/>
|
|
@@ -93,29 +108,144 @@ function AsyncOperation(props) {
|
|
|
93
108
|
<Button
|
|
94
109
|
text="Reset"
|
|
95
110
|
icon={ChevronLeft}
|
|
96
|
-
onPress={() =>
|
|
111
|
+
onPress={() => resetToInitialState()}
|
|
97
112
|
/>
|
|
98
113
|
</Toolbar>;
|
|
99
114
|
}
|
|
100
115
|
},
|
|
116
|
+
operationsInProgress = useSelector((state) => state.app.operationsInProgress),
|
|
117
|
+
isInProgress = operationsInProgress.includes(action),
|
|
118
|
+
forceUpdate = useForceUpdate(),
|
|
101
119
|
[footer, setFooter] = useState(getFooter()),
|
|
102
|
-
[results, setResults] = useState(''),
|
|
103
|
-
[
|
|
120
|
+
[results, setResults] = useState(isInProgress ? 'Checking progress...' : null),
|
|
121
|
+
[progress, setProgress] = useState(null),
|
|
122
|
+
[isStuck, setIsStuck] = useState(false),
|
|
123
|
+
[currentTabIx, setCurrentTab] = useState(isInProgress ? 1 : 0),
|
|
124
|
+
previousProgressRef = useRef(null),
|
|
125
|
+
unchangedProgressCountRef = useRef(0),
|
|
126
|
+
intervalRef = useRef(null),
|
|
127
|
+
formValuesRef = useRef(null),
|
|
128
|
+
getPreviousProgress = () => {
|
|
129
|
+
return previousProgressRef.current;
|
|
130
|
+
},
|
|
131
|
+
setPreviousProgress = (progress) => {
|
|
132
|
+
previousProgressRef.current = progress;
|
|
133
|
+
},
|
|
134
|
+
getUnchangedProgressCount = () => {
|
|
135
|
+
return unchangedProgressCountRef.current;
|
|
136
|
+
},
|
|
137
|
+
setUnchangedProgressCount = (count) => {
|
|
138
|
+
unchangedProgressCountRef.current = count;
|
|
139
|
+
forceUpdate();
|
|
140
|
+
},
|
|
141
|
+
getInterval = () => {
|
|
142
|
+
return intervalRef.current;
|
|
143
|
+
},
|
|
144
|
+
setIntervalRef = (interval) => { // 'setInterval' is a reserved name
|
|
145
|
+
intervalRef.current = interval;
|
|
146
|
+
},
|
|
147
|
+
getFormValues = () => {
|
|
148
|
+
return formValuesRef.current;
|
|
149
|
+
},
|
|
150
|
+
setFormValues = (values) => {
|
|
151
|
+
formValuesRef.current = values;
|
|
152
|
+
},
|
|
104
153
|
showResults = (results) => {
|
|
105
154
|
setCurrentTab(1);
|
|
106
155
|
setFooter(getFooter('results'));
|
|
107
156
|
setResults(results);
|
|
157
|
+
getProgress();
|
|
108
158
|
},
|
|
109
|
-
|
|
159
|
+
getProgress = (immediately = false) => {
|
|
160
|
+
if (getProgressUpdates) {
|
|
161
|
+
|
|
162
|
+
async function fetchProgress() {
|
|
163
|
+
const
|
|
164
|
+
method = Repository.methods.edit,
|
|
165
|
+
progressAction = 'get' + action.charAt(0).toUpperCase() + action.slice(1) + 'Progress',
|
|
166
|
+
uri = Repository.getModel() + '/' + progressAction,
|
|
167
|
+
result = await Repository._send(method, uri, getFormValues());
|
|
168
|
+
|
|
169
|
+
const response = Repository._processServerResponse(result);
|
|
170
|
+
if (!response.success) {
|
|
171
|
+
alert(result.message);
|
|
172
|
+
clearProgress();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const progress = parseProgress ? parseProgress(response) : response.message
|
|
177
|
+
if (progress === PROGRESS_COMPLETED) {
|
|
178
|
+
clearProgress();
|
|
179
|
+
setProgress(progress);
|
|
180
|
+
} else {
|
|
181
|
+
// in process
|
|
182
|
+
let newUnchangedProgressCount = getUnchangedProgressCount();
|
|
183
|
+
if (progress === getPreviousProgress()) {
|
|
184
|
+
newUnchangedProgressCount++;
|
|
185
|
+
setUnchangedProgressCount(newUnchangedProgressCount);
|
|
186
|
+
if (progressStuckThreshold !== null && newUnchangedProgressCount >= progressStuckThreshold) {
|
|
187
|
+
clearProgress();
|
|
188
|
+
setProgress('The operation appears to be stuck.');
|
|
189
|
+
setIsStuck(true);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
setPreviousProgress(progress);
|
|
193
|
+
setProgress(progress);
|
|
194
|
+
setUnchangedProgressCount(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (immediately) {
|
|
200
|
+
fetchProgress();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const interval = setInterval(fetchProgress, updateInterval);
|
|
204
|
+
setIntervalRef(interval);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
resetToInitialState = () => {
|
|
110
208
|
setCurrentTab(0);
|
|
111
209
|
setFooter(getFooter());
|
|
210
|
+
clearProgress();
|
|
211
|
+
},
|
|
212
|
+
clearProgress = () => {
|
|
213
|
+
setIsInProgress(false);
|
|
214
|
+
setIsStuck(false);
|
|
215
|
+
setProgress(null);
|
|
216
|
+
setPreviousProgress(null);
|
|
217
|
+
setUnchangedProgressCount(0);
|
|
218
|
+
clearInterval(getInterval());
|
|
219
|
+
setIntervalRef(null);
|
|
220
|
+
},
|
|
221
|
+
setIsInProgress = (isInProgress) => {
|
|
222
|
+
dispatch({
|
|
223
|
+
type: 'app/setOperationsInProgress',
|
|
224
|
+
payload: {
|
|
225
|
+
operation: action,
|
|
226
|
+
isInProgress,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
unchangedProgressCount = getUnchangedProgressCount();
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
|
|
234
|
+
if (isInProgress) {
|
|
235
|
+
getProgress(true); // true to fetch immediately
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
// clear the interval when the component unmounts
|
|
240
|
+
clearInterval(getInterval());
|
|
112
241
|
};
|
|
242
|
+
}, []);
|
|
113
243
|
|
|
114
244
|
return <Panel {...props} footer={footer}>
|
|
115
245
|
<TabBar
|
|
116
246
|
tabs={[
|
|
117
247
|
{
|
|
118
|
-
title: '
|
|
248
|
+
title: 'Start',
|
|
119
249
|
icon: Play,
|
|
120
250
|
isDisabled: currentTabIx !== 0,
|
|
121
251
|
content: <Form
|
|
@@ -129,9 +259,15 @@ function AsyncOperation(props) {
|
|
|
129
259
|
},
|
|
130
260
|
{
|
|
131
261
|
title: 'Results',
|
|
132
|
-
icon: Stop,
|
|
262
|
+
icon: isInProgress ? EllipsisHorizontal : Stop,
|
|
133
263
|
isDisabled: currentTabIx !== 1,
|
|
134
|
-
content: <
|
|
264
|
+
content: <ScrollView className="ScrollView h-full w-full">
|
|
265
|
+
<Box className={`p-4 ${isStuck ? 'text-red-400 font-bold' : ''}`}>
|
|
266
|
+
{progress ?
|
|
267
|
+
progress + (unchangedProgressCount > 0 ? ' (unchanged x' + unchangedProgressCount + ')' : '') :
|
|
268
|
+
results}
|
|
269
|
+
</Box>
|
|
270
|
+
</ScrollView>,
|
|
135
271
|
},
|
|
136
272
|
]}
|
|
137
273
|
currentTabIx={currentTabIx}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PROGRESS_COMPLETED = 'Completed';
|