@onehat/ui 0.4.113 → 0.4.114
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 +1 -1
- package/src/Components/Buttons/Button.js +2 -0
- package/src/Components/Editor/ReportPresetsEditor.js +39 -0
- package/src/Components/Form/Field/Combo/ReportPresetsComboEditor.js +15 -0
- package/src/Components/Form/Form.js +29 -6
- package/src/Components/Grid/Grid.js +67 -0
- package/src/Components/Hoc/withEditor.js +1 -1
- package/src/Components/Report/Report.js +90 -10
- package/src/Components/Window/ReportPresetsEditorWindow.js +34 -0
- package/src/Components/index.js +2 -0
- package/src/Functions/downloadInBackground.js +3 -1
- package/src/Functions/downloadWithFetch.js +48 -44
- package/src/Functions/getErrorMessageFromResponse.js +21 -0
- package/src/Functions/getReport.js +70 -3
- package/src/Functions/hasSameId.js +3 -0
- package/src/Functions/testProps.js +7 -0
- package/src/Models/Slices/AuthSlice.js +2 -2
- package/src/PlatformImports/Web/Attachments.js +4 -0
package/package.json
CHANGED
|
@@ -64,6 +64,8 @@ const ButtonComponent = forwardRef((props, ref) => {
|
|
|
64
64
|
'flex',
|
|
65
65
|
'flex-row',
|
|
66
66
|
'items-center',
|
|
67
|
+
'disabled:opacity-40',
|
|
68
|
+
'disabled:cursor-not-allowed',
|
|
67
69
|
);
|
|
68
70
|
if (isExpandToFillVertical) {
|
|
69
71
|
// IMPORTANT! Otherwise the button will cut off the vertical content due to size classes automatically added by Gluestack (e.g. h-10)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Editor from './Editor.js';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
|
|
4
|
+
export default function ReportPresetsEditor(props) {
|
|
5
|
+
|
|
6
|
+
const
|
|
7
|
+
items = [
|
|
8
|
+
{
|
|
9
|
+
"type": "Column",
|
|
10
|
+
"flex": 1,
|
|
11
|
+
"defaults": {},
|
|
12
|
+
"items": [
|
|
13
|
+
{
|
|
14
|
+
"type": "FieldSet",
|
|
15
|
+
"title": "General",
|
|
16
|
+
"reference": "general",
|
|
17
|
+
"defaults": {},
|
|
18
|
+
"items": [
|
|
19
|
+
{
|
|
20
|
+
"name": "report_presets__name"
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
ancillaryItems = [],
|
|
28
|
+
columnDefaults = { // defaults for each column defined in 'items', for use in Form amd Viewer
|
|
29
|
+
};
|
|
30
|
+
return <Editor
|
|
31
|
+
reference="ReportPresetsEditor"
|
|
32
|
+
title="Report Presets"
|
|
33
|
+
items={items}
|
|
34
|
+
ancillaryItems={ancillaryItems}
|
|
35
|
+
columnDefaults={columnDefaults}
|
|
36
|
+
{...props}
|
|
37
|
+
/>;
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ComboEditor } from './Combo.js';
|
|
2
|
+
import ReportPresetsEditorWindow from '../../../Window/ReportPresetsEditorWindow.js';
|
|
3
|
+
|
|
4
|
+
function ReportPresetsComboEditor(props) {
|
|
5
|
+
return <ComboEditor
|
|
6
|
+
reference="ReportPresetsCombo"
|
|
7
|
+
model="ReportPresets"
|
|
8
|
+
uniqueRepository={true}
|
|
9
|
+
Editor={ReportPresetsEditorWindow}
|
|
10
|
+
usePermissions={true}
|
|
11
|
+
{...props}
|
|
12
|
+
/>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default ReportPresetsComboEditor;
|
|
@@ -109,6 +109,7 @@ function Form(props) {
|
|
|
109
109
|
formSetup, // this fn will be executed after the form setup is complete
|
|
110
110
|
additionalEditButtons,
|
|
111
111
|
useAdditionalEditButtons = true,
|
|
112
|
+
additionalFooterItems, // overrides additionalFooterButtons if both are provided
|
|
112
113
|
additionalFooterButtons,
|
|
113
114
|
disableFooter = false,
|
|
114
115
|
hideResetButton = false,
|
|
@@ -207,6 +208,25 @@ function Form(props) {
|
|
|
207
208
|
// Fallback to empty schema that allows any fields and defaults to valid
|
|
208
209
|
return yup.object().noUnknown(false).default({});
|
|
209
210
|
})() || yup.object().shape({}), // on rare occasions, validatorToUse was null. This fixes it
|
|
211
|
+
requiredFieldsFromValidatorDescription = (() => {
|
|
212
|
+
if (editorType !== EDITOR_TYPE__PLAIN || !validatorToUse?.describe) {
|
|
213
|
+
return new Set();
|
|
214
|
+
}
|
|
215
|
+
const description = validatorToUse.describe();
|
|
216
|
+
const fields = description?.fields || {};
|
|
217
|
+
const requiredFieldNames = new Set();
|
|
218
|
+
Object.entries(fields).forEach(([fieldName, fieldConfig]) => {
|
|
219
|
+
if (!fieldConfig) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const hasRequiredTest = Array.isArray(fieldConfig.tests)
|
|
223
|
+
&& fieldConfig.tests.some((test) => test?.name === 'required');
|
|
224
|
+
if (fieldConfig.optional === false || hasRequiredTest) {
|
|
225
|
+
requiredFieldNames.add(fieldName);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return requiredFieldNames;
|
|
229
|
+
})(),
|
|
210
230
|
{
|
|
211
231
|
control,
|
|
212
232
|
formState,
|
|
@@ -899,6 +919,9 @@ function Form(props) {
|
|
|
899
919
|
if (!isMultiple) { // Don't require fields if editing multiple records
|
|
900
920
|
if (getIsRequired) {
|
|
901
921
|
isRequired = getIsRequired(formGetValues, formState);
|
|
922
|
+
} else if (requiredFieldsFromValidatorDescription.has(name)) {
|
|
923
|
+
// submitted validator (describe fallback for plain editor)
|
|
924
|
+
isRequired = true;
|
|
902
925
|
} else if (validatorToUse?.fields && validatorToUse.fields[name]?.exclusiveTests?.required) {
|
|
903
926
|
// submitted validator
|
|
904
927
|
isRequired = true;
|
|
@@ -1277,7 +1300,7 @@ function Form(props) {
|
|
|
1277
1300
|
formButtons = null,
|
|
1278
1301
|
scrollButtons = null,
|
|
1279
1302
|
footer = null,
|
|
1280
|
-
|
|
1303
|
+
footerItems = null,
|
|
1281
1304
|
formComponents,
|
|
1282
1305
|
editor,
|
|
1283
1306
|
additionalButtons,
|
|
@@ -1419,10 +1442,10 @@ function Form(props) {
|
|
|
1419
1442
|
if (!!onSubmit) {
|
|
1420
1443
|
showSubmitBtn = true;
|
|
1421
1444
|
}
|
|
1422
|
-
|
|
1445
|
+
footerItems =
|
|
1423
1446
|
<>
|
|
1424
|
-
|
|
1425
|
-
{additionalFooterButtons && _.map(additionalFooterButtons, (props, ix) => {
|
|
1447
|
+
{additionalFooterItems}
|
|
1448
|
+
{!additionalFooterItems && additionalFooterButtons && _.map(additionalFooterButtons, (props, ix) => {
|
|
1426
1449
|
let isDisabled = false;
|
|
1427
1450
|
if (props.disableOnInvalid) {
|
|
1428
1451
|
isDisabled = !formState.isValid;
|
|
@@ -1534,7 +1557,7 @@ function Form(props) {
|
|
|
1534
1557
|
'rounded-b-lg',
|
|
1535
1558
|
'bg-primary-700',
|
|
1536
1559
|
)}
|
|
1537
|
-
>{
|
|
1560
|
+
>{footerItems}</HStack>
|
|
1538
1561
|
</Box>;
|
|
1539
1562
|
} else {
|
|
1540
1563
|
if (!disableFooter) {
|
|
@@ -1559,7 +1582,7 @@ function Form(props) {
|
|
|
1559
1582
|
footerClassName += ' ' + footerProps.className;
|
|
1560
1583
|
}
|
|
1561
1584
|
footer = <Footer {...footerProps} className={footerClassName}>
|
|
1562
|
-
{
|
|
1585
|
+
{footerItems}
|
|
1563
1586
|
</Footer>;
|
|
1564
1587
|
}
|
|
1565
1588
|
}
|
|
@@ -446,6 +446,55 @@ function GridComponent(props) {
|
|
|
446
446
|
}
|
|
447
447
|
return items;
|
|
448
448
|
},
|
|
449
|
+
isScrollbarPress = (e) => {
|
|
450
|
+
if (CURRENT_MODE !== UI_MODE_WEB) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const
|
|
455
|
+
nativeEvent = e?.nativeEvent || e,
|
|
456
|
+
clientX = nativeEvent?.clientX,
|
|
457
|
+
clientY = nativeEvent?.clientY;
|
|
458
|
+
|
|
459
|
+
if (_.isNil(clientX) || _.isNil(clientY)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function isPointOnScrollbar(element) {
|
|
464
|
+
if (!element || typeof element.getBoundingClientRect !== 'function') {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const rect = element.getBoundingClientRect();
|
|
469
|
+
if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const verticalScrollbarWidth = element.offsetWidth - element.clientWidth;
|
|
474
|
+
if (verticalScrollbarWidth > 0 && clientX >= (rect.right - verticalScrollbarWidth)) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const horizontalScrollbarHeight = element.offsetHeight - element.clientHeight;
|
|
479
|
+
if (horizontalScrollbarHeight > 0 && clientY >= (rect.bottom - horizontalScrollbarHeight)) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return false;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const currentTarget = e?.currentTarget;
|
|
487
|
+
let target = nativeEvent?.target || e?.target;
|
|
488
|
+
|
|
489
|
+
while(target && target !== currentTarget) {
|
|
490
|
+
if (isPointOnScrollbar(target)) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
target = target.parentElement;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return isPointOnScrollbar(currentTarget);
|
|
497
|
+
},
|
|
449
498
|
renderRow = (row) => {
|
|
450
499
|
if (row.item.isDestroyed) {
|
|
451
500
|
return null;
|
|
@@ -534,6 +583,15 @@ function GridComponent(props) {
|
|
|
534
583
|
}
|
|
535
584
|
}}
|
|
536
585
|
onLongPress={(e) => {
|
|
586
|
+
if (isScrollbarPress(e)) {
|
|
587
|
+
if (e.preventDefault && e.cancelable) {
|
|
588
|
+
e.preventDefault();
|
|
589
|
+
}
|
|
590
|
+
if (e.stopPropagation) {
|
|
591
|
+
e.stopPropagation();
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
537
595
|
if (e.preventDefault && e.cancelable) {
|
|
538
596
|
e.preventDefault();
|
|
539
597
|
}
|
|
@@ -560,6 +618,15 @@ function GridComponent(props) {
|
|
|
560
618
|
// web only; happens before onLongPress triggers
|
|
561
619
|
// different behavior here than onLongPress:
|
|
562
620
|
// if user clicks on a header row or phantom record, or if onContextMenu is not set, pass to the browser's context menu
|
|
621
|
+
if (isScrollbarPress(e)) {
|
|
622
|
+
if (e.preventDefault && e.cancelable) {
|
|
623
|
+
e.preventDefault();
|
|
624
|
+
}
|
|
625
|
+
if (e.stopPropagation) {
|
|
626
|
+
e.stopPropagation();
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
563
630
|
if (isHeaderRow || isReorderMode) {
|
|
564
631
|
return
|
|
565
632
|
}
|
|
@@ -663,7 +663,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
663
663
|
} finally {
|
|
664
664
|
setIsWaitModalShown(false);
|
|
665
665
|
}
|
|
666
|
-
if (isSuccess) {
|
|
666
|
+
if (isSuccess && duplicateEntity) {
|
|
667
667
|
setIsIgnoreNextSelectionChange(true);
|
|
668
668
|
setSelection([duplicateEntity]);
|
|
669
669
|
doEdit();
|
|
@@ -22,8 +22,11 @@ import Form from '../Form/Form.js';
|
|
|
22
22
|
import IconButton from '../Buttons/IconButton.js';
|
|
23
23
|
import withComponent from '../Hoc/withComponent.js';
|
|
24
24
|
import withAlert from '../Hoc/withAlert.js';
|
|
25
|
+
import getComponentFromType from '../../Functions/getComponentFromType.js';
|
|
25
26
|
import testProps from '../../Functions/testProps.js';
|
|
26
27
|
import ChartLine from '../Icons/ChartLine.js';
|
|
28
|
+
import Calendar from '../Icons/Calendar.js';
|
|
29
|
+
import Plus from '../Icons/Plus.js';
|
|
27
30
|
import Pdf from '../Icons/Pdf.js';
|
|
28
31
|
import Excel from '../Icons/Excel.js';
|
|
29
32
|
import getReport from '../../Functions/getReport.js';
|
|
@@ -42,14 +45,22 @@ function Report(props) {
|
|
|
42
45
|
showReportHeaders = true,
|
|
43
46
|
isQuickReport = false,
|
|
44
47
|
isDisabled = false,
|
|
48
|
+
usePresets = false,
|
|
49
|
+
useQueue = false,
|
|
50
|
+
useScheduledReports = false,
|
|
45
51
|
disabledMessage = 'Report is Disabled',
|
|
46
52
|
additionalData = {},
|
|
47
53
|
quickReportData = {},
|
|
48
54
|
alert,
|
|
49
55
|
} = props,
|
|
50
|
-
|
|
56
|
+
formProps = props._form || {},
|
|
57
|
+
footerProps = formProps.footerProps || {},
|
|
58
|
+
footerClassName = clsx(
|
|
59
|
+
footerProps.className,
|
|
60
|
+
'flex-wrap',
|
|
61
|
+
),
|
|
51
62
|
onPressQuickReport = () => {
|
|
52
|
-
downloadReport({
|
|
63
|
+
return downloadReport({
|
|
53
64
|
reportId,
|
|
54
65
|
reportType: REPORT_TYPES__EXCEL,
|
|
55
66
|
showReportHeaders,
|
|
@@ -59,9 +70,13 @@ function Report(props) {
|
|
|
59
70
|
},
|
|
60
71
|
});
|
|
61
72
|
},
|
|
62
|
-
downloadReport = (args) => {
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
downloadReport = async (args) => {
|
|
74
|
+
try {
|
|
75
|
+
alert('Download started');
|
|
76
|
+
await getReport(args);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
alert(error?.message || 'Unable to download report. Please try again.');
|
|
79
|
+
}
|
|
65
80
|
};
|
|
66
81
|
|
|
67
82
|
const propsIcon = props._icon || {};
|
|
@@ -161,10 +176,64 @@ function Report(props) {
|
|
|
161
176
|
</VStackNative>;
|
|
162
177
|
}
|
|
163
178
|
|
|
179
|
+
let footerItems = [];
|
|
180
|
+
if (usePresets) {
|
|
181
|
+
footerItems.push({
|
|
182
|
+
...testProps('reportPresetsComboEditor'),
|
|
183
|
+
key: 'reportPresetsComboEditor',
|
|
184
|
+
type: 'ReportPresetsComboEditor',
|
|
185
|
+
tooltip: 'Report Presets',
|
|
186
|
+
placeholder: 'Report Presets',
|
|
187
|
+
disableEdit: true,
|
|
188
|
+
className: 'w-[100px]',
|
|
189
|
+
baseParams: {
|
|
190
|
+
reportId,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (useScheduledReports) {
|
|
195
|
+
footerItems.push({
|
|
196
|
+
...testProps('scheduleReportBtn'),
|
|
197
|
+
key: 'scheduleReportBtn',
|
|
198
|
+
type: 'Button',
|
|
199
|
+
tooltip: 'Schedule Report',
|
|
200
|
+
icon: Calendar,
|
|
201
|
+
onPress: (data) => scheduleReport({
|
|
202
|
+
reportId,
|
|
203
|
+
data: {
|
|
204
|
+
...data,
|
|
205
|
+
...additionalData,
|
|
206
|
+
},
|
|
207
|
+
reportType: REPORT_TYPES__EXCEL,
|
|
208
|
+
showReportHeaders,
|
|
209
|
+
}),
|
|
210
|
+
disableOnInvalid: true,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (useQueue) {
|
|
214
|
+
footerItems.push({
|
|
215
|
+
...testProps('queueBtn'),
|
|
216
|
+
key: 'queueBtn',
|
|
217
|
+
type: 'Button',
|
|
218
|
+
tooltip: 'Add to Queue',
|
|
219
|
+
icon: Plus,
|
|
220
|
+
onPress: (data) => addToQueue({
|
|
221
|
+
reportId,
|
|
222
|
+
data: {
|
|
223
|
+
...data,
|
|
224
|
+
...additionalData,
|
|
225
|
+
},
|
|
226
|
+
reportType: REPORT_TYPES__EXCEL,
|
|
227
|
+
showReportHeaders,
|
|
228
|
+
}),
|
|
229
|
+
disableOnInvalid: true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
164
232
|
if (!disableExcel) {
|
|
165
|
-
|
|
233
|
+
footerItems.push({
|
|
166
234
|
...testProps('excelBtn'),
|
|
167
235
|
key: 'excelBtn',
|
|
236
|
+
type: 'Button',
|
|
168
237
|
text: 'Download Excel',
|
|
169
238
|
icon: Excel,
|
|
170
239
|
onPress: (data) => downloadReport({
|
|
@@ -180,9 +249,10 @@ function Report(props) {
|
|
|
180
249
|
});
|
|
181
250
|
}
|
|
182
251
|
if (!disablePdf) {
|
|
183
|
-
|
|
252
|
+
footerItems.push({
|
|
184
253
|
...testProps('pdfBtn'),
|
|
185
254
|
key: 'pdfBtn',
|
|
255
|
+
type: 'Button',
|
|
186
256
|
text: 'Download PDF',
|
|
187
257
|
icon: Pdf,
|
|
188
258
|
onPress: (data) => downloadReport({
|
|
@@ -197,6 +267,12 @@ function Report(props) {
|
|
|
197
267
|
disableOnInvalid: true,
|
|
198
268
|
});
|
|
199
269
|
}
|
|
270
|
+
if (footerItems.length) {
|
|
271
|
+
footerItems = footerItems.map(item => {
|
|
272
|
+
const Component = getComponentFromType(item.type);
|
|
273
|
+
return <Component {...item} />;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
200
276
|
return <VStackNative
|
|
201
277
|
{...testProps('Report-' + reportId)}
|
|
202
278
|
className={clsx(
|
|
@@ -225,9 +301,13 @@ function Report(props) {
|
|
|
225
301
|
</VStack>
|
|
226
302
|
</HStack>
|
|
227
303
|
<Form
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{...
|
|
304
|
+
editorType={EDITOR_TYPE__PLAIN}
|
|
305
|
+
additionalFooterItems={footerItems}
|
|
306
|
+
{...formProps}
|
|
307
|
+
footerProps={{
|
|
308
|
+
...footerProps,
|
|
309
|
+
className: footerClassName,
|
|
310
|
+
}}
|
|
231
311
|
/>
|
|
232
312
|
</Box>
|
|
233
313
|
{isDisabled &&
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COPYRIGHT NOTICE
|
|
3
|
+
* This file is categorized as "Custom Source Code"
|
|
4
|
+
* and is subject to the terms and conditions defined in the
|
|
5
|
+
* "LICENSE.txt" file, which is part of this source code package.
|
|
6
|
+
*/
|
|
7
|
+
import UiGlobals from '../../UiGlobals.js';
|
|
8
|
+
import Panel from '../Panel/Panel.js';
|
|
9
|
+
import useAdjustedWindowSize from '../../Hooks/useAdjustedWindowSize.js';
|
|
10
|
+
import ReportPresetsEditor from '../Editor/ReportPresetsEditor.js';
|
|
11
|
+
|
|
12
|
+
export default function ReportPresetsEditorWindow(props) {
|
|
13
|
+
const {
|
|
14
|
+
style, // prevent it being passed to Editor
|
|
15
|
+
...propsToPass
|
|
16
|
+
} = props,
|
|
17
|
+
styles = UiGlobals.styles,
|
|
18
|
+
[width, height] = useAdjustedWindowSize(styles.DEFAULT_WINDOW_WIDTH, styles.DEFAULT_WINDOW_HEIGHT);
|
|
19
|
+
|
|
20
|
+
return <Panel
|
|
21
|
+
{...props}
|
|
22
|
+
reference="ReportPresetsEditorWindow"
|
|
23
|
+
isCollapsible={false}
|
|
24
|
+
model="ReportPresets"
|
|
25
|
+
titleSuffix={props.editorMode === 'EDITOR_MODE__VIEW' || props.isEditorViewOnly ? ' Viewer' : ' Editor'}
|
|
26
|
+
className="ReportPresetsEditorWindow bg-white p-0"
|
|
27
|
+
isWindow={true}
|
|
28
|
+
w={width}
|
|
29
|
+
h={height}
|
|
30
|
+
flex={null}
|
|
31
|
+
>
|
|
32
|
+
<ReportPresetsEditor {...propsToPass} />
|
|
33
|
+
</Panel>;
|
|
34
|
+
}
|
package/src/Components/index.js
CHANGED
|
@@ -283,6 +283,7 @@ import PlusMinusButton from './Buttons/PlusMinusButton.js';
|
|
|
283
283
|
import PmCalcDebugViewer from './Viewer/PmCalcDebugViewer.js';
|
|
284
284
|
import PmStatusesViewer from './Viewer/PmStatusesViewer.js';
|
|
285
285
|
import RadioGroup from './Form/Field/RadioGroup/RadioGroup.js';
|
|
286
|
+
import ReportPresetsComboEditor from './Form/Field/Combo/ReportPresetsComboEditor.js';
|
|
286
287
|
// import Slider from './Form/Field/Slider.js'; // Currently, Slider is not compatible with the new React architecture. Temporarily remove it from index.js to prevent issues.
|
|
287
288
|
import SquareButton from './Buttons/SquareButton.js';
|
|
288
289
|
import TabPanel from './Panel/TabPanel.js';
|
|
@@ -583,6 +584,7 @@ const components = {
|
|
|
583
584
|
PmCalcDebugViewer,
|
|
584
585
|
PmStatusesViewer,
|
|
585
586
|
RadioGroup,
|
|
587
|
+
ReportPresetsComboEditor,
|
|
586
588
|
// Slider,
|
|
587
589
|
SquareButton,
|
|
588
590
|
TabPanel,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import qs from 'qs';
|
|
2
|
+
import getErrorMessageFromResponse from './getErrorMessageFromResponse.js';
|
|
2
3
|
|
|
3
4
|
const downloadInBackground = async (url, data, authHeaders = {}) => {
|
|
4
5
|
try {
|
|
@@ -13,7 +14,8 @@ const downloadInBackground = async (url, data, authHeaders = {}) => {
|
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
if (!response.ok) {
|
|
16
|
-
|
|
17
|
+
const errorMessage = await getErrorMessageFromResponse(response);
|
|
18
|
+
throw new Error(errorMessage || `HTTP error! status: ${response.status}`);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
// Get the blob from the response
|
|
@@ -1,46 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import getErrorMessageFromResponse from './getErrorMessageFromResponse.js';
|
|
2
|
+
|
|
3
|
+
const downloadWithFetch = async (url, options = {}, win = null) => {
|
|
4
|
+
const res = await fetch(url, options);
|
|
5
|
+
|
|
6
|
+
if (!res.ok) {
|
|
7
|
+
const errorMessage = await getErrorMessageFromResponse(res);
|
|
8
|
+
throw new Error(errorMessage || `HTTP error! status: ${res.status}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const contentDisposition = res.headers.get('Content-Disposition');
|
|
12
|
+
let filename = 'download';
|
|
13
|
+
if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) {
|
|
14
|
+
const matches = /filename="([^"]*)"/.exec(contentDisposition);
|
|
15
|
+
if (matches != null && matches[1]) {
|
|
16
|
+
filename = matches[1];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const blob = await res.blob();
|
|
21
|
+
|
|
22
|
+
// if (!win) {
|
|
23
|
+
// const
|
|
24
|
+
// winName = 'Download',
|
|
25
|
+
// opts = 'location=0,menubar=0,scrollbars=0';
|
|
26
|
+
// win = window.open('about:blank', winName, opts);
|
|
27
|
+
// }
|
|
28
|
+
|
|
29
|
+
// const file = win.URL.createObjectURL(blob);
|
|
30
|
+
// obj.window = win;
|
|
31
|
+
const file = URL.createObjectURL(blob);
|
|
32
|
+
|
|
33
|
+
// const link = win.document.createElement('a');
|
|
34
|
+
const link = document.createElement('a');
|
|
35
|
+
link.href = file;
|
|
36
|
+
link.download = filename; // Set the filename from the Content-Disposition header
|
|
37
|
+
link.target = "_blank";
|
|
38
|
+
link.click();
|
|
39
|
+
|
|
40
|
+
// win.URL.revokeObjectURL(file); // if you revoke it, the PDF viewer will not be able to download the PDF.
|
|
41
|
+
|
|
42
|
+
// const newWin = win.open(file);
|
|
43
|
+
// win.location.assign(file);
|
|
44
|
+
// setTimeout(() => {
|
|
45
|
+
// win.close();
|
|
46
|
+
// }, 2000);
|
|
47
|
+
|
|
48
|
+
return { window: win };
|
|
45
49
|
};
|
|
46
50
|
export default downloadWithFetch;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const getErrorMessageFromResponse = async (response) => {
|
|
2
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
3
|
+
|
|
4
|
+
if (contentType.includes('application/json')) {
|
|
5
|
+
const errorData = await response.json().catch(() => null);
|
|
6
|
+
if (typeof errorData === 'string' && errorData.trim()) {
|
|
7
|
+
return errorData;
|
|
8
|
+
}
|
|
9
|
+
if (errorData?.message) {
|
|
10
|
+
return errorData.message;
|
|
11
|
+
}
|
|
12
|
+
if (errorData?.error) {
|
|
13
|
+
return errorData.error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const text = await response.text().catch(() => '');
|
|
18
|
+
return text?.trim() || null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default getErrorMessageFromResponse;
|
|
@@ -9,6 +9,72 @@ import {
|
|
|
9
9
|
getRepositoryAuthHeaders,
|
|
10
10
|
} from './authFunctions.js';
|
|
11
11
|
import UiGlobals from '../UiGlobals.js';
|
|
12
|
+
import _ from 'lodash';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// keeps safe serializable values (primitives, arrays, plain objects, Date to ISO),
|
|
17
|
+
// and removes cyclic/non-plain references, preventing "RangeError: Cyclic object value" errors.
|
|
18
|
+
const sanitizeReportData = (value, seen = new WeakSet()) => {
|
|
19
|
+
if (value == null) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const valueType = typeof value;
|
|
24
|
+
if (
|
|
25
|
+
valueType === 'string'
|
|
26
|
+
|| valueType === 'number'
|
|
27
|
+
|| valueType === 'boolean'
|
|
28
|
+
) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (valueType === 'bigint') {
|
|
32
|
+
return value.toString();
|
|
33
|
+
}
|
|
34
|
+
if (valueType === 'function' || valueType === 'symbol') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (value instanceof Date) {
|
|
39
|
+
return value.toISOString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (valueType !== 'object') {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (seen.has(value)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
seen.add(value);
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
const output = [];
|
|
53
|
+
for (const item of value) {
|
|
54
|
+
const sanitized = sanitizeReportData(item, seen);
|
|
55
|
+
if (sanitized !== undefined) {
|
|
56
|
+
output.push(sanitized);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
seen.delete(value);
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!_.isPlainObject(value)) {
|
|
64
|
+
seen.delete(value);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const output = {};
|
|
69
|
+
for (const [key, item] of Object.entries(value)) {
|
|
70
|
+
const sanitized = sanitizeReportData(item, seen);
|
|
71
|
+
if (sanitized !== undefined) {
|
|
72
|
+
output[key] = sanitized;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
seen.delete(value);
|
|
76
|
+
return output;
|
|
77
|
+
};
|
|
12
78
|
|
|
13
79
|
export default function getReport(args) {
|
|
14
80
|
const {
|
|
@@ -24,18 +90,19 @@ export default function getReport(args) {
|
|
|
24
90
|
|
|
25
91
|
const
|
|
26
92
|
url = UiGlobals.baseURL + 'Reports/getReport',
|
|
93
|
+
sanitizedData = sanitizeReportData(data) || {},
|
|
27
94
|
params = {
|
|
28
95
|
report_id: reportId,
|
|
29
96
|
outputFileType: reportType,
|
|
30
97
|
showReportHeaders,
|
|
31
|
-
...
|
|
98
|
+
...sanitizedData,
|
|
32
99
|
},
|
|
33
100
|
user = UiGlobals?.redux?.getState ? UiGlobals.redux.getState()?.auth?.user : null,
|
|
34
101
|
token = getUserToken(user),
|
|
35
102
|
authHeaders = getRepositoryAuthHeaders(token);
|
|
36
103
|
|
|
37
104
|
if (reportType === REPORT_TYPES__EXCEL) {
|
|
38
|
-
downloadInBackground(url, params, authHeaders);
|
|
105
|
+
return downloadInBackground(url, params, authHeaders);
|
|
39
106
|
} else {
|
|
40
107
|
const options = {
|
|
41
108
|
method: 'POST',
|
|
@@ -45,6 +112,6 @@ export default function getReport(args) {
|
|
|
45
112
|
},
|
|
46
113
|
body: JSON.stringify(params),
|
|
47
114
|
};
|
|
48
|
-
downloadWithFetch(url, options);
|
|
115
|
+
return downloadWithFetch(url, options);
|
|
49
116
|
}
|
|
50
117
|
};
|
|
@@ -344,7 +344,7 @@ export const verifyStartupAuthThunk = createAsyncThunk(
|
|
|
344
344
|
const userData = getUserData(user);
|
|
345
345
|
const token = getUserToken(userData);
|
|
346
346
|
if (!token) {
|
|
347
|
-
await dispatch(forceUnauthenticatedThunk(
|
|
347
|
+
await dispatch(forceUnauthenticatedThunk(null));
|
|
348
348
|
return false;
|
|
349
349
|
}
|
|
350
350
|
|
|
@@ -364,7 +364,7 @@ export const verifyStartupAuthThunk = createAsyncThunk(
|
|
|
364
364
|
dispatch(setAuthStatus(AUTH_STATUS_AUTHENTICATED));
|
|
365
365
|
return true;
|
|
366
366
|
} catch (error) {
|
|
367
|
-
await dispatch(forceUnauthenticatedThunk(
|
|
367
|
+
await dispatch(forceUnauthenticatedThunk(null));
|
|
368
368
|
return false;
|
|
369
369
|
}
|
|
370
370
|
}
|
|
@@ -855,6 +855,7 @@ function AttachmentsElement(props) {
|
|
|
855
855
|
'AttachmentsElement-icon-VStack1',
|
|
856
856
|
'h-full',
|
|
857
857
|
'flex-1',
|
|
858
|
+
'min-h-0',
|
|
858
859
|
'border',
|
|
859
860
|
'p-1',
|
|
860
861
|
isLoading ? [
|
|
@@ -866,6 +867,9 @@ function AttachmentsElement(props) {
|
|
|
866
867
|
<HStack
|
|
867
868
|
className={clsx(
|
|
868
869
|
'AttachmentsElement-HStack',
|
|
870
|
+
'flex-1',
|
|
871
|
+
'min-h-0',
|
|
872
|
+
'overflow-y-auto',
|
|
869
873
|
'gap-2',
|
|
870
874
|
'flex-wrap',
|
|
871
875
|
'items-start',
|