@xh/hoist 59.3.1 → 59.3.2

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
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 59.3.2 - 2023-11-21
4
+
5
+ ### 🐞 Bug Fixes
6
+
7
+ * `ZoneGrid` will more gracefully handle state that has become out of sync with its mapper requirements.
8
+
3
9
  ## 59.3.1 - 2023-11-10
4
10
 
5
11
  ### 🐞 Bug Fixes
@@ -49,7 +49,7 @@ import {
49
49
  } from '@ag-grid-community/core';
50
50
  import {Icon} from '@xh/hoist/icon';
51
51
  import {throwIf, withDefault} from '@xh/hoist/utils/js';
52
- import {castArray, forOwn, isEmpty, isFinite, isPlainObject, isString} from 'lodash';
52
+ import {castArray, forOwn, isEmpty, isFinite, isPlainObject, isString, find} from 'lodash';
53
53
  import {ReactNode} from 'react';
54
54
  import {ZoneMapperConfig, ZoneMapperModel} from './impl/ZoneMapperModel';
55
55
  import {ZoneGridPersistenceModel} from './impl/ZoneGridPersistenceModel';
@@ -326,7 +326,7 @@ export class ZoneGridModel extends HoistModel {
326
326
 
327
327
  this.availableColumns = columns.map(it => ({...it, hidden: true}));
328
328
  this.limits = limits;
329
- this.mappings = this.parseMappings(mappings);
329
+ this.mappings = this.parseMappings(mappings, true);
330
330
 
331
331
  this.leftColumnSpec = leftColumnSpec;
332
332
  this.rightColumnSpec = rightColumnSpec;
@@ -397,7 +397,7 @@ export class ZoneGridModel extends HoistModel {
397
397
 
398
398
  @action
399
399
  setMappings(mappings: Record<Zone, Some<string | ZoneMapping>>) {
400
- this.mappings = this.parseMappings(mappings);
400
+ this.mappings = this.parseMappings(mappings, false);
401
401
  this.gridModel.setColumns(this.getColumns());
402
402
  }
403
403
 
@@ -611,46 +611,62 @@ export class ZoneGridModel extends HoistModel {
611
611
  }
612
612
 
613
613
  private parseMappings(
614
- mappings: Record<Zone, Some<string | ZoneMapping>>
614
+ mappings: Record<Zone, Some<string | ZoneMapping>>,
615
+ strict: boolean
615
616
  ): Record<Zone, ZoneMapping[]> {
616
617
  const ret = {} as Record<Zone, ZoneMapping[]>;
617
- forOwn(mappings, (rawMapping, zone) => {
618
- // 1) Standardize mapping into an array of ZoneMappings
619
- const mapping = [];
620
- castArray(rawMapping).forEach(it => {
621
- if (!it) return;
618
+ forOwn(mappings, (rawMapping, zone: Zone) => {
619
+ try {
620
+ ret[zone] = this.parseZoneMapping(zone, rawMapping);
621
+ } catch (e) {
622
+ if (strict) throw e;
623
+ console.warn(e.message);
624
+ ret[zone] = this._defaultState.mappings[zone];
625
+ }
626
+ });
627
+ return ret;
628
+ }
622
629
 
623
- const ret = isString(it) ? {field: it} : it,
624
- col = this.findColumnSpec(ret);
630
+ private parseZoneMapping(zone: Zone, rawMapping: Some<string | ZoneMapping>): ZoneMapping[] {
631
+ const ret: ZoneMapping[] = [];
625
632
 
626
- throwIf(!col, `Column not found for field ${ret.field}`);
627
- return mapping.push(ret);
628
- });
633
+ // 1) Standardize raw mapping into an array of ZoneMappings
634
+ castArray(rawMapping).forEach(it => {
635
+ if (!it) return;
629
636
 
630
- // 2) Ensure mapping respects configured limits
631
- const limit = this.limits?.[zone];
632
- if (limit) {
633
- throwIf(
634
- isFinite(limit.min) && mapping.length < limit.min,
635
- `Requires minimum ${limit.min} mappings in zone "${zone}"`
636
- );
637
- throwIf(
638
- isFinite(limit.max) && mapping.length > limit.max,
639
- `Exceeds maximum ${limit.max} mappings in zone "${zone}"`
640
- );
641
-
642
- if (!isEmpty(limit.only)) {
643
- mapping.forEach(it => {
644
- throwIf(
645
- !limit.only.includes(it.field),
646
- `Field "${it.field}" not allowed in zone "${zone}"`
647
- );
648
- });
649
- }
650
- }
637
+ const fieldSpec = isString(it) ? {field: it} : it,
638
+ col = this.findColumnSpec(fieldSpec);
639
+
640
+ throwIf(!col, `Column not found for field '${fieldSpec.field}'`);
651
641
 
652
- ret[zone] = mapping;
642
+ ret.push(fieldSpec);
653
643
  });
644
+
645
+ // 2) Ensure we respect configured limits
646
+ const limit = this.limits?.[zone];
647
+ if (limit) {
648
+ throwIf(
649
+ isFinite(limit.min) && ret.length < limit.min,
650
+ `Requires minimum ${limit.min} mappings in zone "${zone}."`
651
+ );
652
+
653
+ throwIf(
654
+ isFinite(limit.max) && ret.length > limit.max,
655
+ `Exceeds maximum ${limit.max} mappings in zone "${zone}".`
656
+ );
657
+
658
+ if (!isEmpty(limit.only)) {
659
+ const offender = find(ret, it => !limit.only.includes(it.field));
660
+ throwIf(offender, `Field "${offender?.field}" not allowed in zone "${zone}".`);
661
+ }
662
+ }
663
+
664
+ // 3) Ensure top zones have at least the minimum required single field
665
+ throwIf(
666
+ (zone == 'tl' || zone == 'tr') && isEmpty(ret),
667
+ `Top mapping '${zone}' requires at least one field.`
668
+ );
669
+
654
670
  return ret;
655
671
  }
656
672
 
package/core/XH.ts CHANGED
@@ -674,9 +674,9 @@ export class XHApi {
674
674
 
675
675
  /** Get the first active model that matches the given selector, or null if none found. */
676
676
  getModel<T extends HoistModel>(selector: ModelSelector = '*'): T {
677
- instanceManager.models.forEach(m => {
678
- if (m.matchesSelector(selector, true)) return m;
679
- });
677
+ for (let m of instanceManager.models) {
678
+ if (m.matchesSelector(selector, true)) return m as T;
679
+ }
680
680
  return null;
681
681
  }
682
682
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "59.3.1",
3
+ "version": "59.3.2",
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",
@@ -125,7 +125,7 @@ export class EnvironmentService extends HoistService {
125
125
  } else if (mode === 'forceReload') {
126
126
  XH.suspendApp({
127
127
  reason: 'APP_UPDATE',
128
- message: `A new version of ${XH.clientAppName} is available!`
128
+ message: `A new version of ${XH.clientAppName} is now available (${appVersion}) and requires an immediate update.`
129
129
  });
130
130
  }
131
131
  }
@@ -15,19 +15,20 @@ import {isBoolean, isFinite, isFunction, isNil, isString, pull} from 'lodash';
15
15
  * Promise-aware recurring task timer for use by framework and applications.
16
16
  *
17
17
  * This object is designed to be robust across failing tasks, and never to re-run the task
18
- * simultaneously, unless in the case of a timeout. Callers can optionally specify
19
- * the duration of asynchronous tasks by returning a Promise from runFn.
18
+ * simultaneously, unless in the case of a timeout. Callers can optionally specify the duration
19
+ * of asynchronous tasks by returning a Promise from runFn.
20
20
  *
21
- * This object seeks to mirror the API and semantics of the server-side equivalent 'Timer'
22
- * as closely as possible. However, there are important differences due to the synchronous
23
- * nature of javascript. In particular, there is no support for 'runImmediatelyAndBlock', and the
24
- * 'timeout' argument will not be able to interrupt synchronous activity of the runFn.
21
+ * This object seeks to mirror the API and semantics of `Timer.groovy` from Hoist Core as closely
22
+ * as possible. However, there are important differences due to the synchronous nature of
23
+ * javascript. In particular, there is no support for `runImmediatelyAndBlock`, and the `timeout`
24
+ * argument will not be able to interrupt synchronous activity of the runFn.
25
25
  *
26
- * All public properties should be considered read-only. See `setInterval()` to change the interval
27
- * of this timer dynamically.
26
+ * All public properties should be considered read-only.
27
+ * See `setInterval()` to change the interval of this Timer dynamically.
28
28
  */
29
29
  export class Timer {
30
30
  static _timers: Timer[] = [];
31
+ static MIN_INTERVAL_MS = 500;
31
32
 
32
33
  runFn: () => any = null;
33
34
  interval: number | (() => number) = null;
@@ -41,6 +42,8 @@ export class Timer {
41
42
  isRunning: boolean = false;
42
43
  lastRun: Date = null;
43
44
 
45
+ private warnedIntervals = new Set();
46
+
44
47
  /** Create a new Timer. */
45
48
  static create({
46
49
  runFn,
@@ -67,9 +70,7 @@ export class Timer {
67
70
  this._timers = [];
68
71
  }
69
72
 
70
- /**
71
- * Permanently cancel this timer.
72
- */
73
+ /** Permanently cancel this timer. */
73
74
  cancel() {
74
75
  this.cancelInternal();
75
76
  pull(Timer._timers, this);
@@ -77,7 +78,6 @@ export class Timer {
77
78
 
78
79
  /**
79
80
  * Change the interval of this timer.
80
- *
81
81
  * @param interval - ms to wait between runs or any value `<=0` to pause the timer.
82
82
  */
83
83
  setInterval(interval: number) {
@@ -94,7 +94,10 @@ export class Timer {
94
94
  this.intervalUnits = args.intervalUnits;
95
95
  this.timeoutUnits = args.timeoutUnits;
96
96
  this.delay = this.parseDelay(args.delay);
97
- throwIf(this.interval == null || this.runFn == null, 'Missing req arguments for Timer');
97
+ throwIf(
98
+ this.interval == null || this.runFn == null,
99
+ 'Missing required arguments for Timer - both interval and runFn must be specified.'
100
+ );
98
101
 
99
102
  wait(this.delay).then(() => this.heartbeatAsync());
100
103
  }
@@ -133,18 +136,27 @@ export class Timer {
133
136
  return isString(val) ? () => XH.configService.get(val) : val;
134
137
  }
135
138
 
136
- private parseDelay(val): number {
139
+ private parseDelay(val: number | boolean): number {
137
140
  if (isBoolean(val)) return val ? this.intervalMs : 0;
138
141
  return isFinite(val) ? val : 0;
139
142
  }
140
143
 
141
144
  private get intervalMs() {
142
- const {interval, intervalUnits} = this;
145
+ const {interval, intervalUnits, warnedIntervals} = this,
146
+ min = Timer.MIN_INTERVAL_MS;
147
+
143
148
  if (isNil(interval)) return null;
149
+
144
150
  let ret = (isFunction(interval) ? interval() : interval) * intervalUnits;
145
- if (ret > 0 && ret < 500) {
146
- console.warn('Timer cannot be set for values less than 500ms.');
147
- ret = 500;
151
+
152
+ if (ret > 0 && ret < min) {
153
+ if (!warnedIntervals.has(ret)) {
154
+ warnedIntervals.add(ret);
155
+ console.warn(
156
+ `Timer interval of ${ret}ms requested - forcing to min interval of ${min}ms.`
157
+ );
158
+ }
159
+ ret = min;
148
160
  }
149
161
  return ret;
150
162
  }