@xh/hoist 74.0.0-SNAPSHOT.1748629362620 → 74.0.0-SNAPSHOT.1749036244810

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/CHANGELOG.md CHANGED
@@ -9,10 +9,13 @@
9
9
  ### 🎁 New Features
10
10
  * Added `ViewManagerModel.preserveUnsavedChanges` flag to opt-out of that behaviour.
11
11
  * Added `PersistOptions.settleTime` to configure time to wait for state to settle before persisting.
12
+ * Support for gridcolumn level `onCellClicked` events.
12
13
 
13
14
  ### 🐞 Bug Fixes
14
15
  * Improved `ViewManagerModel.settleTime` by delegating to individual `PersistenceProviders`.
15
16
  * Fixed bug where grid column state could become unintentionally dirty when columns were hidden.
17
+ * Improved `WebsocketService` heartbeat detection to auto-reconnect when the socket reports as open
18
+ and heartbeats can be sent, but no heartbeat acknowledgements are being received from the server.
16
19
 
17
20
  ## v73.0.1 - 2025-05-19
18
21
 
@@ -120,7 +120,9 @@ export interface GridConfig {
120
120
  */
121
121
  onRowDoubleClicked?: (e: RowDoubleClickedEvent) => void;
122
122
  /**
123
- * Callback when a cell is clicked.
123
+ * Callback when any cell on the grid is clicked - inspect the event to determine the column.
124
+ * Note that {@link ColumnSpec.onCellClicked} is a more targeted handler scoped to a single
125
+ * column, which might be more convenient when clicks on only one column are of interest.
124
126
  */
125
127
  onCellClicked?: (e: CellClickedEvent) => void;
126
128
  /**
@@ -4,6 +4,7 @@ import { FunctionComponent, ReactNode } from 'react';
4
4
  import { GridModel } from '../GridModel';
5
5
  import { ColumnCellClassFn, ColumnCellClassRuleFn, ColumnComparator, ColumnEditableFn, ColumnEditorFn, ColumnEditorProps, ColumnExcelFormatFn, ColumnExportValueFn, ColumnGetValueFn, ColumnHeaderClassFn, ColumnHeaderNameFn, ColumnRenderer, ColumnSetValueFn, ColumnSortSpec, ColumnSortValueFn, ColumnTooltipFn } from '../Types';
6
6
  import type { ColDef } from '@xh/hoist/kit/ag-grid';
7
+ import { CellClickedEvent } from '@ag-grid-community/core';
7
8
  export interface ColumnSpec {
8
9
  /**
9
10
  * Name of data store field to display within the column, or object containing properties
@@ -248,6 +249,11 @@ export interface ColumnSpec {
248
249
  * many rows + multiple actions per row. Defaults to false;
249
250
  */
250
251
  actionsShowOnHoverOnly?: boolean;
252
+ /**
253
+ * Callback when a cell within this column clicked.
254
+ * See also {@link GridConfig.onCellClicked}, called when any cell within the grid is clicked.
255
+ */
256
+ onCellClicked?: (e: CellClickedEvent) => void;
251
257
  /**
252
258
  * "escape hatch" object to pass directly to Ag-Grid for desktop implementations. Note these
253
259
  * options may be used / overwritten by the framework itself, and are not all guaranteed to be
@@ -337,6 +343,7 @@ export declare class Column {
337
343
  actionsShowOnHoverOnly?: boolean;
338
344
  fieldSpec: FieldSpec;
339
345
  omit: Thunkable<boolean>;
346
+ onCellClicked?: (e: CellClickedEvent) => void;
340
347
  gridModel: GridModel;
341
348
  agOptions: ColDef;
342
349
  appData: PlainObject;
@@ -45,6 +45,8 @@ export declare class WebSocketService extends HoistService {
45
45
  private _timer;
46
46
  private _socket;
47
47
  private _subsByTopic;
48
+ private _lastHeartbeatSent;
49
+ private _lastHeartbeatReceived;
48
50
  constructor();
49
51
  initAsync(): Promise<void>;
50
52
  /**
@@ -70,7 +72,9 @@ export declare class WebSocketService extends HoistService {
70
72
  getFormattedTelemetry(): PlainObject;
71
73
  private connect;
72
74
  private disconnect;
75
+ private reconnect;
73
76
  private heartbeatOrReconnect;
77
+ private get heartbeatWasUnacknowledged();
74
78
  private onServerInstanceChange;
75
79
  onOpen(ev: any): void;
76
80
  onClose(ev: any): void;
@@ -255,7 +255,9 @@ export interface GridConfig {
255
255
  onRowDoubleClicked?: (e: RowDoubleClickedEvent) => void;
256
256
 
257
257
  /**
258
- * Callback when a cell is clicked.
258
+ * Callback when any cell on the grid is clicked - inspect the event to determine the column.
259
+ * Note that {@link ColumnSpec.onCellClicked} is a more targeted handler scoped to a single
260
+ * column, which might be more convenient when clicks on only one column are of interest.
259
261
  */
260
262
  onCellClicked?: (e: CellClickedEvent) => void;
261
263
 
@@ -71,6 +71,7 @@ import type {
71
71
  ValueGetterParams,
72
72
  ValueSetterParams
73
73
  } from '@xh/hoist/kit/ag-grid';
74
+ import {CellClickedEvent} from '@ag-grid-community/core';
74
75
 
75
76
  export interface ColumnSpec {
76
77
  /**
@@ -374,6 +375,12 @@ export interface ColumnSpec {
374
375
  */
375
376
  actionsShowOnHoverOnly?: boolean;
376
377
 
378
+ /**
379
+ * Callback when a cell within this column clicked.
380
+ * See also {@link GridConfig.onCellClicked}, called when any cell within the grid is clicked.
381
+ */
382
+ onCellClicked?: (e: CellClickedEvent) => void;
383
+
377
384
  /**
378
385
  * "escape hatch" object to pass directly to Ag-Grid for desktop implementations. Note these
379
386
  * options may be used / overwritten by the framework itself, and are not all guaranteed to be
@@ -479,6 +486,7 @@ export class Column {
479
486
  actionsShowOnHoverOnly?: boolean;
480
487
  fieldSpec: FieldSpec;
481
488
  omit: Thunkable<boolean>;
489
+ onCellClicked?: (e: CellClickedEvent) => void;
482
490
 
483
491
  gridModel: GridModel;
484
492
  agOptions: ColDef;
@@ -551,6 +559,7 @@ export class Column {
551
559
  actionsShowOnHoverOnly,
552
560
  actions,
553
561
  omit,
562
+ onCellClicked,
554
563
  agOptions,
555
564
  appData,
556
565
  ...rest
@@ -657,6 +666,7 @@ export class Column {
657
666
 
658
667
  this.actions = actions;
659
668
  this.actionsShowOnHoverOnly = actionsShowOnHoverOnly ?? false;
669
+ this.onCellClicked = onCellClicked;
660
670
 
661
671
  this.gridModel = gridModel;
662
672
  this.agOptions = agOptions ? clone(agOptions) : {};
@@ -758,7 +768,8 @@ export class Column {
758
768
  if (event.shiftKey && event.key === 'Enter') return true;
759
769
 
760
770
  return false;
761
- }
771
+ },
772
+ onCellClicked: this.onCellClicked
762
773
  };
763
774
 
764
775
  // We will change this setter as needed to install the renderer in the proper location
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "74.0.0-SNAPSHOT.1748629362620",
3
+ "version": "74.0.0-SNAPSHOT.1749036244810",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
@@ -70,6 +70,9 @@ export class WebSocketService extends HoistService {
70
70
  private _socket: WebSocket;
71
71
  private _subsByTopic: Record<string, WebSocketSubscription[]> = {};
72
72
 
73
+ private _lastHeartbeatSent: number = null;
74
+ private _lastHeartbeatReceived: number = null;
75
+
73
76
  constructor() {
74
77
  super();
75
78
  makeObservable(this);
@@ -177,6 +180,10 @@ export class WebSocketService extends HoistService {
177
180
  if (s === this._socket) this.onMessage(data);
178
181
  };
179
182
  this._socket = s;
183
+
184
+ // Reset heartbeat tracking - any prior values no longer relevant.
185
+ this._lastHeartbeatReceived = null;
186
+ this._lastHeartbeatSent = null;
180
187
  } catch (e) {
181
188
  this.logError('Failure creating WebSocket', e);
182
189
  }
@@ -192,24 +199,41 @@ export class WebSocketService extends HoistService {
192
199
  this.updateConnectedStatus();
193
200
  }
194
201
 
202
+ private reconnect() {
203
+ this.disconnect();
204
+ this.connect();
205
+ }
206
+
195
207
  private heartbeatOrReconnect() {
196
208
  this.updateConnectedStatus();
197
- if (this.connected) {
198
- this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
199
- this.noteTelemetryEvent('heartbeatSent');
200
- } else {
201
- this.logWarn('Heartbeat found websocket not connected - attempting to reconnect...');
209
+
210
+ // If there is a problem, attempt to reconnect and come back on the next cycle.
211
+ const {connected, heartbeatWasUnacknowledged} = this;
212
+ if (!connected || heartbeatWasUnacknowledged) {
213
+ this.logWarn(
214
+ `Heartbeat found ${!connected ? 'websocket not connected' : 'last heartbeat not acknowledged'} - attempting to reconnect...`
215
+ );
202
216
  this.noteTelemetryEvent('heartbeatReconnectAttempt');
203
- this.disconnect();
204
- this.connect();
217
+ this.reconnect();
218
+ return;
205
219
  }
220
+
221
+ // If all looks OK, send a heartbeat message.
222
+ this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
223
+ this.noteTelemetryEvent('heartbeatSent');
224
+ this._lastHeartbeatSent = Date.now();
225
+ }
226
+
227
+ // We expect the server to respond immediately to every heartbeat. There will be a tiny window
228
+ // while the message is round-tripping, but that's not material to our check on HEARTBEAT_INTERVAL.
229
+ private get heartbeatWasUnacknowledged() {
230
+ return this._lastHeartbeatSent > this._lastHeartbeatReceived;
206
231
  }
207
232
 
208
233
  private onServerInstanceChange() {
209
234
  this.logWarn('Server instance changed - attempting to connect to new instance.');
210
235
  this.noteTelemetryEvent('instanceChangeReconnectAttempt');
211
- this.disconnect();
212
- this.connect();
236
+ this.reconnect();
213
237
  }
214
238
 
215
239
  //------------------------
@@ -256,6 +280,7 @@ export class WebSocketService extends HoistService {
256
280
  XH.clientHealthService.sendReportAsync();
257
281
  break;
258
282
  case this.HEARTBEAT_TOPIC:
283
+ this._lastHeartbeatReceived = Date.now();
259
284
  this.noteTelemetryEvent('heartbeatReceived');
260
285
  break;
261
286
  }