@onehat/ui 0.4.119 → 0.4.122

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.119",
3
+ "version": "0.4.122",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import { cloneElement, useState, useEffect, useRef, useCallback, } from 'react';
2
2
  import {
3
3
  BoxNative,
4
4
  HStack,
5
+ HStackNative,
5
6
  VStack,
6
7
  } from '@project-components/Gluestack';
7
8
  import clsx from 'clsx';
@@ -681,7 +682,7 @@ function Container(props) {
681
682
  return <VStack className="Container-all flex-1 min-w-0">
682
683
  {northComponent}
683
684
  {!getNorthIsCollapsed() && northSplitter}
684
- <HStack
685
+ <HStackNative
685
686
  className="Container-mid w-full flex-[100] min-w-0"
686
687
  onLayout={(e) => {
687
688
  // Measure available horizontal space for side panels.
@@ -707,7 +708,7 @@ function Container(props) {
707
708
  </VStack>
708
709
  {!getEastIsCollapsed() && eastSplitter}
709
710
  {eastComponent}
710
- </HStack>
711
+ </HStackNative>
711
712
  {!getSouthIsCollapsed() && southSplitter}
712
713
  {southComponent}
713
714
  </VStack>;
@@ -1,12 +1,12 @@
1
1
  import ArrayCombo from './ArrayCombo.js';
2
2
 
3
3
  const data = [
4
- ['+1 day', 'Daily'],
5
- ['+1 week', 'Weekly'],
6
- ['+2 week', 'Bi-weekly'],
7
- ['+1 month', 'Monthly'],
8
- ['+3 month', 'Quarterly'],
9
- ['+1 year', 'Yearly']
4
+ ['1 day', 'Daily'],
5
+ ['1 week', 'Weekly'],
6
+ // ['2 week', 'Bi-weekly'], // ambibuous, don't use
7
+ ['1 month', 'Monthly'],
8
+ ['1 quarter', 'Quarterly'],
9
+ ['1 year', 'Yearly']
10
10
  ];
11
11
 
12
12
  export default function IntervalsCombo(props) {
@@ -0,0 +1,33 @@
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
+
8
+ import ArrayCombo from './ArrayCombo.js';
9
+ import {
10
+ REPORT_QUEUE_STATUS__ALL,
11
+ REPORT_QUEUE_STATUS__COMPLETED,
12
+ REPORT_QUEUE_STATUS__FAILED,
13
+ REPORT_QUEUE_STATUS__IN_PROCESS,
14
+ REPORT_QUEUE_STATUS__PENDING,
15
+ } from '../../../../Constants/ReportQueueStatuses.js';
16
+
17
+ const data = [
18
+ [REPORT_QUEUE_STATUS__ALL, 'All'],
19
+ [REPORT_QUEUE_STATUS__PENDING, 'Only Pending'],
20
+ [REPORT_QUEUE_STATUS__IN_PROCESS, 'Only In Process'],
21
+ [REPORT_QUEUE_STATUS__FAILED, 'Only Failures'],
22
+ [REPORT_QUEUE_STATUS__COMPLETED, 'Only Completed'],
23
+ ];
24
+
25
+ function ReportQueueStatusesCombo(props) {
26
+ return <ArrayCombo
27
+ data={data}
28
+ disableDirectEntry={true}
29
+ {...props}
30
+ />;
31
+ }
32
+
33
+ export default ReportQueueStatusesCombo;
@@ -275,6 +275,7 @@ export const DateElement = forwardRef((props, ref) => {
275
275
  if (limitWidth) {
276
276
  width = 150;
277
277
  }
278
+ height = 150;
278
279
  break;
279
280
  default:
280
281
  }
@@ -1040,6 +1040,7 @@ function Form(props) {
1040
1040
  icon,
1041
1041
  selectorId,
1042
1042
  selectorSelectedField,
1043
+ disableTitleSuffix = false,
1043
1044
  ...itemPropsToPass
1044
1045
  } = item,
1045
1046
  titleElement;
@@ -1076,7 +1077,7 @@ function Form(props) {
1076
1077
  initialEditorMode={ancillaryInitialEditorMode}
1077
1078
  />;
1078
1079
  if (title) {
1079
- if (record?.displayValue) {
1080
+ if (record?.displayValue && !disableTitleSuffix) {
1080
1081
  title += ' for ' + record.displayValue;
1081
1082
  }
1082
1083
  titleElement = <Text
@@ -0,0 +1,277 @@
1
+
2
+ import { useState, useEffect, } from 'react';
3
+ import {
4
+ Box,
5
+ Text,
6
+ } from '@project-components/Gluestack';
7
+ import clsx from 'clsx';
8
+ import { useSelector, useDispatch } from 'react-redux';
9
+ import oneHatData from '@onehat/data';
10
+ import {
11
+ setIsWaitModalShown,
12
+ } from '../../Models/Slices/SystemSlice';
13
+ import {
14
+ selectUser,
15
+ } from '../../Models/Slices/AuthSlice.js';
16
+ import {
17
+ REPORT_QUEUE_STATUS__ALL,
18
+ } from '../../Constants/ReportQueueStatuses.js';
19
+ import IconButton from '../Buttons/IconButton';
20
+ import withAlert from '../../Components/Hoc/withAlert.js';
21
+ import useForceUpdate from '../../Hooks/useForceUpdate.js';
22
+ import getComponentFromType from '../../Functions/getComponentFromType.js';
23
+ import Rotate from '../Icons/Rotate.js';
24
+ import X from '../Icons/X.js';
25
+
26
+ function ReportsQueue(props) {
27
+ const {
28
+ // withAlert
29
+ alert,
30
+ showInfo,
31
+ } = props,
32
+ dispatch = useDispatch(),
33
+ forceUpdate = useForceUpdate(),
34
+ user = useSelector(selectUser),
35
+ UtilQueuedReports = oneHatData.getRepository('UtilQueuedReports'),
36
+ onRequeueFailedJob = async (entity) => {
37
+ dispatch(setIsWaitModalShown(true));
38
+
39
+ try {
40
+ const result = await UtilQueuedReports._send('POST', 'UtilQueuedReports/requeueFailedJob', {
41
+ util_queued_report_id: entity.id,
42
+ });
43
+ const response = UtilQueuedReports._processServerResponse(result);
44
+ if (response.success) {
45
+
46
+ await entity.reload();
47
+
48
+ forceUpdate();
49
+
50
+ showInfo('Job requeued successfully.');
51
+ }
52
+
53
+ } catch (error) {
54
+ alert('An error occurred while requeuing the job. Please try again.');
55
+ } finally {
56
+ dispatch(setIsWaitModalShown(false));
57
+ }
58
+ },
59
+ onCancel = async (entity) => {
60
+ dispatch(setIsWaitModalShown(true));
61
+
62
+ try {
63
+ const result = await UtilQueuedReports._send('POST', 'UtilQueuedReports/cancelPendingJob', {
64
+ util_queued_report_id: entity.id,
65
+ });
66
+ const response = UtilQueuedReports._processServerResponse(result);
67
+ if (response.success) {
68
+
69
+ await entity.reload();
70
+
71
+ forceUpdate();
72
+
73
+ showInfo('Job cancelled successfully.');
74
+ }
75
+
76
+ } catch (error) {
77
+ alert('An error occurred while cancelling the job. Please try again.');
78
+ } finally {
79
+ dispatch(setIsWaitModalShown(false));
80
+ }
81
+ };
82
+
83
+ useEffect(() => {
84
+
85
+ setTimeout(() => {
86
+ UtilQueuedReports.reload();
87
+ }, 60 * 1000);
88
+
89
+ }, [UtilQueuedReports]);
90
+
91
+ const UtilQueuedReportsFilteredGridEditor = getComponentFromType('UtilQueuedReportsFilteredGridEditor');
92
+
93
+ return <UtilQueuedReportsFilteredGridEditor
94
+ reference="ReportsQueue"
95
+ usePermissions={false}
96
+ Repository={UtilQueuedReports}
97
+
98
+ title="Reports Queue"
99
+ className="w-full h-full"
100
+ searchAllText={false}
101
+ showClearFiltersButton={false}
102
+ customFilters={[
103
+ {
104
+ id: 'status',
105
+ title: 'Status',
106
+ tooltip: 'Select which status to display in the queue.',
107
+ field: 'status',
108
+ type: 'ReportQueueStatusesCombo',
109
+ value: REPORT_QUEUE_STATUS__ALL,
110
+ getRepoFilters: (value) => {
111
+ return [
112
+ {
113
+ name: 'status',
114
+ value,
115
+ },
116
+ ];
117
+ },
118
+ },
119
+ {
120
+ id: 'showAllUsers',
121
+ title: 'All Users?',
122
+ tooltip: 'Should we include queued reports from ALL users, so you can see the overall queue status?',
123
+ field: 'showAllUsers',
124
+ type: 'Toggle',
125
+ value: false,
126
+ getRepoFilters: (value) => {
127
+ return [
128
+ {
129
+ name: 'showAllUsers',
130
+ value,
131
+ },
132
+ ];
133
+ },
134
+ },
135
+ ]}
136
+ columnsConfig={[
137
+ {
138
+ id: 'action',
139
+ header: 'Action',
140
+ w: 70,
141
+ isSortable: false,
142
+ isEditable: false,
143
+ isReorderable: false,
144
+ isResizable: false,
145
+ isHidable: false,
146
+ renderer: (entity, fieldName, cellProps, key) => {
147
+ const
148
+ isUser = entity.util_queued_reports__user_id === user?.id,
149
+ className = clsx(
150
+ cellProps.className,
151
+ 'flex',
152
+ 'items-center',
153
+ 'justify-center',
154
+ );
155
+ let action,
156
+ icon,
157
+ tooltip;
158
+ if (entity.util_queued_reports__is_in_process && isUser) {
159
+ action = onCancel;
160
+ icon = X;
161
+ tooltip = 'Cancel this report';
162
+ } else if (entity.util_queued_reports__success === false && isUser) {
163
+ action = onRequeueFailedJob;
164
+ icon = Rotate;
165
+ tooltip = 'Requeue this failed report';
166
+ } else {
167
+ // no available action
168
+ return <Box {...cellProps} className={className} />;
169
+ }
170
+ return <IconButton
171
+ key={key}
172
+ {...cellProps}
173
+ className={className}
174
+ icon={icon}
175
+ _icon={{
176
+ size: 'xl',
177
+ }}
178
+ onPress={() => action(entity)}
179
+ tooltip={tooltip}
180
+ />;
181
+ },
182
+ },
183
+ {
184
+ "id": "util_queued_reports__report_title",
185
+ "header": "Report", // MOD
186
+ "fieldName": "util_queued_reports__report_title",
187
+ "isSortable": false,
188
+ "isEditable": false,
189
+ "isReorderable": true,
190
+ "isResizable": true,
191
+ "w": 250 // MOD
192
+ },
193
+ {
194
+ "id": "util_queued_reports__report_preset_name",
195
+ "header": "Preset", // MOD
196
+ "fieldName": "util_queued_reports__report_preset_name",
197
+ "isSortable": false,
198
+ "isEditable": false,
199
+ "isReorderable": true,
200
+ "isResizable": true,
201
+ "w": 150
202
+ },
203
+ {
204
+ "id": "util_queued_reports__submitted",
205
+ "header": "Submitted",
206
+ "fieldName": "util_queued_reports__submitted",
207
+ "isSortable": true,
208
+ "isEditable": true,
209
+ "isReorderable": true,
210
+ "isResizable": true,
211
+ "w": 200
212
+ },
213
+ {
214
+ "id": "util_queued_reports__is_in_process",
215
+ "header": "In Process?",
216
+ "fieldName": "util_queued_reports__is_in_process",
217
+ "isSortable": true,
218
+ "isEditable": true,
219
+ "isReorderable": true,
220
+ "isResizable": true,
221
+ "w": 100
222
+ },
223
+ {
224
+ "id": "util_queued_reports__success",
225
+ "header": "Success",
226
+ "fieldName": "util_queued_reports__success",
227
+ "isSortable": true,
228
+ "isEditable": true,
229
+ "isReorderable": true,
230
+ "isResizable": true,
231
+ "w": 100
232
+ },
233
+ {
234
+ "id": "util_queued_reports__run_time",
235
+ "header": "Run Time",
236
+ "fieldName": "util_queued_reports__run_time",
237
+ "isSortable": true,
238
+ "isEditable": true,
239
+ "isReorderable": true,
240
+ "isResizable": true,
241
+ "w": 100
242
+ },
243
+ {
244
+ "id": "users__username",
245
+ "header": "User",
246
+ "fieldName": "users__username",
247
+ "isSortable": false,
248
+ "isEditable": false,
249
+ "isReorderable": false,
250
+ "isResizable": false,
251
+ "w": 100
252
+ },
253
+ ]}
254
+ getRowProps={(item) => {
255
+ const rowProps = {
256
+ borderBottomWidth: 1,
257
+ borderBottomColor: 'trueGray.500',
258
+ };
259
+ if (item.util_queued_reports__is_in_process) {
260
+ rowProps.bg = '#f9eabb';
261
+ } else if (item.util_queued_reports__success === false) {
262
+ rowProps.bg = '#ffd1d1';
263
+ }
264
+ return rowProps;
265
+ }}
266
+ disableAdd={true}
267
+ disableEdit={true}
268
+ disableDelete={true}
269
+ disableDuplicate={true}
270
+ disableView={true}
271
+ disableCopy={true}
272
+
273
+ {...props}
274
+ />;
275
+ }
276
+
277
+ export default withAlert(ReportsQueue);
@@ -63,6 +63,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
63
63
  initialEditorMode = EDITOR_MODE__VIEW,
64
64
  stayInEditModeOnSelectionChange = false,
65
65
  inheritParentEditorMode = true,
66
+ ignoreGlobalStayInEditModeOnSelectionChange = false,
66
67
 
67
68
  // withComponent
68
69
  self,
@@ -863,8 +864,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
863
864
 
864
865
  let isIgnoreNextSelectionChange = getIsIgnoreNextSelectionChange(),
865
866
  doStayInEditModeOnSelectionChange = stayInEditModeOnSelectionChange;
866
- if (!_.isNil(UiGlobals.stayInEditModeOnSelectionChange)) {
867
- // allow global override to for this property
867
+ if (!_.isNil(UiGlobals.stayInEditModeOnSelectionChange) && !ignoreGlobalStayInEditModeOnSelectionChange) {
868
+ // allow global override for this property
868
869
  doStayInEditModeOnSelectionChange = UiGlobals.stayInEditModeOnSelectionChange;
869
870
  }
870
871
  if (doStayInEditModeOnSelectionChange) {
@@ -0,0 +1,12 @@
1
+ import { createIcon } from "../Gluestack/icon";
2
+ import Svg, { Path } from "react-native-svg"
3
+
4
+ const SvgComponent = createIcon({
5
+ Root: Svg,
6
+ viewBox: '420 310 730 940',
7
+ path: <Path
8
+ d="M972.547 1227.359c-38.672 12.891-84.961 19.336-138.867 19.336-112.5 0-205.469-33.593-278.907-100.781-89.062-80.859-133.593-199.609-133.593-356.25 0-157.812 45.703-277.148 137.109-358.008 74.609-66.015 167.383-99.023 278.32-99.023 111.719 0 205.469 34.961 281.25 104.883 87.5 80.859 131.25 193.945 131.25 339.257 0 76.954-9.375 141.407-28.125 193.36-15.234 49.609-37.695 90.82-67.382 123.633l99.609 93.164-94.336 98.437-104.297-98.437c-31.64 19.14-58.984 32.617-82.031 40.429zM933.875 1071.5l-87.305-83.203 93.164-97.266 87.305 83.203c13.672-28.125 23.242-52.734 28.711-73.828 8.594-31.64 12.891-68.554 12.891-110.742 0-96.875-19.825-171.777-59.473-224.707-39.648-52.93-97.559-79.395-173.73-79.395-71.485 0-128.516 25.391-171.094 76.172-42.578 50.782-63.867 126.758-63.867 227.93 0 118.359 30.468 203.125 91.406 254.297 39.453 33.203 86.719 49.805 141.797 49.805 20.703 0 40.625-2.539 59.765-7.618 10.547-2.734 24.024-7.617 40.43-14.648z"
9
+ />,
10
+ });
11
+
12
+ export default SvgComponent
@@ -1,4 +1,4 @@
1
- import { cloneElement, isValidElement } from 'react';
1
+ import { cloneElement, isValidElement, useState, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  HStack,
@@ -9,7 +9,11 @@ import {
9
9
  VStackNative,
10
10
  } from '@project-components/Gluestack';
11
11
  import clsx from 'clsx';
12
+ import { useSelector, useDispatch } from 'react-redux';
12
13
  import { EDITOR_TYPE__PLAIN } from '../../Constants/Editor';
14
+ import {
15
+ selectUser,
16
+ } from '../../Models/Slices/AuthSlice.js';
13
17
  import {
14
18
  UI_MODE_WEB,
15
19
  CURRENT_MODE,
@@ -18,6 +22,7 @@ import {
18
22
  REPORT_TYPES__EXCEL,
19
23
  REPORT_TYPES__PDF,
20
24
  } from '../../Constants/ReportTypes.js';
25
+ import oneHatData from '@onehat/data';
21
26
  import Form from '../Form/Form.js';
22
27
  import IconButton from '../Buttons/IconButton.js';
23
28
  import withComponent from '../Hoc/withComponent.js';
@@ -28,8 +33,10 @@ import ChartLine from '../Icons/ChartLine.js';
28
33
  import Calendar from '../Icons/Calendar.js';
29
34
  import Plus from '../Icons/Plus.js';
30
35
  import Pdf from '../Icons/Pdf.js';
36
+ import Share from '../Icons/Share.js';
31
37
  import Excel from '../Icons/Excel.js';
32
38
  import getReport from '../../Functions/getReport.js';
39
+ import * as yup from 'yup';
33
40
  import _ from 'lodash';
34
41
 
35
42
  function Report(props) {
@@ -41,7 +48,9 @@ function Report(props) {
41
48
  description,
42
49
  reportId,
43
50
  disablePdf = false,
51
+ pdfButtonText = 'Download PDF',
44
52
  disableExcel = false,
53
+ excelButtonText = 'Download Excel',
45
54
  showReportHeaders = true,
46
55
  isQuickReport = false,
47
56
  isDisabled = false,
@@ -51,9 +60,41 @@ function Report(props) {
51
60
  disabledMessage = 'Report is Disabled',
52
61
  additionalData = {},
53
62
  quickReportData = {},
63
+
64
+ // withAlert
54
65
  alert,
66
+ showInfo,
67
+ showModal,
68
+ hideModal,
69
+
70
+ // withComponent
71
+ self,
55
72
  } = props,
56
73
  formProps = props._form || {},
74
+ hasFormItems = formProps?.items?.[0]?.items?.length,
75
+ showPresets = usePresets && hasFormItems,
76
+ user = useSelector(selectUser),
77
+ [isValid, setIsValid] = useState(!hasFormItems), // if there are no form items, consider the form valid by default; otherwise, start as invalid until the form says otherwise
78
+ getCurrentReportFormData = () => {
79
+ const
80
+ form = self?.children?.form,
81
+ formValues = form?.formGetValues?.();
82
+ if (!_.isPlainObject(formValues)) {
83
+ alert('Unable to get form data');
84
+ return {};
85
+ }
86
+ return formValues;
87
+ },
88
+ setCurrentReportFormData = (data) => {
89
+ const form = self?.children?.form;
90
+ if (!form || !_.isPlainObject(data)) {
91
+ alert('Unable to set form data');
92
+ return;
93
+ }
94
+ _.each(data, (value, key) => {
95
+ form.formSetValue(key, value);
96
+ });
97
+ },
57
98
  footerProps = formProps.footerProps || {},
58
99
  footerClassName = clsx(
59
100
  footerProps.className,
@@ -77,6 +118,175 @@ function Report(props) {
77
118
  } catch (error) {
78
119
  alert(error?.message || 'Unable to download report. Please try again.');
79
120
  }
121
+ },
122
+ manageReportSchedules = async (formData) => {
123
+ let defaultValues = {
124
+ report_schedules__additional_data: additionalData,
125
+ };
126
+ if (hasFormItems) {
127
+ // check to make sure there is at least one ReportPreset for this report, since the schedule needs to be based on a preset (which captures the form config)
128
+ // If not, show alert saying create preset first
129
+ const ReportPresets = self?.children?.reportPresetsComboEditor?.repository;
130
+ if (!ReportPresets) {
131
+ alert('Unable to access report presets. Please try again.');
132
+ return;
133
+ }
134
+ let reportPresets = ReportPresets.getEntities();
135
+ if (!reportPresets?.length) {
136
+ await ReportPresets.load();
137
+ reportPresets = ReportPresets.getEntities();
138
+ }
139
+ if (!reportPresets?.length) {
140
+ alert('Please create at least one report preset first, since schedules are based on presets.');
141
+ return;
142
+ }
143
+ } else {
144
+ // Form is empty, but the ReportSchedule needs a report_preset_id to work.
145
+ // Get or create one.
146
+ const ReportPresets = oneHatData.getRepository('ReportPresets');
147
+ const result = await ReportPresets._send('POST', 'ReportPresets/getOrCreate', {
148
+ reportId,
149
+ });
150
+ const response = ReportPresets._processServerResponse(result);
151
+ if (!response.success) {
152
+ showInfo('Failed to get or create report preset: ' + (response.message || 'Unknown error'));
153
+ return;
154
+ }
155
+ defaultValues.report_schedules__report_preset_id = response.root.id;
156
+ }
157
+ const ReportSchedulesGridEditor = getComponentFromType('ReportSchedulesGridEditor');
158
+ showModal({
159
+ title: 'Schedules for "' + title + '"',
160
+ body: <ReportSchedulesGridEditor
161
+ baseParams={{
162
+ 'conditions[reportid]': reportId,
163
+ }}
164
+ _editor={{
165
+ hasFormItems,
166
+ reportId,
167
+ }}
168
+ defaultValues={defaultValues}
169
+ />,
170
+ canClose: true,
171
+ whichModal: 'schedulesModal',
172
+ h: 800,
173
+ w: 1100,
174
+ });
175
+ },
176
+ getRepository = () => {
177
+ let repository;
178
+ try {
179
+ // There is no 'Reports' repository (bc there is no 'Reports' model),
180
+ // so just get the first OneBuild repository; doesn't matter which one
181
+
182
+ repository = oneHatData.getRepositoriesByType('onebuild', true); // true to get the first only
183
+
184
+ } catch (error) {
185
+ alert('Error getting repository: ' + (error?.message || error));
186
+ return null;
187
+ }
188
+ return repository;
189
+ },
190
+ addToQueue = async (formData) => {
191
+ const
192
+ repository = getRepository(),
193
+ data = {
194
+ report_id: reportId,
195
+ ...formData,
196
+ ...additionalData,
197
+ },
198
+ result = await repository._send('POST', 'Reports/addToQueue', data),
199
+ response = repository._processServerResponse(result);
200
+ if (!response.success) {
201
+ alert(response.message || 'Failed to add report to queue');
202
+ return;
203
+ }
204
+
205
+ showInfo('Report added to queue.');
206
+ },
207
+ selectReportPreset = (reportPresetId) => {
208
+ // Change the form settings based on the selected preset
209
+ const
210
+ form = self?.children?.form,
211
+ ReportPresets = self?.children?.reportPresetsComboEditor?.repository;
212
+ if (!form || !ReportPresets) {
213
+ return;
214
+ }
215
+
216
+ const reportPreset = ReportPresets?.getById(reportPresetId);
217
+ if (!reportPreset) {
218
+ alert('Selected report preset not found');
219
+ return;
220
+ }
221
+
222
+ // apply the config to the form
223
+ const config = reportPreset.properties.report_presets__config.getParsedValue(); // get the actual JS object
224
+ setCurrentReportFormData(config);
225
+ },
226
+ shareReportPreset = (parent) => {
227
+
228
+ // show a Modal with UserSelector, excluding current user.
229
+ showModal({
230
+ title: 'Share Report Preset',
231
+ body: <Form
232
+ instructions="Please select which user to share with."
233
+ editorType={EDITOR_TYPE__PLAIN}
234
+ className="flex-1"
235
+ items={[
236
+ {
237
+ name: 'instructions',
238
+ type: 'DisplayField',
239
+ text: 'Please select which user to share with.',
240
+ className: 'mb-3',
241
+ },
242
+ {
243
+ type: 'Column',
244
+ flex: 1,
245
+ items: [
246
+ {
247
+ name: 'user_id',
248
+ type: 'UsersCombo',
249
+ label: 'User',
250
+ baseParams: {
251
+ 'conditions[id <>]': user.id,
252
+ },
253
+ },
254
+ ],
255
+ },
256
+ ]}
257
+ validator={yup.object({
258
+ user_id: yup.number().integer().required(),
259
+ })}
260
+ onCancel={(e) => {
261
+ hideModal();
262
+ }}
263
+ onClose={(e) => {
264
+ hideModal();
265
+ }}
266
+ onSubmit={async (data, e) => {
267
+ const
268
+ ReportPresets = self?.children?.reportPresetsComboEditor?.repository,
269
+ reportPreset = parent.selection[0],
270
+ params = {
271
+ report_preset_id: reportPreset.id,
272
+ user_id: data.user_id,
273
+ },
274
+ result = await ReportPresets._send('POST', 'ReportPresets/share', params),
275
+ response = ReportPresets._processServerResponse(result);
276
+
277
+ // Close the modal
278
+ hideModal();
279
+
280
+ if (response.success) {
281
+ showInfo('Report preset shared successfully.');
282
+ }
283
+ }}
284
+ />,
285
+ canClose: true,
286
+ whichModal: 'shareReportPresetModal',
287
+ h: 220,
288
+ w: 500,
289
+ });
80
290
  };
81
291
 
82
292
  const propsIcon = props._icon || {};
@@ -177,37 +387,54 @@ function Report(props) {
177
387
  }
178
388
 
179
389
  let footerItems = [];
180
- if (usePresets) {
390
+ if (showPresets) { // if no form items, no need for ReportPresets!
181
391
  footerItems.push({
182
392
  ...testProps('reportPresetsComboEditor'),
393
+ parent: self,
394
+ reference: 'reportPresetsComboEditor',
183
395
  key: 'reportPresetsComboEditor',
184
396
  type: 'ReportPresetsComboEditor',
185
397
  tooltip: 'Report Presets',
186
- placeholder: 'Report Presets',
187
- disableEdit: true,
188
- className: 'w-[100px]',
398
+ placeholder: 'Presets',
399
+ disableEdit: true, // too complicated to edit, just allow add/delete/share
400
+ className: 'w-[130px]',
189
401
  baseParams: {
190
- reportId,
402
+ 'conditions[reportid]': reportId, // reportid is a generated field, so you can search on it, but change capitalization
403
+ },
404
+ onChangeValue: selectReportPreset,
405
+ _grid: {
406
+ canRecordBeAdded: () => isValid, // only allow creating a preset if the form is valid, since the preset will capture the current form config
407
+ onBeforeAdd: (addValues) => {
408
+ // add the current form values to ReportPresets.config when creating a new preset
409
+ return {
410
+ ...addValues,
411
+ report_presets__config: {
412
+ ...getCurrentReportFormData(),
413
+ report_id: reportId,
414
+ },
415
+ };
416
+ },
417
+ additionalToolbarButtons: [
418
+ {
419
+ ...testProps('shareBtn'),
420
+ key: 'shareBtn',
421
+ text: 'Share Report Preset',
422
+ icon: Share,
423
+ getIsButtonDisabled: (selection) => !selection?.[0]?.id,
424
+ handler: shareReportPreset,
425
+ },
426
+ ],
191
427
  },
192
428
  });
193
429
  }
194
430
  if (useScheduledReports) {
195
431
  footerItems.push({
196
- ...testProps('scheduleReportBtn'),
197
- key: 'scheduleReportBtn',
432
+ ...testProps('manageReportSchedulesBtn'),
433
+ key: 'manageReportSchedulesBtn',
198
434
  type: 'Button',
199
- tooltip: 'Schedule Report',
435
+ tooltip: 'Manage delivery schedules for this report',
200
436
  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,
437
+ onPress: manageReportSchedules,
211
438
  });
212
439
  }
213
440
  if (useQueue) {
@@ -215,17 +442,9 @@ function Report(props) {
215
442
  ...testProps('queueBtn'),
216
443
  key: 'queueBtn',
217
444
  type: 'Button',
218
- tooltip: 'Add to Queue',
445
+ tooltip: 'Immediately add to Queue',
219
446
  icon: Plus,
220
- onPress: (data) => addToQueue({
221
- reportId,
222
- data: {
223
- ...data,
224
- ...additionalData,
225
- },
226
- reportType: REPORT_TYPES__EXCEL,
227
- showReportHeaders,
228
- }),
447
+ onPress: addToQueue,
229
448
  disableOnInvalid: true,
230
449
  });
231
450
  }
@@ -234,8 +453,9 @@ function Report(props) {
234
453
  ...testProps('excelBtn'),
235
454
  key: 'excelBtn',
236
455
  type: 'Button',
237
- text: 'Download Excel',
456
+ text: excelButtonText,
238
457
  icon: Excel,
458
+ tooltip: excelButtonText !== 'Download Excel' ? 'Download Excel' : null,
239
459
  onPress: (data) => downloadReport({
240
460
  reportId,
241
461
  data: {
@@ -253,8 +473,9 @@ function Report(props) {
253
473
  ...testProps('pdfBtn'),
254
474
  key: 'pdfBtn',
255
475
  type: 'Button',
256
- text: 'Download PDF',
476
+ text: pdfButtonText,
257
477
  icon: Pdf,
478
+ tooltip: pdfButtonText !== 'Download PDF' ? 'Download PDF' : null,
258
479
  onPress: (data) => downloadReport({
259
480
  reportId,
260
481
  data: {
@@ -270,9 +491,17 @@ function Report(props) {
270
491
  if (footerItems.length) {
271
492
  footerItems = footerItems.map(item => {
272
493
  const Component = getComponentFromType(item.type);
273
- return <Component {...item} />;
494
+ const { key, ...componentProps } = item;
495
+ return <Component key={key} {...componentProps} />;
274
496
  });
275
497
  }
498
+ let additionalDataComponent = null;
499
+ if (!_.isEmpty(additionalData)) {
500
+ const ReportAdditionalData = getComponentFromType('ReportAdditionalData');
501
+ additionalDataComponent = <ReportAdditionalData
502
+ additionalData={additionalData}
503
+ />;
504
+ }
276
505
  return <VStackNative
277
506
  {...testProps('Report-' + reportId)}
278
507
  className={clsx(
@@ -298,9 +527,12 @@ function Report(props) {
298
527
  <VStack className="flex-1">
299
528
  <Text className="text-2xl max-w-full">{title}</Text>
300
529
  <Text className="text-sm">{description}</Text>
530
+ {additionalDataComponent}
301
531
  </VStack>
302
532
  </HStack>
303
533
  <Form
534
+ parent={self}
535
+ reference="form"
304
536
  editorType={EDITOR_TYPE__PLAIN}
305
537
  additionalFooterItems={footerItems}
306
538
  {...formProps}
@@ -308,6 +540,9 @@ function Report(props) {
308
540
  ...footerProps,
309
541
  className: footerClassName,
310
542
  }}
543
+ onValidityChange={(isValid) => {
544
+ setIsValid(isValid);
545
+ }}
311
546
  />
312
547
  </Box>
313
548
  {isDisabled &&
@@ -353,4 +588,13 @@ function Report(props) {
353
588
  </VStackNative>;
354
589
  }
355
590
 
356
- export default withComponent(withAlert(Report));
591
+ function withAdditionalProps(WrappedComponent) {
592
+ return (props) => {
593
+ return <WrappedComponent
594
+ reference={props.reference || 'report'}
595
+ {...props}
596
+ />;
597
+ };
598
+ }
599
+
600
+ export default withAdditionalProps(withComponent(withAlert(Report)));
@@ -7,7 +7,10 @@ import {
7
7
  } from '@project-components/Gluestack';
8
8
  import clsx from 'clsx';
9
9
  import ChartPie from '../Icons/ChartPie.js';
10
+ import Q from '../Icons/Q.js';
11
+ import getComponentFromType from '../../Functions/getComponentFromType.js';
10
12
  import ScreenHeader from '../Layout/ScreenHeader.js';
13
+ import ReportsQueue from '../Grid/ReportsQueue.js';
11
14
  import TabBar from '../Tab/TabBar.js';
12
15
  import _ from 'lodash';
13
16
 
@@ -18,6 +21,7 @@ export default function ReportsManager(props) {
18
21
  reports = [],
19
22
  reportTabs,
20
23
  initialReportTabIx = 0,
24
+ showQueueTab = false,
21
25
  id,
22
26
  self,
23
27
  isActive = false,
@@ -65,6 +69,14 @@ export default function ReportsManager(props) {
65
69
  </VStackNative>
66
70
  </ScrollView>,
67
71
  })) : [];
72
+
73
+ if (showQueueTab) {
74
+ tabBarTabs.push({
75
+ title: 'Queue',
76
+ icon: Q,
77
+ content: <ReportsQueue />,
78
+ });
79
+ }
68
80
 
69
81
  return <VStack
70
82
  className="overflow-hidden flex-1 w-full"
@@ -284,6 +284,7 @@ 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
286
  import ReportPresetsComboEditor from './Form/Field/Combo/ReportPresetsComboEditor.js';
287
+ import ReportQueueStatusesCombo from './Form/Field/Combo/ReportQueueStatusesCombo.js';
287
288
  // 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.
288
289
  import SquareButton from './Buttons/SquareButton.js';
289
290
  import TabPanel from './Panel/TabPanel.js';
@@ -585,6 +586,7 @@ const components = {
585
586
  PmStatusesViewer,
586
587
  RadioGroup,
587
588
  ReportPresetsComboEditor,
589
+ ReportQueueStatusesCombo,
588
590
  // Slider,
589
591
  SquareButton,
590
592
  TabPanel,
@@ -0,0 +1,5 @@
1
+ export const REPORT_QUEUE_STATUS__ALL = 'ALL';
2
+ export const REPORT_QUEUE_STATUS__PENDING = 'PENDING';
3
+ export const REPORT_QUEUE_STATUS__IN_PROCESS = 'IN_PROCESS';
4
+ export const REPORT_QUEUE_STATUS__COMPLETED = 'COMPLETED';
5
+ export const REPORT_QUEUE_STATUS__FAILED = 'FAILED';
@@ -233,7 +233,7 @@ function AttachmentsElement(props) {
233
233
 
234
234
  } = props,
235
235
  styles = UiGlobals.styles,
236
- model = _.isArray(selectorSelected) && selectorSelected[0] ? selectorSelected[0].repository?.name : selectorSelected?.repository?.name,
236
+ model = _.isArray(selectorSelected) && selectorSelected[0] ? selectorSelected[0].schema?.name : selectorSelected?.schema?.name,
237
237
  modelidCalc = _.isArray(selectorSelected) ? _.map(selectorSelected, (entity) => entity[selectorSelectedField]) : selectorSelected?.[selectorSelectedField],
238
238
  modelid = useRef(modelidCalc),
239
239
  id = props.id || (model && modelid.current ? `attachments-${model}-${modelid.current}` : 'attachments'),
@@ -770,7 +770,7 @@ function AttachmentsElement(props) {
770
770
  wasAlreadyLoaded = AttachmentDirectories.isLoaded,
771
771
  currentConditions = AttachmentDirectories.getParamConditions() || {},
772
772
  newConditions = {
773
- 'conditions[AttachmentDirectories.model]': selectorSelected.repository.name,
773
+ 'conditions[AttachmentDirectories.model]': selectorSelected.schema?.name,
774
774
  'conditions[AttachmentDirectories.modelid]': selectorSelected[selectorSelectedField],
775
775
  };
776
776
  let doReload = false;
@@ -1135,7 +1135,7 @@ function AttachmentsElement(props) {
1135
1135
  maxFileSize={styles.ATTACHMENTS_MAX_FILESIZE}
1136
1136
  autoClean={true}
1137
1137
  uploadConfig={{
1138
- url: Attachments.api.baseURL + Attachments.schema.name + '/uploadAttachment',
1138
+ url: Attachments.api.baseURL + Attachments.schema?.name + '/uploadAttachment',
1139
1139
  method: 'POST',
1140
1140
  headers: Attachments.headers,
1141
1141
  autoUpload,