@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.58",
3
+ "version": "0.4.60",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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 && canRecordBeEdited(selection)) {
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 = { 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';