@onehat/ui 0.4.119 → 0.4.120

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.120",
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) {
@@ -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
@@ -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,161 @@ 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
+ if (hasFormItems) {
124
+ // 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)
125
+ // If not, show alert saying create preset first
126
+ const ReportPresets = self?.children?.reportPresetsComboEditor?.repository;
127
+ if (!ReportPresets) {
128
+ alert('Unable to access report presets. Please try again.');
129
+ return;
130
+ }
131
+ let reportPresets = ReportPresets.getEntities();
132
+ if (!reportPresets?.length) {
133
+ await ReportPresets.load();
134
+ reportPresets = ReportPresets.getEntities();
135
+ }
136
+ if (!reportPresets?.length) {
137
+ alert('Please create at least one report preset first, since schedules are based on presets.');
138
+ return;
139
+ }
140
+ }
141
+ const ReportSchedulesGridEditor = getComponentFromType('ReportSchedulesGridEditor');
142
+ showModal({
143
+ title: 'Schedules for "' + title + '"',
144
+ body: <ReportSchedulesGridEditor
145
+ baseParams={{
146
+ 'conditions[reportid]': reportId,
147
+ }}
148
+ _editor={{
149
+ hasFormItems,
150
+ reportId,
151
+ }}
152
+ defaultValues={{
153
+ report_schedules__additional_data: additionalData,
154
+ }}
155
+ />,
156
+ canClose: true,
157
+ whichModal: 'schedulesModal',
158
+ h: 800,
159
+ w: 1100,
160
+ });
161
+ },
162
+ getRepository = () => {
163
+ let repository;
164
+ try {
165
+ // There is no 'Reports' repository (bc there is no 'Reports' model),
166
+ // so just get the first OneBuild repository; doesn't matter which one
167
+
168
+ repository = oneHatData.getRepositoriesByType('onebuild', true); // true to get the first only
169
+
170
+ } catch (error) {
171
+ alert('Error getting repository: ' + (error?.message || error));
172
+ return null;
173
+ }
174
+ return repository;
175
+ },
176
+ addToQueue = async (formData) => {
177
+ const
178
+ repository = getRepository(),
179
+ data = {
180
+ report_id: reportId,
181
+ ...formData,
182
+ ...additionalData,
183
+ },
184
+ result = await repository._send('POST', 'Reports/addToQueue', data),
185
+ response = repository._processServerResponse(result);
186
+ if (!response.success) {
187
+ alert(response.message || 'Failed to add report to queue');
188
+ return;
189
+ }
190
+
191
+ showInfo('Report added to queue.');
192
+ },
193
+ selectReportPreset = (reportPresetId) => {
194
+ // Change the form settings based on the selected preset
195
+ const
196
+ form = self?.children?.form,
197
+ ReportPresets = self?.children?.reportPresetsComboEditor?.repository;
198
+ if (!form || !ReportPresets) {
199
+ return;
200
+ }
201
+
202
+ const reportPreset = ReportPresets?.getById(reportPresetId);
203
+ if (!reportPreset) {
204
+ alert('Selected report preset not found');
205
+ return;
206
+ }
207
+
208
+ // apply the config to the form
209
+ const config = reportPreset.properties.report_presets__config.getParsedValue(); // get the actual JS object
210
+ setCurrentReportFormData(config);
211
+ },
212
+ shareReportPreset = (parent) => {
213
+
214
+ // show a Modal with UserSelector, excluding current user.
215
+ showModal({
216
+ title: 'Share Report Preset',
217
+ body: <Form
218
+ instructions="Please select which user to share with."
219
+ editorType={EDITOR_TYPE__PLAIN}
220
+ className="flex-1"
221
+ items={[
222
+ {
223
+ name: 'instructions',
224
+ type: 'DisplayField',
225
+ text: 'Please select which user to share with.',
226
+ className: 'mb-3',
227
+ },
228
+ {
229
+ type: 'Column',
230
+ flex: 1,
231
+ items: [
232
+ {
233
+ name: 'user_id',
234
+ type: 'UsersCombo',
235
+ label: 'User',
236
+ baseParams: {
237
+ 'conditions[id <>]': user.id,
238
+ },
239
+ },
240
+ ],
241
+ },
242
+ ]}
243
+ validator={yup.object({
244
+ user_id: yup.number().integer().required(),
245
+ })}
246
+ onCancel={(e) => {
247
+ hideModal();
248
+ }}
249
+ onClose={(e) => {
250
+ hideModal();
251
+ }}
252
+ onSubmit={async (data, e) => {
253
+ const
254
+ ReportPresets = self?.children?.reportPresetsComboEditor?.repository,
255
+ reportPreset = parent.selection[0],
256
+ params = {
257
+ report_preset_id: reportPreset.id,
258
+ user_id: data.user_id,
259
+ },
260
+ result = await ReportPresets._send('POST', 'ReportPresets/share', params),
261
+ response = ReportPresets._processServerResponse(result);
262
+
263
+ // Close the modal
264
+ hideModal();
265
+
266
+ if (response.success) {
267
+ showInfo('Report preset shared successfully.');
268
+ }
269
+ }}
270
+ />,
271
+ canClose: true,
272
+ whichModal: 'shareReportPresetModal',
273
+ h: 220,
274
+ w: 500,
275
+ });
80
276
  };
81
277
 
82
278
  const propsIcon = props._icon || {};
@@ -177,37 +373,54 @@ function Report(props) {
177
373
  }
178
374
 
179
375
  let footerItems = [];
180
- if (usePresets) {
376
+ if (showPresets) { // if no form items, no need for ReportPresets!
181
377
  footerItems.push({
182
378
  ...testProps('reportPresetsComboEditor'),
379
+ parent: self,
380
+ reference: 'reportPresetsComboEditor',
183
381
  key: 'reportPresetsComboEditor',
184
382
  type: 'ReportPresetsComboEditor',
185
383
  tooltip: 'Report Presets',
186
- placeholder: 'Report Presets',
187
- disableEdit: true,
188
- className: 'w-[100px]',
384
+ placeholder: 'Presets',
385
+ disableEdit: true, // too complicated to edit, just allow add/delete/share
386
+ className: 'w-[130px]',
189
387
  baseParams: {
190
- reportId,
388
+ 'conditions[reportid]': reportId, // reportid is a generated field, so you can search on it, but change capitalization
389
+ },
390
+ onChangeValue: selectReportPreset,
391
+ _grid: {
392
+ canRecordBeAdded: () => isValid, // only allow creating a preset if the form is valid, since the preset will capture the current form config
393
+ onBeforeAdd: (addValues) => {
394
+ // add the current form values to ReportPresets.config when creating a new preset
395
+ return {
396
+ ...addValues,
397
+ report_presets__config: {
398
+ ...getCurrentReportFormData(),
399
+ report_id: reportId,
400
+ },
401
+ };
402
+ },
403
+ additionalToolbarButtons: [
404
+ {
405
+ ...testProps('shareBtn'),
406
+ key: 'shareBtn',
407
+ text: 'Share Report Preset',
408
+ icon: Share,
409
+ getIsButtonDisabled: (selection) => !selection?.[0]?.id,
410
+ handler: shareReportPreset,
411
+ },
412
+ ],
191
413
  },
192
414
  });
193
415
  }
194
416
  if (useScheduledReports) {
195
417
  footerItems.push({
196
- ...testProps('scheduleReportBtn'),
197
- key: 'scheduleReportBtn',
418
+ ...testProps('manageReportSchedulesBtn'),
419
+ key: 'manageReportSchedulesBtn',
198
420
  type: 'Button',
199
- tooltip: 'Schedule Report',
421
+ tooltip: 'Manage delivery schedules for this report',
200
422
  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,
423
+ onPress: manageReportSchedules,
211
424
  });
212
425
  }
213
426
  if (useQueue) {
@@ -215,17 +428,9 @@ function Report(props) {
215
428
  ...testProps('queueBtn'),
216
429
  key: 'queueBtn',
217
430
  type: 'Button',
218
- tooltip: 'Add to Queue',
431
+ tooltip: 'Immediately add to Queue',
219
432
  icon: Plus,
220
- onPress: (data) => addToQueue({
221
- reportId,
222
- data: {
223
- ...data,
224
- ...additionalData,
225
- },
226
- reportType: REPORT_TYPES__EXCEL,
227
- showReportHeaders,
228
- }),
433
+ onPress: addToQueue,
229
434
  disableOnInvalid: true,
230
435
  });
231
436
  }
@@ -234,8 +439,9 @@ function Report(props) {
234
439
  ...testProps('excelBtn'),
235
440
  key: 'excelBtn',
236
441
  type: 'Button',
237
- text: 'Download Excel',
442
+ text: excelButtonText,
238
443
  icon: Excel,
444
+ tooltip: excelButtonText !== 'Download Excel' ? 'Download Excel' : null,
239
445
  onPress: (data) => downloadReport({
240
446
  reportId,
241
447
  data: {
@@ -253,8 +459,9 @@ function Report(props) {
253
459
  ...testProps('pdfBtn'),
254
460
  key: 'pdfBtn',
255
461
  type: 'Button',
256
- text: 'Download PDF',
462
+ text: pdfButtonText,
257
463
  icon: Pdf,
464
+ tooltip: pdfButtonText !== 'Download PDF' ? 'Download PDF' : null,
258
465
  onPress: (data) => downloadReport({
259
466
  reportId,
260
467
  data: {
@@ -270,9 +477,17 @@ function Report(props) {
270
477
  if (footerItems.length) {
271
478
  footerItems = footerItems.map(item => {
272
479
  const Component = getComponentFromType(item.type);
273
- return <Component {...item} />;
480
+ const { key, ...componentProps } = item;
481
+ return <Component key={key} {...componentProps} />;
274
482
  });
275
483
  }
484
+ let additionalDataComponent = null;
485
+ if (!_.isEmpty(additionalData)) {
486
+ const ReportAdditionalData = getComponentFromType('ReportAdditionalData');
487
+ additionalDataComponent = <ReportAdditionalData
488
+ additionalData={additionalData}
489
+ />;
490
+ }
276
491
  return <VStackNative
277
492
  {...testProps('Report-' + reportId)}
278
493
  className={clsx(
@@ -298,9 +513,12 @@ function Report(props) {
298
513
  <VStack className="flex-1">
299
514
  <Text className="text-2xl max-w-full">{title}</Text>
300
515
  <Text className="text-sm">{description}</Text>
516
+ {additionalDataComponent}
301
517
  </VStack>
302
518
  </HStack>
303
519
  <Form
520
+ parent={self}
521
+ reference="form"
304
522
  editorType={EDITOR_TYPE__PLAIN}
305
523
  additionalFooterItems={footerItems}
306
524
  {...formProps}
@@ -308,6 +526,9 @@ function Report(props) {
308
526
  ...footerProps,
309
527
  className: footerClassName,
310
528
  }}
529
+ onValidityChange={(isValid) => {
530
+ setIsValid(isValid);
531
+ }}
311
532
  />
312
533
  </Box>
313
534
  {isDisabled &&
@@ -353,4 +574,13 @@ function Report(props) {
353
574
  </VStackNative>;
354
575
  }
355
576
 
356
- export default withComponent(withAlert(Report));
577
+ function withAdditionalProps(WrappedComponent) {
578
+ return (props) => {
579
+ return <WrappedComponent
580
+ reference={props.reference || 'report'}
581
+ {...props}
582
+ />;
583
+ };
584
+ }
585
+
586
+ export default withAdditionalProps(withComponent(withAlert(Report)));
@@ -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;