@xh/hoist 59.2.0 → 59.3.0

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 (104) hide show
  1. package/CHANGELOG.md +63 -14
  2. package/admin/AppComponent.ts +1 -1
  3. package/admin/tabs/activity/ActivityTab.ts +1 -1
  4. package/admin/tabs/general/GeneralTab.ts +2 -2
  5. package/admin/tabs/general/config/ConfigPanel.ts +1 -0
  6. package/admin/tabs/monitor/MonitorTab.ts +1 -1
  7. package/admin/tabs/server/ServerTab.ts +1 -1
  8. package/admin/tabs/userData/UserDataTab.ts +1 -1
  9. package/cmp/ag-grid/AgGrid.scss +51 -25
  10. package/cmp/ag-grid/AgGrid.ts +8 -2
  11. package/cmp/badge/Badge.ts +18 -5
  12. package/cmp/chart/Chart.ts +13 -11
  13. package/cmp/clock/Clock.ts +6 -5
  14. package/cmp/dataview/DataView.ts +5 -3
  15. package/cmp/form/Form.ts +25 -6
  16. package/cmp/grid/Grid.ts +41 -25
  17. package/cmp/grid/GridModel.ts +2 -2
  18. package/cmp/grid/Types.ts +1 -1
  19. package/cmp/grid/columns/Column.ts +45 -2
  20. package/cmp/grid/impl/GridHScrollbar.ts +140 -0
  21. package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
  22. package/cmp/input/HoistInputModel.ts +4 -4
  23. package/cmp/input/HoistInputProps.ts +3 -1
  24. package/cmp/layout/Box.ts +4 -2
  25. package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
  26. package/cmp/store/StoreFilterField.ts +2 -2
  27. package/cmp/tab/TabContainer.ts +1 -1
  28. package/cmp/zoneGrid/Types.ts +47 -0
  29. package/cmp/zoneGrid/ZoneGrid.ts +62 -0
  30. package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
  31. package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
  32. package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
  33. package/cmp/zoneGrid/index.ts +3 -0
  34. package/core/HoistComponent.ts +23 -10
  35. package/core/HoistProps.ts +25 -6
  36. package/core/XH.ts +49 -27
  37. package/core/elem.ts +11 -3
  38. package/core/impl/InstanceManager.ts +24 -1
  39. package/core/model/HoistModel.ts +4 -4
  40. package/data/RecordAction.ts +7 -4
  41. package/data/StoreRecord.ts +8 -1
  42. package/desktop/appcontainer/AppContainer.ts +2 -0
  43. package/desktop/cmp/appbar/AppBar.ts +8 -6
  44. package/desktop/cmp/button/Button.ts +14 -3
  45. package/desktop/cmp/button/ButtonGroup.ts +14 -3
  46. package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
  47. package/desktop/cmp/button/index.ts +1 -0
  48. package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
  49. package/desktop/cmp/dash/container/DashContainer.ts +11 -4
  50. package/desktop/cmp/error/ErrorMessage.ts +9 -8
  51. package/desktop/cmp/form/FormField.ts +34 -10
  52. package/desktop/cmp/grid/columns/Actions.ts +2 -1
  53. package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
  54. package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
  55. package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
  56. package/desktop/cmp/input/Checkbox.ts +3 -3
  57. package/desktop/cmp/input/CodeInput.ts +2 -1
  58. package/desktop/cmp/input/DateInput.ts +128 -123
  59. package/desktop/cmp/input/JsonInput.ts +1 -1
  60. package/desktop/cmp/input/NumberInput.ts +3 -2
  61. package/desktop/cmp/input/RadioInput.ts +3 -1
  62. package/desktop/cmp/input/Select.ts +31 -4
  63. package/desktop/cmp/input/SwitchInput.ts +2 -1
  64. package/desktop/cmp/input/TextArea.ts +3 -3
  65. package/desktop/cmp/input/TextInput.ts +51 -47
  66. package/desktop/cmp/panel/Panel.ts +21 -19
  67. package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
  68. package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
  69. package/desktop/cmp/record/RecordActionBar.ts +12 -3
  70. package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
  71. package/desktop/cmp/rest/Actions.ts +10 -5
  72. package/desktop/cmp/rest/RestGrid.ts +20 -6
  73. package/desktop/cmp/rest/impl/RestForm.ts +5 -4
  74. package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
  75. package/desktop/cmp/tab/TabSwitcher.ts +8 -3
  76. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  77. package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
  78. package/desktop/cmp/toolbar/Toolbar.ts +3 -1
  79. package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
  80. package/desktop/cmp/treemap/TreeMap.ts +5 -3
  81. package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
  82. package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
  83. package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
  84. package/dynamics/desktop.ts +2 -0
  85. package/dynamics/mobile.ts +2 -0
  86. package/inspector/instances/InstancesModel.ts +2 -2
  87. package/mobile/appcontainer/AppContainer.ts +2 -0
  88. package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
  89. package/mobile/cmp/button/index.ts +1 -0
  90. package/mobile/cmp/error/ErrorMessage.ts +4 -4
  91. package/mobile/cmp/input/Select.scss +1 -0
  92. package/mobile/cmp/input/Select.ts +7 -0
  93. package/mobile/cmp/input/TextInput.ts +1 -0
  94. package/mobile/cmp/panel/DialogPanel.scss +18 -6
  95. package/mobile/cmp/panel/DialogPanel.ts +3 -1
  96. package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
  97. package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
  98. package/package.json +4 -3
  99. package/styles/vars.scss +3 -3
  100. package/svc/InspectorService.ts +1 -1
  101. package/utils/js/DomUtils.ts +10 -0
  102. package/utils/js/LangUtils.ts +10 -0
  103. package/utils/js/TestUtils.ts +9 -0
  104. package/utils/js/index.ts +1 -0
@@ -4,23 +4,23 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {inRange, isNil} from 'lodash';
8
+ import moment from 'moment';
7
9
  import {box, span} from '@xh/hoist/cmp/layout';
8
10
  import {
11
+ BoxProps,
9
12
  hoistCmp,
10
13
  HoistModel,
14
+ HoistProps,
11
15
  managed,
12
16
  useLocalModel,
13
- XH,
14
- BoxProps,
15
- HoistProps
17
+ XH
16
18
  } from '@xh/hoist/core';
17
19
  import {fmtCompactDate, fmtDateTime} from '@xh/hoist/format';
18
- import {action, observable, makeObservable, computed} from '@xh/hoist/mobx';
20
+ import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
19
21
  import {Timer} from '@xh/hoist/utils/async';
20
- import {SECONDS} from '@xh/hoist/utils/datetime';
22
+ import {DAYS, HOURS, LocalDate, SECONDS} from '@xh/hoist/utils/datetime';
21
23
  import {withDefault} from '@xh/hoist/utils/js';
22
- import {getLayoutProps} from '@xh/hoist/utils/react';
23
- import moment from 'moment';
24
24
 
25
25
  interface RelativeTimestampProps extends HoistProps, BoxProps {
26
26
  /**
@@ -63,11 +63,27 @@ export interface RelativeTimestampOptions {
63
63
 
64
64
  /** Time to which the input timestamp is compared. */
65
65
  relativeTo?: Date | number;
66
+
67
+ /**
68
+ * Governs if calendar days should be used for computing return label rather than the native
69
+ * moment 24-hour day.
70
+ *
71
+ * - Set to 'always' to ensure that any output less than 25 days will refer to the difference
72
+ * in *calendar* days.
73
+ * - Set to 'useTimeForSameDay' for a similar behavior, but showing time differences if the
74
+ * two dates are on the same calendar day.
75
+ * - Set to 'useTimeFor24Hr' for a similar behavior, but showing time differences for
76
+ * any differences less than 24 hours.
77
+ *
78
+ * Null (default) will indicate that the standard behavior, based on moment, be used, whereby
79
+ * 'day' refers simply to a 24-hour time period and has no relation to the calendar.
80
+ */
81
+ localDateMode?: 'always' | 'useTimeForSameDay' | 'useTimeFor24Hr';
66
82
  }
67
83
 
68
84
  /**
69
85
  * A component to display the approximate amount of time between a given timestamp and now in a
70
- * friendly, human readable format (e.g. '6 minutes ago' or 'two hours from now').
86
+ * friendly, human-readable format (e.g. '6 minutes ago' or 'two hours from now').
71
87
  *
72
88
  * Automatically updates on a regular interval to stay current.
73
89
  */
@@ -75,13 +91,13 @@ export const [RelativeTimestamp, relativeTimestamp] = hoistCmp.withFactory<Relat
75
91
  displayName: 'RelativeTimestamp',
76
92
  className: 'xh-relative-timestamp',
77
93
 
78
- render({className, ...props}, ref) {
94
+ render({className, bind, timestamp, options, ...rest}, ref) {
79
95
  const impl = useLocalModel(RelativeTimestampLocalModel);
80
96
 
81
97
  return box({
82
98
  className,
83
- ...getLayoutProps(props),
84
99
  ref,
100
+ ...rest,
85
101
  item: span({
86
102
  className: 'xh-title-tip',
87
103
  item: impl.display,
@@ -94,7 +110,8 @@ export const [RelativeTimestamp, relativeTimestamp] = hoistCmp.withFactory<Relat
94
110
  class RelativeTimestampLocalModel extends HoistModel {
95
111
  override xhImpl = true;
96
112
 
97
- @observable display = '';
113
+ @observable display: string = '';
114
+
98
115
  model: HoistModel;
99
116
 
100
117
  @managed
@@ -103,14 +120,14 @@ class RelativeTimestampLocalModel extends HoistModel {
103
120
  interval: 5 * SECONDS
104
121
  });
105
122
 
106
- get timestamp() {
123
+ get timestamp(): Date | number {
107
124
  const {model} = this,
108
125
  {timestamp, bind} = this.componentProps;
109
126
  return withDefault(timestamp, model && bind ? model[bind] : null);
110
127
  }
111
128
 
112
129
  @computed.struct
113
- get options() {
130
+ get options(): RelativeTimestampOptions {
114
131
  return this.componentProps.options;
115
132
  }
116
133
 
@@ -136,65 +153,114 @@ class RelativeTimestampLocalModel extends HoistModel {
136
153
 
137
154
  /**
138
155
  * Returns a string describing the approximate amount of time between a given timestamp and the
139
- * present moment in a friendly, human readable format.
156
+ * present moment in a friendly, human-readable format.
140
157
  */
141
158
  export function getRelativeTimestamp(
142
159
  timestamp: Date | number,
143
160
  options: RelativeTimestampOptions = {}
144
- ) {
145
- const relTo = options.relativeTo,
146
- relFmt = relTo ? (fmtCompactDate(relTo, {asHtml: true}) as string) : null,
147
- relFmtIsTime = relFmt?.includes(':');
161
+ ): string {
162
+ const {localDateMode} = options,
163
+ relFmt = !isNil(options.relativeTo)
164
+ ? (fmtCompactDate(options.relativeTo, {asHtml: true}) as string)
165
+ : null;
148
166
 
149
167
  options = {
150
168
  allowFuture: false,
151
169
  short: XH.isMobileApp,
152
- futureSuffix: relTo ? `after ${relFmt}` : 'from now',
153
- pastSuffix: relTo ? `before ${relFmt}` : 'ago',
154
- equalString: relTo ? `${relFmtIsTime ? 'at' : 'on'} ${relFmt}` : 'just now',
170
+ pastSuffix: defaultPastSuffix(relFmt),
171
+ futureSuffix: defaultFutureSuffix(relFmt, localDateMode),
172
+ equalString: defaultEqualString(relFmt, localDateMode),
155
173
  epsilon: 10,
156
174
  emptyResult: '',
157
175
  prefix: '',
158
- relativeTo: Date.now(),
159
176
  ...options
160
177
  };
161
178
 
162
179
  if (!timestamp) return options.emptyResult;
163
180
 
164
- return doFormat(timestamp, options);
181
+ let ret = doFormat(timestamp, options);
182
+
183
+ if (options.prefix) ret = options.prefix + ' ' + ret;
184
+
185
+ return ret;
165
186
  }
166
187
 
167
188
  //------------------------
168
189
  // Implementation
169
190
  //------------------------
170
- function doFormat(timestamp: Date | number, opts: RelativeTimestampOptions) {
171
- const {prefix, equalString, epsilon, allowFuture, short} = opts,
172
- diff = toTimestamp(opts.relativeTo) - toTimestamp(timestamp),
173
- elapsed = Math.abs(diff),
174
- isEqual = elapsed <= (epsilon ?? 0) * SECONDS,
191
+ function defaultPastSuffix(relFmt: String): string {
192
+ return relFmt ? `before ${relFmt}` : 'ago';
193
+ }
194
+
195
+ function defaultFutureSuffix(relFmt: String, localDateMode: string): string {
196
+ if (relFmt) return `after ${relFmt}`;
197
+ return localDateMode == 'always' ? 'from today' : 'from now';
198
+ }
199
+
200
+ function defaultEqualString(relFmt: String, localDateMode: string): string {
201
+ if (relFmt) return `${relFmt?.includes(':') ? 'at' : 'on'} ${relFmt}`;
202
+ return localDateMode == 'always' ? 'today' : 'just now';
203
+ }
204
+
205
+ function doFormat(timestamp: Date | number, opts: RelativeTimestampOptions): string {
206
+ let {relativeTo, localDateMode, epsilon, equalString, allowFuture, short} = opts,
207
+ baseTimestamp = withDefault(relativeTo ?? Date.now()),
208
+ diff = toTimestamp(baseTimestamp) - toTimestamp(timestamp),
209
+ elapsed = Math.abs(diff);
210
+
211
+ // 0) Snap elapsed to calendar elapsed for localDateMode.
212
+ // (+ Early out for tomorrow/yesterday)
213
+ if (localDateMode && inRange(elapsed, 0, 26 * DAYS)) {
214
+ const dayDiff = LocalDate.from(baseTimestamp).diff(LocalDate.from(timestamp));
215
+
216
+ if (
217
+ !(localDateMode == 'useTimeForSameDay' && dayDiff == 0) &&
218
+ !(localDateMode == 'useTimeFor24Hr' && elapsed <= 24 * HOURS)
219
+ ) {
220
+ elapsed = Math.abs(dayDiff * DAYS);
221
+
222
+ if (isNil(relativeTo)) {
223
+ if (dayDiff == -1) return 'tomorrow';
224
+ if (dayDiff == 1) return 'yesterday';
225
+ }
226
+ }
227
+ }
228
+
229
+ const isEqual = elapsed <= (epsilon ?? 0) * SECONDS,
175
230
  isFuture = !isEqual && diff < 0;
176
231
 
177
- let ret;
178
- if (isEqual) {
179
- ret = equalString;
180
- } else if (isFuture && !allowFuture) {
232
+ // 1) Degenerate cases
233
+ if (isFuture && !allowFuture) {
181
234
  console.warn(`Unexpected future date provided for timestamp: ${elapsed}ms in the future.`);
182
- ret = '[????]';
183
- } else {
235
+ return '[????]';
236
+ }
237
+
238
+ // 2) Handle (epsilon) equals
239
+ if (isEqual) {
240
+ return equalString;
241
+ }
242
+
243
+ // 3) Basic timestamp, with suffix /prefix
244
+ let ret = '';
245
+ if (elapsed < 60 * SECONDS) {
184
246
  // By default, moment will show 'a few seconds' for durations of 0-45 seconds. At the higher
185
247
  // end of that range that output is a bit too inaccurate, so we replace as per below.
186
- ret = elapsed < 60 * SECONDS ? '<1 minute' : moment.duration(elapsed).humanize();
248
+ ret = '<1 minute';
249
+ } else {
250
+ // Main delegate to humanize. Use 24h threshold vs. default 22h to avoid corner case
251
+ // transition w/localDateMode = limited: e.g. 21 hours -> zero days.
252
+ ret = moment.duration(elapsed).humanize({h: 24});
187
253
 
188
254
  // Moment outputs e.g. "a minute" instead of "1 minute". This creates some awkwardness
189
255
  // when the leading number comes and goes - "<1 minute" -> "a minute" -> "2 minutes".
190
256
  ret = ret.replace(/^(an|a) /, '1 ');
191
-
192
- if (short) ret = ret.replace('minute', 'min').replace('second', 'sec');
193
-
194
- ret += ' ' + (isFuture ? opts.futureSuffix : opts.pastSuffix);
195
257
  }
196
258
 
197
- return prefix ? prefix + ' ' + ret : ret;
259
+ if (short) ret = ret.replace('minute', 'min').replace('second', 'sec');
260
+ const suffix = isFuture ? opts.futureSuffix : opts.pastSuffix;
261
+ if (suffix) ret = ret + ' ' + suffix;
262
+
263
+ return ret;
198
264
  }
199
265
 
200
266
  function toTimestamp(v: Date | number): number {
@@ -75,8 +75,8 @@ export interface StoreFilterFieldProps extends DefaultHoistProps {
75
75
  */
76
76
  store?: Store;
77
77
 
78
- /** Width of the input in pixels. */
79
- width?: number;
78
+ /** Width of the input in pixels or string with unit. */
79
+ width?: string | number;
80
80
  }
81
81
 
82
82
  /**
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp, uses, XH, refreshContextView, BoxProps, HoistProps} from '@xh/hoist/core';
7
+ import {BoxProps, hoistCmp, HoistProps, refreshContextView, uses, XH} from '@xh/hoist/core';
8
8
  import {tabContainerImpl as desktopTabContainerImpl} from '@xh/hoist/dynamics/desktop';
9
9
  import {tabContainerImpl as mobileTabContainerImpl} from '@xh/hoist/dynamics/mobile';
10
10
  import {TabContainerModel} from './TabContainerModel';
@@ -0,0 +1,47 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2023 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {Column, ColumnRenderer, ColumnSortSpec} from '@xh/hoist/cmp/grid';
9
+ import {PersistOptions} from '@xh/hoist/core';
10
+
11
+ export type Zone = 'tl' | 'tr' | 'bl' | 'br';
12
+
13
+ export interface ZoneMapping {
14
+ /** Field to display. Must match a Field found in the Store */
15
+ field: string;
16
+ /** True to prefix the field value with its name */
17
+ showLabel?: boolean;
18
+ }
19
+
20
+ export interface ZoneLimit {
21
+ /** Min number of fields that should be mapped to the zone */
22
+ min?: number;
23
+ /** Max number of fields that should be mapped to the zone */
24
+ max?: number;
25
+ /** Array of allowed fields for the zone */
26
+ only?: string[];
27
+ }
28
+
29
+ export interface ZoneField {
30
+ field: string;
31
+ displayName: string;
32
+ label: string;
33
+ renderer: ColumnRenderer;
34
+ column: Column;
35
+ chooserGroup: string;
36
+ sortable: boolean;
37
+ sortingOrder: ColumnSortSpec[];
38
+ }
39
+
40
+ export interface ZoneGridModelPersistOptions extends PersistOptions {
41
+ /** True to include mapping information (default true) */
42
+ persistMapping?: boolean;
43
+ /** True to include grouping information (default true) */
44
+ persistGrouping?: boolean;
45
+ /** True to include sorting information (default true) */
46
+ persistSort?: boolean;
47
+ }
@@ -0,0 +1,62 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2023 Extremely Heavy Industries Inc.
6
+ */
7
+ import {hoistCmp, HoistProps, LayoutProps, TestSupportProps, uses, XH} from '@xh/hoist/core';
8
+ import {fragment} from '@xh/hoist/cmp/layout';
9
+ import {GridOptions} from '@xh/hoist/kit/ag-grid';
10
+ import {grid} from '@xh/hoist/cmp/grid';
11
+ import {splitLayoutProps} from '@xh/hoist/utils/react';
12
+ import {zoneMapper as desktopZoneMapper} from '@xh/hoist/dynamics/desktop';
13
+ import {zoneMapper as mobileZoneMapper} from '@xh/hoist/dynamics/mobile';
14
+ import {ZoneGridModel} from './ZoneGridModel';
15
+
16
+ export interface ZoneGridProps extends HoistProps<ZoneGridModel>, LayoutProps, TestSupportProps {
17
+ /**
18
+ * Options for ag-Grid's API.
19
+ *
20
+ * This constitutes an 'escape hatch' for applications that need to get to the underlying
21
+ * ag-Grid API. It should be used with care. Settings made here might be overwritten and/or
22
+ * interfere with the implementation of this component and its use of the ag-Grid API.
23
+ *
24
+ * Note that changes to these options after the component's initial render will be ignored.
25
+ */
26
+ agOptions?: GridOptions;
27
+ }
28
+
29
+ /**
30
+ * A ZoneGrid is a specialized version of the Grid component.
31
+ *
32
+ * It displays its data with multi-line full-width rows, each broken into four zones for
33
+ * top/bottom and left/right - (tl, tr, bl, br). Zone mappings determine which of the
34
+ * available fields should be extracted from the record and rendered into each zone.
35
+ */
36
+ export const [ZoneGrid, zoneGrid] = hoistCmp.withFactory<ZoneGridProps>({
37
+ displayName: 'ZoneGrid',
38
+ model: uses(ZoneGridModel),
39
+ className: 'xh-zone-grid',
40
+
41
+ render({model, className, testId, ...props}, ref) {
42
+ const {gridModel, mapperModel} = model,
43
+ [layoutProps] = splitLayoutProps(props),
44
+ platformZoneMapper = XH.isMobileApp ? mobileZoneMapper : desktopZoneMapper;
45
+
46
+ return fragment(
47
+ grid({
48
+ ...layoutProps,
49
+ className,
50
+ testId,
51
+ ref,
52
+ model: gridModel,
53
+ agOptions: {
54
+ suppressRowGroupHidesColumns: true,
55
+ suppressMakeColumnVisibleAfterUnGroup: true,
56
+ ...props.agOptions
57
+ }
58
+ }),
59
+ mapperModel ? platformZoneMapper() : null
60
+ );
61
+ }
62
+ });