@onehat/ui 0.4.58 → 0.4.59

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.58",
3
+ "version": "0.4.59",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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 = { msg, data };
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
- if (!Repository || !action) {
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.children.form.formGetValues(),
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
- alert(result.message);
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="Initiate"
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={() => reset()}
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
- [currentTabIx, setCurrentTab] = useState(0),
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
- reset = () => {
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: 'Initiate',
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: <Box className="p-2">{results}</Box>,
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';