@onehat/ui 0.3.36 → 0.3.37

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.3.36",
3
+ "version": "0.3.37",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState, useRef, isValidElement, } from 'react';
2
+ import { View, } from 'react-native';
2
3
  import {
3
4
  Box,
4
5
  Column,
@@ -28,6 +29,7 @@ import withEditor from '../Hoc/withEditor.js';
28
29
  import withPdfButton from '../Hoc/withPdfButton.js';
29
30
  import inArray from '../../Functions/inArray.js';
30
31
  import getComponentFromType from '../../Functions/getComponentFromType.js';
32
+ import buildAdditionalButtons from '../../Functions/buildAdditionalButtons.js';
31
33
  import Button from '../Buttons/Button.js';
32
34
  import IconButton from '../Buttons/IconButton.js';
33
35
  import AngleLeft from '../Icons/AngleLeft.js';
@@ -38,6 +40,8 @@ import Footer from '../Layout/Footer.js';
38
40
  import Label from '../Form/Label.js';
39
41
  import _ from 'lodash';
40
42
 
43
+ const CONTAINER_THRESHOLD = 900;
44
+
41
45
  // TODO: memoize field Components
42
46
 
43
47
  // Modes:
@@ -73,6 +77,7 @@ function Form(props) {
73
77
  submitBtnLabel,
74
78
  onSubmit,
75
79
  additionalEditButtons,
80
+ additionalFooterButtons,
76
81
 
77
82
  // sizing of outer container
78
83
  h,
@@ -113,6 +118,7 @@ function Form(props) {
113
118
  isSingle = !isMultiple, // for convenience
114
119
  forceUpdate = useForceUpdate(),
115
120
  [previousRecord, setPreviousRecord] = useState(record),
121
+ [containerWidth, setContainerWidth] = useState(),
116
122
  initialValues = _.merge(startingValues, (record && !record.isDestroyed ? record.submitValues : {})),
117
123
  defaultValues = isMultiple ? getNullFieldValues(initialValues, Repository) : initialValues, // when multiple entities, set all default values to null
118
124
  {
@@ -267,9 +273,9 @@ function Form(props) {
267
273
  return <Row>{elements}</Row>;
268
274
  },
269
275
  buildFromItems = () => {
270
- return _.map(items, (item, ix) => buildNextLayer(item, ix, columnDefaults));
276
+ return _.map(items, (item, ix) => buildFromItem(item, ix, columnDefaults));
271
277
  },
272
- buildNextLayer = (item, ix, defaults) => {
278
+ buildFromItem = (item, ix, defaults) => {
273
279
  let {
274
280
  type,
275
281
  title,
@@ -321,9 +327,26 @@ function Form(props) {
321
327
  if (_.isEmpty(items)) {
322
328
  return null;
323
329
  }
330
+ if (type === 'Column') {
331
+ if (containerWidth < CONTAINER_THRESHOLD) {
332
+ // everything is in one column
333
+ if (propsToPass.hasOwnProperty('flex')) {
334
+ delete propsToPass.flex;
335
+ }
336
+ if (propsToPass.hasOwnProperty('width')) {
337
+ delete propsToPass.width;
338
+ }
339
+ if (propsToPass.hasOwnProperty('w')) {
340
+ delete propsToPass.w;
341
+ }
342
+ propsToPass.w = '100%';
343
+ propsToPass.mb = 1;
344
+ }
345
+ propsToPass.pl = 3;
346
+ }
324
347
  const itemDefaults = item.defaults;
325
348
  children = _.map(items, (item, ix) => {
326
- return buildNextLayer(item, ix, itemDefaults);
349
+ return buildFromItem(item, ix, itemDefaults);
327
350
  });
328
351
  return <Element key={ix} title={title} {...itemDefaults} {...propsToPass} {...editorTypeProps}>{children}</Element>;
329
352
  }
@@ -432,6 +455,14 @@ function Form(props) {
432
455
 
433
456
  }
434
457
  }
458
+
459
+ if (item.additionalEditButtons) {
460
+ element = <Row flex={1} flexWrap="wrap">
461
+ {element}
462
+ {buildAdditionalButtons(item.additionalEditButtons, self, { fieldState, formSetValue, formGetValues, formState })}
463
+ </Row>;
464
+ }
465
+
435
466
  if (label && editorType !== EDITOR_TYPE__INLINE) {
436
467
  const labelProps = {};
437
468
  if (defaults?.labelWidth) {
@@ -485,43 +516,6 @@ function Form(props) {
485
516
  }
486
517
  return components;
487
518
  },
488
- buildAdditionalButtons = (configs) => {
489
- const additionalButtons = [];
490
- _.each(additionalEditButtons, (config) => {
491
- const {
492
- key,
493
- text,
494
- handler,
495
- icon,
496
- isDisabled,
497
- color = '#fff',
498
- } = config,
499
- buttonProps = {};
500
- if (key) {
501
- buttonProps.key = key;
502
- buttonProps.reference = key;
503
- }
504
- if (handler) {
505
- buttonProps.onPress = handler;
506
- }
507
- if (icon) {
508
- buttonProps.leftIcon = <Icon as={icon} color="#fff" size="sm" />;
509
- }
510
- if (isDisabled) {
511
- buttonProps.isDisabled = isDisabled;
512
- }
513
-
514
- const button = <Button
515
- color={color}
516
- ml={2}
517
- parent={self}
518
- reference={key}
519
- {...buttonProps}
520
- >{text}</Button>;
521
- additionalButtons.push(button);
522
- });
523
- return additionalButtons;
524
- },
525
519
  onSubmitError = (errors, e) => {
526
520
  debugger;
527
521
  if (editorType === EDITOR_TYPE__INLINE) {
@@ -542,6 +536,13 @@ function Form(props) {
542
536
  const values = record.submitValues;
543
537
  reset(values);
544
538
  }
539
+ },
540
+ onLayoutDecorated = (e) => {
541
+ if (onLayout) {
542
+ onLayout(e);
543
+ }
544
+
545
+ setContainerWidth(e.nativeEvent.layout.width);
545
546
  };
546
547
 
547
548
  useEffect(() => {
@@ -596,148 +597,166 @@ function Form(props) {
596
597
  sizeProps.maxHeight = maxHeight;
597
598
  }
598
599
 
599
- const savingProps = {};
600
- if (isSaving) {
601
- savingProps.borderTopWidth = 2;
602
- savingProps.borderTopColor = '#f00';
603
- }
600
+ let formComponents,
601
+ editor,
602
+ additionalButtons,
603
+ isSaveDisabled = false,
604
+ isSubmitDisabled = false,
605
+ savingProps = {};
604
606
 
607
+ if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
608
+
609
+ if (isSaving) {
610
+ savingProps.borderTopWidth = 2;
611
+ savingProps.borderTopColor = '#f00';
612
+ }
605
613
 
606
- let formComponents,
607
- editor;
608
- if (editorType === EDITOR_TYPE__INLINE) {
609
- formComponents = buildFromColumnsConfig();
610
- editor = <ScrollView
611
- horizontal={true}
612
- flex={1}
613
- bg="#fff"
614
- py={1}
615
- borderTopWidth={3}
616
- borderBottomWidth={5}
617
- borderTopColor="primary.100"
618
- borderBottomColor="primary.100"
619
- >{formComponents}</ScrollView>;
620
- // } else if (editorType === EDITOR_TYPE__PLAIN) {
621
- // formComponents = buildFromItems();
622
- // const formAncillaryComponents = buildAncillary();
623
- // editor = <>
624
- // <Column p={4}>{formComponents}</Column>
625
- // <Column pt={4}>{formAncillaryComponents}</Column>
626
- // </>;
627
- } else {
628
- formComponents = buildFromItems();
629
- const formAncillaryComponents = buildAncillary();
630
- editor = <ScrollView _web={{ minHeight, }} width="100%" pb={1}>
631
- <Column p={4}>{formComponents}</Column>
632
- <Column m={2} pt={4}>{formAncillaryComponents}</Column>
633
- </ScrollView>;
634
- }
614
+ if (editorType === EDITOR_TYPE__INLINE) {
615
+ formComponents = buildFromColumnsConfig();
616
+ editor = <ScrollView
617
+ horizontal={true}
618
+ flex={1}
619
+ bg="#fff"
620
+ py={1}
621
+ borderTopWidth={3}
622
+ borderBottomWidth={5}
623
+ borderTopColor="primary.100"
624
+ borderBottomColor="primary.100"
625
+ >{formComponents}</ScrollView>;
626
+ // } else if (editorType === EDITOR_TYPE__PLAIN) {
627
+ // formComponents = buildFromItems();
628
+ // const formAncillaryComponents = buildAncillary();
629
+ // editor = <>
630
+ // <Column p={4}>{formComponents}</Column>
631
+ // <Column pt={4}>{formAncillaryComponents}</Column>
632
+ // </>;
633
+ } else {
634
+ formComponents = buildFromItems();
635
+ const formAncillaryComponents = buildAncillary();
636
+ editor = <ScrollView _web={{ minHeight, }} width="100%" pb={1}>
637
+ {containerWidth >= CONTAINER_THRESHOLD ? <Row p={4} pl={0}>{formComponents}</Row> : null}
638
+ {containerWidth < CONTAINER_THRESHOLD ? <Column p={4}>{formComponents}</Column> : null}
639
+ <Column m={2} pt={4}>{formAncillaryComponents}</Column>
640
+ </ScrollView>;
641
+ }
635
642
 
636
- let editorModeF;
637
- switch(editorMode) {
638
- case EDITOR_MODE__VIEW:
639
- editorModeF = 'View';
640
- break;
641
- case EDITOR_MODE__ADD:
642
- editorModeF = 'Add';
643
- break;
644
- case EDITOR_MODE__EDIT:
645
- editorModeF = isMultiple ? 'Edit Multiple' : 'Edit';
646
- break;
647
- }
643
+ let editorModeF;
644
+ switch(editorMode) {
645
+ case EDITOR_MODE__VIEW:
646
+ editorModeF = 'View';
647
+ break;
648
+ case EDITOR_MODE__ADD:
649
+ editorModeF = 'Add';
650
+ break;
651
+ case EDITOR_MODE__EDIT:
652
+ editorModeF = isMultiple ? 'Edit Multiple' : 'Edit';
653
+ break;
654
+ }
648
655
 
649
- let isSaveDisabled = false,
650
- isSubmitDisabled = false;
651
- if (!_.isEmpty(formState.errors)) {
652
- isSaveDisabled = true;
653
- isSubmitDisabled = true;
654
- }
655
- if (_.isEmpty(formState.dirtyFields) && !record?.isRemotePhantom) {
656
- isSaveDisabled = true;
657
- }
656
+ if (!_.isEmpty(formState.errors)) {
657
+ isSaveDisabled = true;
658
+ isSubmitDisabled = true;
659
+ }
660
+ if (_.isEmpty(formState.dirtyFields) && !record?.isRemotePhantom) {
661
+ isSaveDisabled = true;
662
+ }
658
663
 
659
- if (editorType === EDITOR_TYPE__INLINE) {
660
- buttonGroupProps.position = 'fixed';
661
- buttonGroupProps.left = 10; // TODO: I would prefer to have this be centered, but it's a lot more complex than just making it stick to the left
662
- footerProps.alignItems = 'flex-start';
663
- }
664
+ if (editorType === EDITOR_TYPE__INLINE) {
665
+ buttonGroupProps.position = 'fixed';
666
+ buttonGroupProps.left = 10; // TODO: I would prefer to have this be centered, but it's a lot more complex than just making it stick to the left
667
+ footerProps.alignItems = 'flex-start';
668
+ }
664
669
 
665
- const additionalButtons = buildAdditionalButtons(additionalEditButtons);
670
+ additionalButtons = buildAdditionalButtons(additionalEditButtons);
671
+ }
666
672
 
667
- return <Column {...sizeProps} onLayout={onLayout} ref={formRef}>
668
-
669
- <Row px={4} pt={4} alignItems="center" justifyContent="flex-end">
670
- {isSingle && editorMode === EDITOR_MODE__EDIT && onBack &&
671
- <Button
672
- key="backBtn"
673
- onPress={onBack}
674
- leftIcon={<Icon as={AngleLeft} color="#fff" size="sm" />}
675
- color="#fff"
676
- >Back</Button>}
677
- {isSingle && editorMode === EDITOR_MODE__EDIT && onViewMode &&
678
- <Button
679
- key="viewBtn"
680
- onPress={onViewMode}
681
- leftIcon={<Icon as={Eye} color="#fff" size="sm" />}
682
- color="#fff"
683
- >To View</Button>}
684
- </Row>
685
- {editorMode === EDITOR_MODE__EDIT && !_.isEmpty(additionalButtons) &&
686
- <Row p={4} alignItems="center" justifyContent="flex-end">
687
- {additionalButtons}
688
- </Row>}
689
-
690
- {editor}
691
-
692
- <Footer justifyContent="flex-end" {...footerProps} {...savingProps}>
693
- {onDelete && editorMode === EDITOR_MODE__EDIT && isSingle &&
694
- <Row flex={1} justifyContent="flex-start">
673
+ return <Column {...sizeProps} onLayout={onLayoutDecorated} ref={formRef}>
674
+ {containerWidth && <>
675
+ <Row px={4} pt={4} alignItems="center" justifyContent="flex-end">
676
+ {isSingle && editorMode === EDITOR_MODE__EDIT && onBack &&
695
677
  <Button
696
- key="deleteBtn"
697
- onPress={onDelete}
698
- bg="warning"
699
- _hover={{
700
- bg: 'warningHover',
701
- }}
678
+ key="backBtn"
679
+ onPress={onBack}
680
+ leftIcon={<Icon as={AngleLeft} color="#fff" size="sm" />}
702
681
  color="#fff"
703
- >Delete</Button>
682
+ >Back</Button>}
683
+ {isSingle && editorMode === EDITOR_MODE__EDIT && onViewMode &&
684
+ <Button
685
+ key="viewBtn"
686
+ onPress={onViewMode}
687
+ leftIcon={<Icon as={Eye} color="#fff" size="sm" />}
688
+ color="#fff"
689
+ >To View</Button>}
690
+ </Row>
691
+ {editorMode === EDITOR_MODE__EDIT && !_.isEmpty(additionalButtons) &&
692
+ <Row p={4} alignItems="center" justifyContent="flex-end" flexWrap="wrap">
693
+ {additionalButtons}
704
694
  </Row>}
705
-
706
- {!isEditorViewOnly && <IconButton
707
- key="resetBtn"
708
- onPress={() => {
709
- if (onReset) {
710
- onReset();
711
- }
712
- reset();
713
- }}
714
- icon={<Rotate color="#fff" />}
715
- />}
716
- {!isEditorViewOnly && isSingle && onCancel && <Button
717
- key="cancelBtn"
718
- variant="ghost"
719
- onPress={onCancel}
720
- color="#fff"
721
- >Cancel</Button>}
722
- {!isEditorViewOnly && onSave && <Button
723
- key="saveBtn"
724
- onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
725
- isDisabled={isSaveDisabled}
726
- color="#fff"
727
- >{editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save'}</Button>}
728
- {onSubmit && <Button
729
- key="submitBtn"
730
- onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
731
- isDisabled={isSubmitDisabled}
695
+
696
+ {editor}
697
+
698
+ <Footer justifyContent="flex-end" {...footerProps} {...savingProps}>
699
+ {onDelete && editorMode === EDITOR_MODE__EDIT && isSingle &&
700
+ <Row flex={1} justifyContent="flex-start">
701
+ <Button
702
+ key="deleteBtn"
703
+ onPress={onDelete}
704
+ bg="warning"
705
+ _hover={{
706
+ bg: 'warningHover',
707
+ }}
732
708
  color="#fff"
733
- >{submitBtnLabel || 'Submit'}</Button>}
734
-
735
- {isEditorViewOnly && onClose && editorType !== EDITOR_TYPE__SIDE && <Button
736
- key="closeBtn"
737
- onPress={onClose}
738
- color="#fff"
739
- >Close</Button>}
740
- </Footer>
709
+ >Delete</Button>
710
+ </Row>}
711
+
712
+ {!isEditorViewOnly &&
713
+ <IconButton
714
+ key="resetBtn"
715
+ onPress={() => {
716
+ if (onReset) {
717
+ onReset();
718
+ }
719
+ reset();
720
+ }}
721
+ icon={<Rotate color="#fff" />}
722
+ />}
723
+ {!isEditorViewOnly && isSingle && onCancel &&
724
+ <Button
725
+ key="cancelBtn"
726
+ variant="ghost"
727
+ onPress={onCancel}
728
+ color="#fff"
729
+ >Cancel</Button>}
730
+ {!isEditorViewOnly && onSave &&
731
+ <Button
732
+ key="saveBtn"
733
+ onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
734
+ isDisabled={isSaveDisabled}
735
+ color="#fff"
736
+ >{editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save'}</Button>}
737
+ {onSubmit &&
738
+ <Button
739
+ key="submitBtn"
740
+ onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
741
+ isDisabled={isSubmitDisabled}
742
+ color="#fff"
743
+ >{submitBtnLabel || 'Submit'}</Button>}
744
+
745
+ {isEditorViewOnly && onClose && editorType !== EDITOR_TYPE__SIDE &&
746
+ <Button
747
+ key="closeBtn"
748
+ onPress={onClose}
749
+ color="#fff"
750
+ >Close</Button>}
751
+
752
+ {additionalFooterButtons && _.map(additionalFooterButtons, (props) => {
753
+ return <Button
754
+ {...props}
755
+ onPress={(e) => handleSubmit(props.onPress, onSubmitError)(e)}
756
+ >{props.text}</Button>;
757
+ })}
758
+ </Footer>
759
+ </>}
741
760
  </Column>;
742
761
  }
743
762
 
@@ -73,6 +73,7 @@ function GridComponent(props) {
73
73
  },
74
74
  flatListProps = {},
75
75
  // enableEditors = false,
76
+ loadOnRender = true,
76
77
  pullToRefresh = true,
77
78
  hideNavColumn = true,
78
79
  noneFoundText,
@@ -147,7 +148,9 @@ function GridComponent(props) {
147
148
  forceUpdate = useForceUpdate(),
148
149
  containerRef = useRef(),
149
150
  gridRef = useRef(),
151
+ gridContainerRef = useRef(),
150
152
  isAddingRef = useRef(),
153
+ [isRendered, setIsRendered] = useState(false),
151
154
  [isReady, setIsReady] = useState(false),
152
155
  [isLoading, setIsLoading] = useState(false),
153
156
  [localColumnsConfig, setLocalColumnsConfigRaw] = useState([]),
@@ -620,30 +623,46 @@ function GridComponent(props) {
620
623
  setDragRowSlot(null);
621
624
  },
622
625
  onLayout = (e) => {
623
- if (disableAdjustingPageSizeToHeight || !Repository || CURRENT_MODE !== UI_MODE_WEB || !gridRef.current || isAddingRef.current) {
624
- return;
625
- }
626
-
627
- const
628
- gr = gridRef.current,
629
- scrollableNode = gr.getScrollableNode(),
630
- scrollableNodeBoundingBox = scrollableNode.getBoundingClientRect(),
631
- scrollableNodeHeight = scrollableNodeBoundingBox.height,
632
- firstRow = scrollableNode.children[0].children[showHeaders ? 1: 0];
633
626
 
634
- if (!firstRow) {
635
- return;
627
+ let doLoad = false;
628
+ if (!isRendered) {
629
+ setIsRendered(true);
630
+ if (loadOnRender && Repository && !Repository.isLoaded && !Repository.isLoading && !Repository.isAutoLoad) {
631
+ doLoad = true; // first time in onLayout only!
632
+ }
636
633
  }
637
634
 
638
- const
639
- rowHeight = firstRow.getBoundingClientRect().height,
640
- rowsPerContainer = Math.floor(scrollableNodeHeight / rowHeight);
641
- let pageSize = rowsPerContainer;
642
- if (showHeaders) {
643
- pageSize--;
635
+ const adjustPageSizeToHeight = !!(disableAdjustingPageSizeToHeight || !Repository || CURRENT_MODE !== UI_MODE_WEB || !gridRef.current || isAddingRef.current);
636
+ if (adjustPageSizeToHeight) {
637
+ // this currently only works on web
638
+ const
639
+ gr = gridContainerRef.current,
640
+ // scrollableNode = gr.getScrollableNode(),
641
+ // scrollableNodeBoundingBox = scrollableNode.getBoundingClientRect(),
642
+ // scrollableNodeHeight = scrollableNodeBoundingBox.height,
643
+ // firstRow = scrollableNode.children[0].children[showHeaders ? 1: 0];
644
+ height = gr.getBoundingClientRect().height;
645
+ // IDEALLY, we want the grid to load right away with appropriate limits.
646
+ // Currently, it's been loading once, then doing layout then loading again with correct limit.
647
+ // How do we get the right limit before it renders??
648
+ // Estimate based on (container height -header -footer) / avg height? This won't work for rows that exceed the avg height.
649
+ // Maybe we do that avg at first, and if it exceeds, then we do another query to lose the later ones, which are hidden anyway.
650
+ // It'll only do that once. Better to hide the offscreen ones, than to show gap at first, and later fill it
651
+ // if (firstRow) { // TODO: this assumes there is a row there already, which is wrong!
652
+ // const
653
+ // rowHeight = firstRow.getBoundingClientRect().height,
654
+ // rowsPerContainer = Math.floor(scrollableNodeHeight / rowHeight);
655
+ // let pageSize = rowsPerContainer;
656
+ // if (showHeaders) {
657
+ // pageSize--;
658
+ // }
659
+ // if (pageSize !== Repository.pageSize) {
660
+ // Repository.setPageSize(pageSize);
661
+ // }
662
+ // }
644
663
  }
645
- if (pageSize !== Repository.pageSize) {
646
- Repository.setPageSize(pageSize);
664
+ if (doLoad) {
665
+ Repository.load();
647
666
  }
648
667
  },
649
668
  debouncedOnLayout = useCallback(_.debounce(onLayout, 500), []);
@@ -827,7 +846,7 @@ function GridComponent(props) {
827
846
  >
828
847
  {topToolbar}
829
848
 
830
- <Column w="100%" flex={1} minHeight={40} borderTopWidth={isLoading ? 2 : 1} borderTopColor={isLoading ? '#f00' : 'trueGray.300'} onClick={() => {
849
+ <Column ref={gridContainerRef} w="100%" flex={1} minHeight={40} borderTopWidth={isLoading ? 2 : 1} borderTopColor={isLoading ? '#f00' : 'trueGray.300'} onClick={() => {
831
850
  if (!isDragMode && !isInlineEditorShown) {
832
851
  deselectAll();
833
852
  }
@@ -138,7 +138,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
138
138
  setEditorMode(EDITOR_MODE__EDIT);
139
139
  setIsEditorShown(true);
140
140
  },
141
- onDelete = async (cb) => {
141
+ onDelete = async (args) => {
142
+ let cb = null;
143
+ if (_.isFunction(args)) {
144
+ cb = args;
145
+ }
142
146
  if (_.isEmpty(selection) || (_.isArray(selection) && (selection.length > 1 || selection[0]?.isDestroyed))) {
143
147
  return;
144
148
  }
@@ -16,6 +16,10 @@ export default function withSideEditor(WrappedComponent, isTree = false) {
16
16
  // withComponent
17
17
  self,
18
18
 
19
+ // pull these out, as we don't want them going to the Editor
20
+ selectorId,
21
+ selectorSelected,
22
+
19
23
  ...propsToPass
20
24
  } = props;
21
25
 
@@ -36,6 +36,10 @@ export default function withWindowedEditor(WrappedComponent, isTree = false) {
36
36
  // withComponent
37
37
  self,
38
38
 
39
+ // pull these out, as we don't want them going to the Editor
40
+ selectorId,
41
+ selectorSelected,
42
+
39
43
  ...propsToPass
40
44
  } = props;
41
45
 
@@ -0,0 +1,14 @@
1
+ // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
2
+ import * as React from "react"
3
+ import Svg, { Path } from "react-native-svg"
4
+ import { Icon } from 'native-base';
5
+
6
+ function SvgComponent(props) {
7
+ return (
8
+ <Icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" {...props}>
9
+ <Path d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM155.7 250.2l36.3 51.9 36.3-51.9c7.6-10.9 22.6-13.5 33.4-5.9s13.5 22.6 5.9 33.4L221.3 344l46.4 66.2c7.6 10.9 5 25.8-5.9 33.4s-25.8 5-33.4-5.9L192 385.8l-36.3 51.9c-7.6 10.9-22.6 13.5-33.4 5.9s-13.5-22.6-5.9-33.4l46.3-66.2-46.4-66.2c-7.6-10.9-5-25.8 5.9-33.4s25.8-5 33.4 5.9z" />
10
+ </Icon>
11
+ )
12
+ }
13
+
14
+ export default SvgComponent
@@ -0,0 +1,124 @@
1
+ import React from 'react';
2
+ import {
3
+ Column,
4
+ Icon,
5
+ Row,
6
+ Text,
7
+ } from 'native-base';
8
+ import { EDITOR_TYPE__PLAIN } from '../../Constants/Editor';
9
+ import {
10
+ UI_MODE_WEB,
11
+ CURRENT_MODE,
12
+ } from '../../Constants/UiModes.js';
13
+ import Form from '../Form/Form.js';
14
+ import withComponent from '../Hoc/withComponent.js';
15
+ import ChartLine from '../Icons/ChartLine.js';
16
+ import Pdf from '../Icons/Pdf.js';
17
+ import Excel from '../Icons/Excel.js';
18
+ import UiGlobals from '../../UiGlobals.js';
19
+ import _ from 'lodash';
20
+
21
+ const
22
+ PDF = 'PDF',
23
+ EXCEL = 'PhpOffice';
24
+
25
+ function Report(props) {
26
+ if (CURRENT_MODE !== UI_MODE_WEB) {
27
+ return <Text>Reports are web only!</Text>;
28
+ }
29
+ const {
30
+ title,
31
+ description,
32
+ reportId,
33
+ // icon,
34
+ disablePdf = false,
35
+ disableExcel = false,
36
+ includePresets = false,
37
+ showReportHeaders = true,
38
+ h = '300px',
39
+ } = props,
40
+ styles = UiGlobals.styles,
41
+ url = UiGlobals.baseURL + 'Reports/getReport',
42
+ buttons = [],
43
+ downloadWithFetch = (data) => {
44
+ const options = {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ body: JSON.stringify(data),
50
+ };
51
+ fetch(url, options)
52
+ .then( res => res.blob() )
53
+ .then( blob => {
54
+ const
55
+ winName = 'ReportWindow',
56
+ opts = 'resizable=yes,height=600,width=800,location=0,menubar=0,scrollbars=1',
57
+ externalWindow = window.open('', winName, opts),
58
+ file = externalWindow.URL.createObjectURL(blob);
59
+ externalWindow.location.assign(file);
60
+ });
61
+ },
62
+ getReport = (reportType, data) => {
63
+ const params = {
64
+ report_id: reportId,
65
+ outputFileType: reportType,
66
+ showReportHeaders,
67
+ // download_token, // not sure this is needed
68
+ ...data,
69
+ },
70
+ closeWindow = reportType === EXCEL;
71
+
72
+ downloadWithFetch(params, closeWindow);
73
+ };
74
+
75
+ const propsIcon = props._icon || {};
76
+ propsIcon.size = 60;
77
+ propsIcon.px = 5;
78
+ let icon = props.icon;
79
+ if (_.isEmpty(icon)) {
80
+ icon = ChartLine;
81
+ }
82
+ if (React.isValidElement(icon)) {
83
+ if (!_.isEmpty(propsIcon)) {
84
+ icon = React.cloneElement(icon, {...propsIcon});
85
+ }
86
+ } else {
87
+ icon = <Icon as={icon} {...propsIcon} />;
88
+ }
89
+
90
+ if (!disableExcel) {
91
+ buttons.push({
92
+ key: 'ExcelBtn',
93
+ text: 'Download Excel',
94
+ leftIcon: <Icon as={Excel} size="md" color="#fff" />,
95
+ onPress: (data) => getReport(EXCEL, data),
96
+ ml: 1,
97
+ });
98
+ }
99
+ if (!disablePdf) {
100
+ buttons.push({
101
+ key: 'pdfBtn',
102
+ text: 'Download PDF',
103
+ leftIcon: <Icon as={Pdf} size="md" color="#fff" />,
104
+ onPress: (data) => getReport(PDF, data),
105
+ ml: 1,
106
+ });
107
+ }
108
+ return <Column w="100%" borderWidth={1} borderColor="primary.300" pt={4} mb={3}>
109
+ <Row>
110
+ {icon && <Column>{icon}</Column>}
111
+ <Column>
112
+ <Text fontSize="2xl">{title}</Text>
113
+ <Text fontSize="sm">{description}</Text>
114
+ </Column>
115
+ </Row>
116
+ <Form
117
+ type={EDITOR_TYPE__PLAIN}
118
+ additionalFooterButtons={buttons}
119
+ {...props._form}
120
+ />
121
+ </Column>;
122
+ }
123
+
124
+ export default withComponent(Report);
@@ -1,4 +1,4 @@
1
- import { useRef, } from 'react';
1
+ import { useRef, useState, } from 'react';
2
2
  import {
3
3
  Column,
4
4
  Icon,
@@ -14,12 +14,15 @@ import withComponent from '../Hoc/withComponent.js';
14
14
  import withPdfButton from '../Hoc/withPdfButton.js';
15
15
  import inArray from '../../Functions/inArray.js';
16
16
  import getComponentFromType from '../../Functions/getComponentFromType.js';
17
+ import buildAdditionalButtons from '../../Functions/buildAdditionalButtons.js';
17
18
  import Button from '../Buttons/Button.js';
18
19
  import Label from '../Form/Label.js';
19
20
  import Pencil from '../Icons/Pencil.js';
20
21
  import Footer from '../Layout/Footer.js';
21
22
  import _ from 'lodash';
22
23
 
24
+ const CONTAINER_THRESHOLD = 900;
25
+
23
26
  function Viewer(props) {
24
27
  const {
25
28
  viewerCanDelete = false,
@@ -48,13 +51,14 @@ function Viewer(props) {
48
51
  } = props,
49
52
  scrollViewRef = useRef(),
50
53
  isMultiple = _.isArray(record),
54
+ [containerWidth, setContainerWidth] = useState(),
51
55
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
52
56
  styles = UiGlobals.styles,
53
57
  flex = props.flex || 1,
54
58
  buildFromItems = () => {
55
- return _.map(items, (item, ix) => buildNextLayer(item, ix, columnDefaults));
59
+ return _.map(items, (item, ix) => buildFromItem(item, ix, columnDefaults));
56
60
  },
57
- buildNextLayer = (item, ix, defaults) => {
61
+ buildFromItem = (item, ix, defaults) => {
58
62
  let {
59
63
  type,
60
64
  title,
@@ -90,9 +94,26 @@ function Viewer(props) {
90
94
  if (_.isEmpty(items)) {
91
95
  return null;
92
96
  }
97
+ if (type === 'Column') {
98
+ if (containerWidth < CONTAINER_THRESHOLD) {
99
+ // everything is in one column
100
+ if (propsToPass.hasOwnProperty('flex')) {
101
+ delete propsToPass.flex;
102
+ }
103
+ if (propsToPass.hasOwnProperty('width')) {
104
+ delete propsToPass.width;
105
+ }
106
+ if (propsToPass.hasOwnProperty('w')) {
107
+ delete propsToPass.w;
108
+ }
109
+ propsToPass.w = '100%';
110
+ propsToPass.mb = 1;
111
+ }
112
+ propsToPass.pl = 3;
113
+ }
93
114
  const defaults = item.defaults;
94
115
  children = _.map(items, (item, ix) => {
95
- return buildNextLayer(item, ix, defaults);
116
+ return buildFromItem(item, ix, defaults);
96
117
  });
97
118
  return <Element key={ix} title={title} {...defaults} {...propsToPass} {...editorTypeProps}>{children}</Element>;
98
119
  }
@@ -121,6 +142,14 @@ function Viewer(props) {
121
142
  {...propsToPass}
122
143
  {...editorTypeProps}
123
144
  />;
145
+
146
+ if (item.additionalViewButtons) {
147
+ element = <Row flexWrap="wrap">
148
+ {element}
149
+ {buildAdditionalButtons(item.additionalViewButtons, self)}
150
+ </Row>;
151
+ }
152
+
124
153
  if (label) {
125
154
  const labelProps = {};
126
155
  if (defaults?.labelWidth) {
@@ -169,42 +198,8 @@ function Viewer(props) {
169
198
  }
170
199
  return components;
171
200
  },
172
- buildAdditionalButtons = (configs) => {
173
- const additionalButtons = [];
174
- _.each(additionalViewButtons, (config) => {
175
- const {
176
- key,
177
- text,
178
- handler,
179
- icon,
180
- isDisabled,
181
- color = '#fff',
182
- } = config,
183
- buttonProps = {};
184
- if (key) {
185
- buttonProps.key = key;
186
- buttonProps.reference = key;
187
- }
188
- if (handler) {
189
- buttonProps.onPress = handler;
190
- }
191
- if (icon) {
192
- buttonProps.leftIcon = <Icon as={icon} color="#fff" size="sm" />;
193
- }
194
- if (isDisabled) {
195
- buttonProps.isDisabled = isDisabled;
196
- }
197
-
198
- const button = <Button
199
- color={color}
200
- ml={2}
201
- parent={self}
202
- reference={key}
203
- {...buttonProps}
204
- >{text}</Button>;
205
- additionalButtons.push(button);
206
- });
207
- return additionalButtons;
201
+ onLayout = (e) => {
202
+ setContainerWidth(e.nativeEvent.layout.width);
208
203
  };
209
204
 
210
205
  if (self) {
@@ -213,52 +208,62 @@ function Viewer(props) {
213
208
 
214
209
  const
215
210
  showDeleteBtn = onDelete && viewerCanDelete,
216
- showCloseBtn = !isSideEditor,
217
- additionalButtons = buildAdditionalButtons();
218
-
219
- return <Column flex={flex} {...props}>
220
- <ScrollView width="100%" _web={{ height: 1 }} ref={scrollViewRef}>
221
- <Column p={4}>
222
- {onEditMode && <Row mb={4} justifyContent="flex-end">
223
- <Button
224
- key="editBtn"
225
- onPress={onEditMode}
226
- leftIcon={<Icon as={Pencil} color="#fff" size="sm" />}
227
- color="#fff"
228
- >To Edit</Button>
229
- </Row>}
230
-
231
- {!_.isEmpty(additionalButtons) &&
232
- <Row p={2} alignItems="center" justifyContent="flex-end">
233
- {additionalButtons}
234
- </Row>}
211
+ showCloseBtn = !isSideEditor;
212
+ let additionalButtons = null,
213
+ viewerComponents = null,
214
+ ancillaryComponents = null;
215
+
216
+
217
+ if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
218
+ additionalButtons = buildAdditionalButtons(additionalViewButtons);
219
+ viewerComponents = buildFromItems();
220
+ ancillaryComponents = buildAncillary();
221
+ }
235
222
 
236
- {buildFromItems()}
223
+ return <Column flex={flex} {...props} onLayout={onLayout}>
224
+ {containerWidth && <>
225
+ {onEditMode && <Row px={4} pt={4} alignItems="center" justifyContent="flex-end">
226
+ <Button
227
+ key="editBtn"
228
+ onPress={onEditMode}
229
+ leftIcon={<Icon as={Pencil} color="#fff" size="sm" />}
230
+ color="#fff"
231
+ >To Edit</Button>
232
+ </Row>}
233
+ {!_.isEmpty(additionalButtons) &&
234
+ <Row p={4} alignItems="center" justifyContent="flex-end" flexWrap="wrap">
235
+ {additionalButtons}
236
+ </Row>}
237
237
 
238
- {buildAncillary()}
238
+ <ScrollView _web={{ height: 1 }} width="100%" pb={1} ref={scrollViewRef}>
239
+ <Column>
240
+ {containerWidth >= CONTAINER_THRESHOLD ? <Row p={4} pl={0}>{viewerComponents}</Row> : null}
241
+ {containerWidth < CONTAINER_THRESHOLD ? <Column p={4}>{viewerComponents}</Column> : null}
242
+ <Column m={2} pt={4}>{ancillaryComponents}</Column>
243
+ </Column>
244
+ </ScrollView>
245
+ {(showDeleteBtn || showCloseBtn) &&
246
+ <Footer justifyContent="flex-end">
247
+ {showDeleteBtn &&
248
+ <Row flex={1} justifyContent="flex-start">
249
+ <Button
250
+ key="deleteBtn"
251
+ onPress={onDelete}
252
+ bg="warning"
253
+ _hover={{
254
+ bg: 'warningHover',
255
+ }}
256
+ color="#fff"
257
+ >Delete</Button>
258
+ </Row>}
259
+ {showCloseBtn && <Button
260
+ key="closeBtn"
261
+ onPress={onClose}
262
+ color="#fff"
263
+ >Close</Button>}
264
+ </Footer>}
239
265
 
240
- </Column>
241
- </ScrollView>
242
- {(showDeleteBtn || showCloseBtn) &&
243
- <Footer justifyContent="flex-end">
244
- {showDeleteBtn &&
245
- <Row flex={1} justifyContent="flex-start">
246
- <Button
247
- key="deleteBtn"
248
- onPress={onDelete}
249
- bg="warning"
250
- _hover={{
251
- bg: 'warningHover',
252
- }}
253
- color="#fff"
254
- >Delete</Button>
255
- </Row>}
256
- {showCloseBtn && <Button
257
- key="closeBtn"
258
- onPress={onClose}
259
- color="#fff"
260
- >Close</Button>}
261
- </Footer>}
266
+ </>}
262
267
  </Column>;
263
268
  }
264
269
 
@@ -0,0 +1,43 @@
1
+ import {
2
+ Icon,
3
+ } from 'native-base';
4
+ import Button from '../Components/Buttons/Button.js';
5
+ import _ from 'lodash';
6
+
7
+ export default function buildAdditionalButtons(configs, self, handlerArgs = {}) {
8
+ const additionalButtons = [];
9
+ _.each(configs, (config) => {
10
+ const {
11
+ key,
12
+ text,
13
+ handler,
14
+ icon,
15
+ isDisabled,
16
+ color = '#fff',
17
+ } = config,
18
+ buttonProps = {
19
+ key,
20
+ reference: key,
21
+ };
22
+ if (handler) {
23
+ buttonProps.onPress = () => handler(handlerArgs);
24
+ }
25
+ if (icon) {
26
+ buttonProps.leftIcon = <Icon as={icon} color="#fff" size="sm" />;
27
+ }
28
+ if (isDisabled) {
29
+ buttonProps.isDisabled = isDisabled;
30
+ }
31
+
32
+ const button = <Button
33
+ color={color}
34
+ ml={2}
35
+ mb={2}
36
+ parent={self}
37
+ reference={key}
38
+ {...buttonProps}
39
+ >{text}</Button>;
40
+ additionalButtons.push(button);
41
+ });
42
+ return additionalButtons;
43
+ }