@onehat/ui 0.4.103 → 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.
@@ -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.103",
3
+ "version": "0.4.104",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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,
@@ -589,6 +589,8 @@ function GridComponent(props) {
589
589
  'flex-row',
590
590
  'grow',
591
591
  'max-h-[80px]',
592
+ 'focus:outline-none', // hide the focus outline
593
+ 'focus-visible:outline-none',
592
594
  )}
593
595
  >
594
596
  {({
@@ -1914,6 +1916,8 @@ function GridComponent(props) {
1914
1916
  'w-full',
1915
1917
  'border',
1916
1918
  'border-grey-300',
1919
+ 'focus:outline-none', // hide the focus outline
1920
+ 'focus-visible:outline-none',
1917
1921
  );
1918
1922
  if (props.className) {
1919
1923
  className += ' ' + props.className;
@@ -490,7 +490,11 @@ const GridRow = forwardRef((props, ref) => {
490
490
  }}
491
491
  >{rowContents}</HStackNative>;
492
492
  if (rowProps.tooltip) {
493
- row = <Tooltip label={rowProps.tooltip} placement="bottom left">{row}</Tooltip>;
493
+ row = <Tooltip
494
+ label={rowProps.tooltip}
495
+ placement="bottom left"
496
+ triggerClassName={rowClassName}
497
+ >{row}</Tooltip>;
494
498
  }
495
499
  return row;
496
500
  }, [
@@ -47,6 +47,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
47
47
  },
48
48
  secondaryEditorType,
49
49
  secondaryOnAdd,
50
+ secondaryOnBeforeAdd,
50
51
  secondaryOnChange, // any kind of crud change
51
52
  secondaryOnBeforeDelete,
52
53
  secondaryOnDelete,
@@ -201,10 +202,26 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
201
202
  }
202
203
 
203
204
  if (getListeners().onBeforeAdd) {
204
- const listenerResult = await getListeners().onBeforeAdd();
205
+ // This listener is set by child components using setWithEditListeners()
206
+ const listenerResult = await getListeners().onBeforeAdd(addValues);
207
+ if (listenerResult === false) {
208
+ return;
209
+ }
210
+ if (listenerResult) {
211
+ // allow the listener to override the addValues by returning an object
212
+ addValues = listenerResult;
213
+ }
214
+ }
215
+ if (secondaryOnBeforeAdd) {
216
+ // This listener is set by parent components using a prop
217
+ const listenerResult = await secondaryOnBeforeAdd(addValues);
205
218
  if (listenerResult === false) {
206
219
  return;
207
220
  }
221
+ if (listenerResult) {
222
+ // allow the listener to override the addValues by returning an object
223
+ addValues = listenerResult;
224
+ }
208
225
  }
209
226
 
210
227
  if (isTree) {
@@ -47,6 +47,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
47
47
  },
48
48
  editorType,
49
49
  onAdd,
50
+ onBeforeAdd,
50
51
  onChange, // any kind of crud change
51
52
  onBeforeDelete,
52
53
  onDelete,
@@ -218,10 +219,26 @@ export default function withEditor(WrappedComponent, isTree = false) {
218
219
  }
219
220
 
220
221
  if (getListeners().onBeforeAdd) {
221
- const listenerResult = await getListeners().onBeforeAdd();
222
+ // This listener is set by child components using setWithEditListeners()
223
+ const listenerResult = await getListeners().onBeforeAdd(addValues);
224
+ if (listenerResult === false) {
225
+ return;
226
+ }
227
+ if (listenerResult) {
228
+ // allow the listener to override the addValues by returning an object
229
+ addValues = listenerResult;
230
+ }
231
+ }
232
+ if (onBeforeAdd) {
233
+ // This listener is set by parent components using a prop
234
+ const listenerResult = await onBeforeAdd(addValues);
222
235
  if (listenerResult === false) {
223
236
  return;
224
237
  }
238
+ if (listenerResult) {
239
+ // allow the listener to override the addValues by returning an object
240
+ addValues = listenerResult;
241
+ }
225
242
  }
226
243
 
227
244
  if (isTree) {
@@ -418,6 +418,9 @@ export default function withPdfButtons(WrappedComponent) {
418
418
  },
419
419
  ];
420
420
  _.each(buttons, (button) => {
421
+ if (!button) {
422
+ return; // guard against null/undefined
423
+ }
421
424
  if (!_.find(additionalEditButtons, btn => button.key === btn.key)) {
422
425
  additionalEditButtons.push(button);
423
426
  }
@@ -68,6 +68,7 @@ export default function withPresetButtons(WrappedComponent) {
68
68
  canDeleteRootNode = false,
69
69
  isSideEditor = false,
70
70
  canEditorViewOnly = false,
71
+ canRecordBeAdded, // fn(selection) returns bool on if the current record(s) can be added
71
72
  canRecordBeEdited, // fn(selection) returns bool on if the current record(s) can be edited
72
73
  canRecordBeDeleted, // fn(selection) returns bool on if the current record(s) can be deleted
73
74
  canRecordBeDuplicated, // fn(selection) returns bool on if the current record(s) can be duplicated
@@ -239,6 +240,7 @@ export default function withPresetButtons(WrappedComponent) {
239
240
  };
240
241
  icon = Plus;
241
242
  if (isNoSelectorSelected() ||
243
+ (canRecordBeAdded && !canRecordBeAdded(selection)) ||
242
244
  (isTree && isEmptySelection())
243
245
  ) {
244
246
  isDisabled = true;
@@ -50,6 +50,7 @@ function Panel(props) {
50
50
  disableTitleChange = false,
51
51
 
52
52
  // Content
53
+ renderWhileCollapsed = false,
53
54
  topToolbar = null,
54
55
  children = null,
55
56
  bottomToolbar = null,
@@ -139,14 +140,7 @@ function Panel(props) {
139
140
  className += ' ' + filteredClassName;
140
141
  }
141
142
 
142
- return <VStackNative
143
- {...testProps(self?.reference)}
144
- className={className}
145
- style={style}
146
- >
147
- {isDisabled && <Mask />}
148
- {headerComponent}
149
- {!isCollapsed && <>
143
+ let content = <>
150
144
  {topToolbar}
151
145
  <VStack
152
146
  className={clsx(
@@ -169,7 +163,41 @@ function Panel(props) {
169
163
  </VStack>
170
164
  {bottomToolbar}
171
165
  {footer}
172
- </>}
166
+ </>;
167
+ if (renderWhileCollapsed) {
168
+ content = <Box
169
+ className={clsx(
170
+ 'Panel-Box',
171
+ ...(isCollapsed ?
172
+ [
173
+ 'w-[0px]',
174
+ 'h-[0px]',
175
+ 'overflow-hidden',
176
+ ] :
177
+ [
178
+ 'w-full',
179
+ 'h-full',
180
+ ])
181
+ )}
182
+ >
183
+ {content}
184
+ </Box>;
185
+ } else {
186
+ if (isCollapsed) {
187
+ // hide the content when collapsed
188
+ content = null;
189
+ }
190
+ }
191
+
192
+
193
+ return <VStackNative
194
+ {...testProps(self?.reference)}
195
+ className={className}
196
+ style={style}
197
+ >
198
+ {isDisabled && <Mask />}
199
+ {headerComponent}
200
+ {content}
173
201
  </VStackNative>;
174
202
 
175
203
  }
@@ -92,18 +92,18 @@ export default function PmEventsEditor(props) {
92
92
  case PM_EVENT_TYPES__INITIAL:
93
93
  case PM_EVENT_TYPES__WORK_ORDER:
94
94
  case PM_EVENT_TYPES__ALERT:
95
+ case PM_EVENT_TYPES__COMPLETE:
95
96
  setIsIntervalHidden(true);
96
97
  setIsDateHidden(true);
97
98
  setIsMeterReadingHidden(false);
98
99
  setIsDetailsHidden(false);
99
100
  setIsPmTechnicianHidden(true);
100
101
  break;
101
- case PM_EVENT_TYPES__COMPLETE:
102
102
  case PM_EVENT_TYPES__RESET:
103
103
  setIsIntervalHidden(true);
104
104
  setIsDateHidden(true);
105
- setIsMeterReadingHidden(false);
106
- setIsDetailsHidden(false);
105
+ setIsMeterReadingHidden(true);
106
+ setIsDetailsHidden(true);
107
107
  setIsPmTechnicianHidden(true);
108
108
  break;
109
109
  case PM_EVENT_TYPES__DELAY_BY_DAYS:
@@ -17,7 +17,7 @@ import ClockRegular from '../../Icons/ClockRegular.js';
17
17
  import OilCan from '../../Icons/OilCan.js';
18
18
  import TabBar from '../../Tab/TabBar.js';
19
19
  import TreeSpecific from '../Layout/TreeSpecific/TreeSpecific.js';
20
- import UpcomingPmsGrid from '../Grid/UpcomingPmsGrid.js';
20
+ import UpcomingPmsGrid from '@src/Components/Grid/UpcomingPmsGrid.js';
21
21
  import PmEventsFilteredGridEditor from '@src/Components/Grid/PmEventsFilteredGridEditor.js';
22
22
  import PmEventsFilteredSideGridEditor from '@src/Components/Grid/PmEventsFilteredSideGridEditor.js';
23
23
  import _ from 'lodash';
@@ -31,6 +31,7 @@ function ManagerScreen(props) {
31
31
  [isModeSet, setIsModeSet] = useState(false),
32
32
  [allowSideBySide, setAllowSideBySide] = useState(false),
33
33
  [mode, setModeRaw] = useState(SCREEN_MODES__SIDE),
34
+ isDisabled = propsToPass._panel?.isDisabled ?? false, // this is kind of a hack, since there's no Panel, but it works
34
35
  actualMode = (!allowSideBySide || mode === SCREEN_MODES__FULL) ? SCREEN_MODES__FULL : SCREEN_MODES__SIDE,
35
36
  setMode = (newMode) => {
36
37
  if (!allowSideBySide && newMode === SCREEN_MODES__SIDE) {
@@ -97,7 +98,7 @@ function ManagerScreen(props) {
97
98
  onFullWidth={() => setMode(SCREEN_MODES__FULL)}
98
99
  onSideBySide={() => setMode(SCREEN_MODES__SIDE)}
99
100
  />
100
- {isRendered && isModeSet && whichComponent}
101
+ {isRendered && isModeSet && !isDisabled && whichComponent}
101
102
  </VStackNative>;
102
103
  }
103
104
 
@@ -11,15 +11,17 @@ const PaginationToolbar = forwardRef((props, ref) => {
11
11
  const {
12
12
  toolbarItems = [],
13
13
  disablePageSize = false,
14
+ disableMinimizing = false,
14
15
  minimize,
15
16
  } = props,
16
17
  [minimizeLocal, setMinimizeLocal] = useState(minimize),
17
18
  propsToPass = _.omit(props, 'toolbarItems'),
18
19
  showPagination = true,//props.Repository?.totalPages > 1,
19
20
  onLayout = (e) => {
20
- if (minimize) {
21
- return; // skip if already minimized
21
+ if (minimize || disableMinimizing) {
22
+ return;
22
23
  }
24
+
23
25
  // Note to future self: this is using hard-coded values.
24
26
  // Eventually might want to make it responsive to actual sizes
25
27
 
@@ -32,7 +34,7 @@ const PaginationToolbar = forwardRef((props, ref) => {
32
34
  threshold = pagingToolbarMinwidth + toolbarItemsMinwidth,
33
35
  shouldMinimize = width < threshold;
34
36
 
35
- if (shouldMinimize !== minimize) {
37
+ if (shouldMinimize !== minimizeLocal) {
36
38
  setMinimizeLocal(shouldMinimize);
37
39
  }
38
40
  };
@@ -350,9 +350,11 @@ function Viewer(props) {
350
350
  return <HStack key={ix} className="Viewer-HStack4 px-2 pb-1">{element}</HStack>;
351
351
  },
352
352
  buildAncillary = () => {
353
- const components = [];
353
+ const
354
+ validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
355
+ components = [];
354
356
  setAncillaryButtons([]);
355
- if (ancillaryItems.length) {
357
+ if (validAncillaryItems.length) {
356
358
 
357
359
  // add the "scroll to top" button
358
360
  getAncillaryButtons().push({
@@ -363,7 +365,7 @@ function Viewer(props) {
363
365
  tooltip: 'Scroll to top',
364
366
  });
365
367
 
366
- _.each(ancillaryItems, (item, ix) => {
368
+ _.each(validAncillaryItems, (item, ix) => {
367
369
  let {
368
370
  type,
369
371
  title = null,
@@ -0,0 +1 @@
1
+ export const PM_SCHEDULES__ONE_OFF = 200;
@@ -5,6 +5,11 @@ import _ from 'lodash';
5
5
  export default function buildAdditionalButtons(configs, self, handlerArgs = {}) {
6
6
  const additionalButtons = [];
7
7
  _.each(configs, (config) => {
8
+
9
+ if (!config) {
10
+ return;
11
+ }
12
+
8
13
  const {
9
14
  key,
10
15
  color = '#fff',