@onehat/ui 0.4.102 → 0.4.104

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.
Files changed (56) hide show
  1. package/.github/copilot-instructions.md.bak.20260307094051 +65 -0
  2. package/package.json +1 -1
  3. package/src/Components/Accordion/Accordion.js +65 -6
  4. package/src/Components/Container/Container.js +10 -4
  5. package/src/Components/Form/Field/Combo/Combo.js +8 -2
  6. package/src/Components/Form/Form.js +17 -9
  7. package/src/Components/Grid/Grid.js +234 -154
  8. package/src/Components/Grid/GridRow.js +5 -1
  9. package/src/Components/Hoc/Secondary/withSecondaryEditor.js +18 -1
  10. package/src/Components/Hoc/withEditor.js +18 -1
  11. package/src/Components/Hoc/withPdfButtons.js +3 -0
  12. package/src/Components/Hoc/withPresetButtons.js +20 -6
  13. package/src/Components/Icons/ArrowsLeftRight.js +10 -0
  14. package/src/Components/Icons/Bar.js +10 -0
  15. package/src/Components/Icons/Box.js +11 -0
  16. package/src/Components/Icons/BoxOpen.js +11 -0
  17. package/src/Components/Icons/Bucket.js +10 -0
  18. package/src/Components/Icons/Bump.js +21 -0
  19. package/src/Components/Icons/Calculator.js +12 -0
  20. package/src/Components/Icons/Dots.js +20 -0
  21. package/src/Components/Icons/Fleets.js +26 -0
  22. package/src/Components/Icons/Microchip.js +12 -0
  23. package/src/Components/Icons/Num1.js +10 -0
  24. package/src/Components/Icons/Num2.js +10 -0
  25. package/src/Components/Icons/Num3.js +10 -0
  26. package/src/Components/Icons/Num4.js +10 -0
  27. package/src/Components/Icons/OilCan.js +11 -0
  28. package/src/Components/Icons/Operations.js +10 -0
  29. package/src/Components/Icons/OverduePms.js +10 -0
  30. package/src/Components/Icons/SackDollar.js +11 -0
  31. package/src/Components/Icons/ShortBar.js +15 -0
  32. package/src/Components/Icons/Tower.js +10 -0
  33. package/src/Components/Icons/UpcomingPms.js +10 -0
  34. package/src/Components/Layout/ScreenHeader.js +35 -3
  35. package/src/Components/Layout/SetupButton.js +31 -0
  36. package/src/Components/Layout/UserIndicator.js +35 -0
  37. package/src/Components/Panel/Panel.js +37 -9
  38. package/src/Components/Pms/Editor/BumpPmsEditor.js +9 -0
  39. package/src/Components/Pms/Editor/MetersEditor.js +173 -0
  40. package/src/Components/Pms/Editor/PmEventsEditor.js +291 -0
  41. package/src/Components/Pms/Grid/UpcomingPmsGrid.js +569 -0
  42. package/src/Components/Pms/Layout/TreeSpecific/MakeTreeSelection.js +11 -0
  43. package/src/Components/Pms/Layout/TreeSpecific/TreeSpecific.js +30 -0
  44. package/src/Components/Pms/Modals/BulkAssignTechnician.js +104 -0
  45. package/src/Components/Pms/Screens/PmsManager.js +136 -0
  46. package/src/Components/Pms/Window/BumpPmsEditorWindow.js +25 -0
  47. package/src/Components/Screens/Manager.js +5 -1
  48. package/src/Components/Toolbar/PaginationToolbar.js +5 -3
  49. package/src/Components/Tree/Tree.js +15 -6
  50. package/src/Components/Viewer/PmCalcDebugViewer.js +164 -146
  51. package/src/Components/Viewer/TextWithLinks.js +9 -1
  52. package/src/Components/Viewer/Viewer.js +43 -33
  53. package/src/Constants/PmSchedules.js +1 -0
  54. package/src/Functions/buildAdditionalButtons.js +5 -0
  55. package/src/Functions/flatten.js +39 -0
  56. package/src/Functions/verifyCanCrudPmEvents.js +33 -0
@@ -0,0 +1,65 @@
1
+ # Copilot Instructions
2
+
3
+ ## General Principles
4
+ - Be direct, concise, and critical. Do not apologize, agree blindly, be sycophantic, or use flattery.
5
+ - If a request is inefficient or flawed, point it out and suggest a better, more secure, or more idiomatic approach.
6
+ - Use "Uncle Bob's Clean Code" principles as a baseline for code quality.
7
+ - Prioritize clarity over cleverness.
8
+ - Avoid unnecessary abstractions.
9
+ - Prefer explicit code over magic.
10
+ - Follow the project's existing coding style and conventions.
11
+ - Do not introduce new dependencies unless absolutely necessary.
12
+ - Always consider security implications and best practices.
13
+ - Aim for full test coverage of new code, and suggest tests for existing code when appropriate.
14
+
15
+ <!-- ## Coding Standards
16
+
17
+ ### Naming
18
+ - Use descriptive variable and function names.
19
+ - Do not abbreviate unless universally understood.
20
+ - Use camelCase for JavaScript variables and functions.
21
+ - Use PascalCase for React components.
22
+ - Use snake_case for database columns.
23
+
24
+ ### Comments
25
+ - Do not write obvious comments.
26
+ - Only comment non-obvious business logic.
27
+ - Prefer self-documenting code.
28
+
29
+ ## Error Handling
30
+ - Always handle errors explicitly.
31
+ - Do not swallow exceptions.
32
+ - Return meaningful error messages.
33
+
34
+ ## Security
35
+ - Never expose secrets or API keys.
36
+ - Validate and sanitize all user input.
37
+ - Use prepared statements for database queries.
38
+
39
+ ## Project-Specific Notes
40
+
41
+ ### Backend (PHP / CakePHP)
42
+ - Follow PSR-12 formatting.
43
+ - Use dependency injection where appropriate.
44
+ - Do not use deprecated framework methods.
45
+ - Prefer modern APIs over legacy helpers.
46
+
47
+ ### Frontend (React / Expo)
48
+ - Use functional components only.
49
+ - Prefer hooks over class components.
50
+ - Avoid inline styles unless necessary.
51
+ - Keep components under 200 lines.
52
+
53
+ ## Testing
54
+ - Suggest unit tests for new logic.
55
+ - Mock external dependencies.
56
+ - Keep tests deterministic.
57
+
58
+ ## Performance
59
+ - Avoid unnecessary loops.
60
+ - Do not perform database queries inside loops.
61
+ - Prefer memoization when appropriate.
62
+
63
+ ## Output Expectations
64
+ - Produce production-ready code.
65
+ - Avoid TODO comments unless necessary. -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.102",
3
+ "version": "0.4.104",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,14 +1,21 @@
1
1
  import { useState, useEffect, useRef, } from 'react';
2
2
  import {
3
+ Box,
3
4
  HStack,
4
- ScrollView,
5
+ Icon,
5
6
  Pressable,
7
+ ScrollView,
8
+ Text,
9
+ TextNative,
6
10
  VStack,
7
11
  VStackNative,
8
12
  } from '@project-components/Gluestack';
9
13
  import clsx from 'clsx';
14
+ import Plus from '../Icons/Plus.js';
15
+ import Minus from '../Icons/Minus.js';
10
16
  import inArray from '../../Functions/inArray.js';
11
17
  import emptyFn from '../../Functions/emptyFn.js';
18
+ import UiGlobals from '../../UiGlobals.js';
12
19
  import _ from 'lodash';
13
20
 
14
21
  // The Accordion has two modes.
@@ -25,11 +32,61 @@ import _ from 'lodash';
25
32
 
26
33
  export default function Accordion(props) {
27
34
  const {
35
+ styles = UiGlobals.styles,
28
36
  sections = [],
29
37
  activeSections = [],
30
38
  setActiveSections = emptyFn,
31
- renderHeader = emptyFn,
32
- renderContent = emptyFn,
39
+ renderHeader = (section, ix, isActive) => {
40
+ return <HStack
41
+ className={clsx(
42
+ 'Header',
43
+ 'bg-grey-300',
44
+ 'items-center',
45
+ 'justify-start',
46
+ 'py-1',
47
+ 'px-2',
48
+ 'border-b-grey-400',
49
+ 'border-b-1',
50
+ styles.PANEL_HEADER_BG,
51
+ )}
52
+ >
53
+ {/* <Text className="text-white flex-1">{section.header}</Text> */}
54
+
55
+ <TextNative
56
+ numberOfLines={1}
57
+ ellipsizeMode="head"
58
+ className={clsx(
59
+ 'Header-TextNative1',
60
+ 'flex-1',
61
+ 'font-bold',
62
+ styles.PANEL_HEADER_TEXT_CLASSNAME,
63
+ )}>{section.header}</TextNative>
64
+ <Icon
65
+ as={isActive ? Minus : Plus}
66
+ className={clsx(
67
+ 'text-black',
68
+ )}
69
+ />
70
+ </HStack>;
71
+ },
72
+ unmountInactiveContent = true,
73
+ renderContent = (section, ix, isActive, ref) => {
74
+ if (unmountInactiveContent) {
75
+ if (!isActive) {
76
+ return null;
77
+ }
78
+ return section.content;
79
+ }
80
+
81
+ // This keeps all content rendered, just hidden (zero height) if it's inActive
82
+ let className = 'w-full overflow-hidden';
83
+ if (!isActive) {
84
+ className += ' h-[0px]';
85
+ }
86
+ return <Box className={className}>
87
+ {section.content}
88
+ </Box>;
89
+ },
33
90
  onAnimationEnd = emptyFn,
34
91
  onLayout,
35
92
  onlyOne = true,
@@ -67,7 +124,9 @@ export default function Accordion(props) {
67
124
 
68
125
  // TODO: Animate height. Possible help here: https://stackoverflow.com/a/57333550 and https://stackoverflow.com/a/64797961
69
126
  if (isActive) {
70
- rowProps.flex = 1;
127
+ if (onlyOne) {
128
+ rowProps.flex = 1;
129
+ }
71
130
  } else {
72
131
  rowProps.h = 0;
73
132
  rowProps.overflow = 'hidden'; // otherwise some elements mistakenly show
@@ -94,7 +153,7 @@ export default function Accordion(props) {
94
153
  >
95
154
  {header}
96
155
  </Pressable>
97
- <HStack {...rowProps} className="bg-[#f00]">
156
+ <HStack {...rowProps}>
98
157
  {content}
99
158
  </HStack>
100
159
  </VStack>;
@@ -129,7 +188,7 @@ export default function Accordion(props) {
129
188
  keyboardShouldPersistTaps="always"
130
189
  className="Accordion-ScrollView flex-1 w-full"
131
190
  contentContainerStyle={{
132
- height: '100%',
191
+ height: onlyOne ? '100%' : undefined,
133
192
  }}
134
193
  >
135
194
  <VStackNative
@@ -125,16 +125,20 @@ function Container(props) {
125
125
  localSouthIsCollapsedRef = useRef(southInitialIsCollapsed),
126
126
  localEastIsCollapsedRef = useRef(eastInitialIsCollapsed),
127
127
  localWestIsCollapsedRef = useRef(westInitialIsCollapsed),
128
+ isSplitterDraggingRef = useRef(false),
128
129
  onLayout = async (e) => {
129
- console.log('Container onLayout', e.nativeEvent.layout.width);
130
+ if (isSplitterDraggingRef.current) {
131
+ return;
132
+ }
130
133
  if (id) {
131
134
  // save prevScreenSize if changed
132
135
  const
133
- height = parseFloat(e.nativeEvent.layout.height),
134
- width = parseFloat(e.nativeEvent.layout.width),
136
+ height = windowSize?.height ?? parseFloat(e.nativeEvent.layout.height),
137
+ width = windowSize?.width ?? parseFloat(e.nativeEvent.layout.width),
135
138
  key = id + '-prevScreenSize',
136
139
  prevScreenSize = await getSaved(key);
137
- if (!prevScreenSize || prevScreenSize.width !== width || prevScreenSize.height !== height) {
140
+ const hasChanged = !prevScreenSize || Math.abs((prevScreenSize.width ?? 0) - width) > 1 || Math.abs((prevScreenSize.height ?? 0) - height) > 1;
141
+ if (hasChanged) {
138
142
  await setSaved(key, {
139
143
  height,
140
144
  width,
@@ -300,10 +304,12 @@ function Container(props) {
300
304
  }
301
305
  },
302
306
  onSplitterDragStart = () => {
307
+ isSplitterDraggingRef.current = true;
303
308
  setIsComponentsDisabled(true);
304
309
  },
305
310
  onSplitterDragStop = (delta, which) => {
306
311
  setIsComponentsDisabled(false);
312
+ isSplitterDraggingRef.current = false;
307
313
  switch(which) {
308
314
  case 'north':
309
315
  onNorthResize(delta);
@@ -90,6 +90,7 @@ export const ComboComponent = forwardRef((props, ref) => {
90
90
  isInTag = false,
91
91
  minimizeForRow = false,
92
92
  reloadOnTrigger = false,
93
+ loadAfterRender = false,
93
94
  searchHasInitialPercent = false,
94
95
  menuHeight,
95
96
  placeholder,
@@ -585,11 +586,16 @@ export const ComboComponent = forwardRef((props, ref) => {
585
586
  };
586
587
 
587
588
  useEffect(() => {
588
- // on render, focus the input
589
589
  if (!isRendered) {
590
590
  return () => {};
591
591
  }
592
+
593
+ if (loadAfterRender) {
594
+ Repository?.reload();
595
+ }
596
+
592
597
  if (autoFocus && !inputRef.current.isFocused()) {
598
+ // on render, focus the input
593
599
  inputRef.current.focus();
594
600
  }
595
601
 
@@ -599,7 +605,7 @@ export const ComboComponent = forwardRef((props, ref) => {
599
605
  }
600
606
  };
601
607
 
602
- }, [isRendered]);
608
+ }, [isRendered, loadAfterRender, Repository]);
603
609
 
604
610
  useEffect(() => {
605
611
  (async () => {
@@ -253,8 +253,9 @@ function Form(props) {
253
253
  'border-r-grey-200',
254
254
  'px-1',
255
255
  styles.INLINE_EDITOR_MIN_WIDTH,
256
- );
257
- _.each(columnsConfig, (config, ix) => {
256
+ ),
257
+ validColumnsConfig = _.filter(columnsConfig, (config) => !!config); // filter out any null/undefined configs
258
+ _.each(validColumnsConfig, (config, ix) => {
258
259
  let {
259
260
  fieldName,
260
261
  isEditable = false,
@@ -273,7 +274,7 @@ function Form(props) {
273
274
  type,
274
275
  editorTypeProps = {},
275
276
  viewerTypeProps = {};
276
-
277
+
277
278
  if (isHidden) {
278
279
  return;
279
280
  }
@@ -965,9 +966,11 @@ function Form(props) {
965
966
  />;
966
967
  },
967
968
  buildAncillary = () => {
968
- const components = [];
969
+ const
970
+ validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
971
+ components = [];
969
972
  setAncillaryButtons([]);
970
- if (ancillaryItems.length) {
973
+ if (validAncillaryItems.length) {
971
974
 
972
975
  // add the "scroll to top" button
973
976
  getAncillaryButtons().push({
@@ -978,7 +981,7 @@ function Form(props) {
978
981
  tooltip: 'Scroll to top',
979
982
  });
980
983
 
981
- _.each(ancillaryItems, (item, ix) => {
984
+ _.each(validAncillaryItems, (item, ix) => {
982
985
  let {
983
986
  type,
984
987
  title = null,
@@ -1213,6 +1216,10 @@ function Form(props) {
1213
1216
  style.maxHeight = maxHeight;
1214
1217
  }
1215
1218
 
1219
+ const
1220
+ // allow horizontal layouts only if there are top-level columns, and screen is wide enough
1221
+ hasTopLevelColumns = _.some(items, (item) => item?.type === 'Column'),
1222
+ shouldUseHorizontalFormLayout = !isItemsCustomLayout && hasTopLevelColumns && containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD;
1216
1223
  let modeHeader = null,
1217
1224
  formButtons = null,
1218
1225
  scrollButtons = null,
@@ -1246,8 +1253,8 @@ function Form(props) {
1246
1253
  formComponents = buildFromItems();
1247
1254
  const formAncillaryComponents = buildAncillary();
1248
1255
  editor = <>
1249
- {containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD && !isItemsCustomLayout ? <HStack className="Form-formComponents-HStack p-4 gap-4 justify-center">{formComponents}</HStack> : null}
1250
- {containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD || isItemsCustomLayout ? <VStack className="Form-formComponents-VStack p-4">{formComponents}</VStack> : null}
1256
+ {shouldUseHorizontalFormLayout ? <HStack className="Form-formComponents-HStack w-full min-w-0 p-4 gap-4 justify-start items-stretch">{formComponents}</HStack> : null}
1257
+ {!shouldUseHorizontalFormLayout ? <VStack className="Form-formComponents-VStack p-4">{formComponents}</VStack> : null}
1251
1258
  {formAncillaryComponents.length ? <VStack className="Form-AncillaryComponents m-2 pt-4 px-2">{formAncillaryComponents}</VStack> : null}
1252
1259
  </>;
1253
1260
 
@@ -1532,7 +1539,8 @@ function Form(props) {
1532
1539
  onScroll={onScroll}
1533
1540
  scrollEventThrottle={16 /* ms */}
1534
1541
  contentContainerStyle={{
1535
- // height: '100%',
1542
+ width: '100%',
1543
+ minWidth: '100%',
1536
1544
  }}
1537
1545
  >
1538
1546
  {scrollToTopAnchor}