@xh/hoist 56.3.0 → 56.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v56.4.0 - 2023-05-10
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Ensure that non-committed values are also checked when filtering a store with a FieldFilter.
8
+ This will maximize chances that records under edit will not disappear from user view due to
9
+ active filters.
10
+
11
+ ### 🐞 Bug Fixes
12
+
13
+ * Fix bug where Grid ColumnHeaders could throw when `groupDisplayType` was set to `singleColumn`.
14
+
15
+ ### ⚙️ Technical
16
+ * Adjustment to core model lookup in Hoist components to better support automated testing.
17
+ Components no longer strictly require rendering within an `AppContainer`.
18
+
19
+ ### ⚙️ Typescript API Adjustments
20
+
21
+ * Improved return types for `FetchService` methods and corrected `FetchOptions` interface.
22
+
3
23
  ## v56.3.0 - 2023-05-08
4
24
 
5
25
  ### 🎁 New Features
@@ -14,6 +14,7 @@ import {numberInput, switchInput, textInput} from '@xh/hoist/desktop/cmp/input';
14
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
15
15
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
16
16
  import {Icon} from '@xh/hoist/icon';
17
+ import {fmtTimeZone} from '@xh/hoist/utils/impl';
17
18
  import {LogDisplayModel} from './LogDisplayModel';
18
19
  import './LogViewer.scss';
19
20
 
@@ -87,11 +88,11 @@ const bbar = hoistCmp.factory(() => {
87
88
 
88
89
  return toolbar({
89
90
  items: [
90
- 'Server time:',
91
+ 'Server time: ',
91
92
  clock({
92
93
  timezone: zone,
93
- format: 'HH:mm [GMT]Z',
94
- className: 'xh-font-family-mono xh-font-size-small'
94
+ format: 'HH:mm',
95
+ suffix: fmtTimeZone(zone, XH.getEnv('serverTimeZoneOffset'))
95
96
  })
96
97
  ],
97
98
  omit: !zone // zone env support requires hoist-core 7.1+
package/cmp/grid/Grid.ts CHANGED
@@ -197,8 +197,8 @@ class GridLocalModel extends HoistModel {
197
197
  clipboardCopy: Icon.copy({asHtml: true})
198
198
  },
199
199
  components: {
200
- agColumnHeader: props => columnHeader(props),
201
- agColumnGroupHeader: props => columnGroupHeader(props)
200
+ agColumnHeader: props => columnHeader({...props, gridModel: model}),
201
+ agColumnGroupHeader: props => columnGroupHeader({...props, gridModel: model})
202
202
  },
203
203
  rowSelection: selModel.mode == 'disabled' ? undefined : selModel.mode,
204
204
  suppressRowClickSelection: !selModel.isEnabled,
@@ -703,7 +703,7 @@ export class Column {
703
703
  lockPinned: !gridModel.enableColumnPinning || XH.isMobileApp,
704
704
  pinned: this.pinned,
705
705
  lockVisible: !this.hideable || !gridModel.colChooserModel || XH.isMobileApp,
706
- headerComponentParams: {gridModel, xhColumn: this},
706
+ headerComponentParams: {xhColumn: this},
707
707
  suppressColumnsToolPanel: this.excludeFromChooser,
708
708
  suppressFiltersToolPanel: this.excludeFromChooser,
709
709
  enableCellChangeFlash: this.highlightOnChange,
@@ -67,7 +67,7 @@ export function useModelLinker(model: HoistModel, modelLookup: ModelLookup, prop
67
67
  if (isLinking) {
68
68
  model._modelLookup = modelLookup;
69
69
  each(model['_xhInjectedParentProperties'], (selector, name) => {
70
- const parentModel = modelLookup.lookupModel(selector);
70
+ const parentModel = modelLookup?.lookupModel(selector);
71
71
  if (!parentModel) {
72
72
  throw XH.exception(
73
73
  `Failed to resolve @lookup for property '${name}' with selector ${formatSelector(
@@ -81,7 +81,7 @@ export function useModelLinker(model: HoistModel, modelLookup: ModelLookup, prop
81
81
 
82
82
  // Linked models with an impl parent that are not explicitly marked should be marked as impl.
83
83
  if (isUndefined(model.xhImpl)) {
84
- const parentModel = modelLookup.lookupModel('*');
84
+ const parentModel = modelLookup?.lookupModel('*');
85
85
  if (parentModel?.xhImpl === true) {
86
86
  model.xhImpl = true;
87
87
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from 'lodash';
20
20
  import {parseFieldValue} from '../Field';
21
21
  import {Store} from '../Store';
22
+ import {StoreRecord} from '../StoreRecord';
22
23
  import {Filter} from './Filter';
23
24
  import {FieldFilterOperator, FieldFilterSpec, FilterTestFn} from './Types';
24
25
 
@@ -111,91 +112,76 @@ export class FieldFilter extends Filter {
111
112
  ? value.map(v => parseFieldValue(v, fieldType))
112
113
  : parseFieldValue(value, fieldType);
113
114
  }
114
- const getVal = store ? r => r.committedData[field] : r => r[field],
115
- doNotFilter = r => store && isNil(r.committedData); // Ignore (do not filter out) record if part of a store and it has no committed data
116
115
 
117
116
  if (FieldFilter.ARRAY_OPERATORS.includes(op)) {
118
117
  value = castArray(value);
119
118
  }
120
119
 
120
+ let opFn: (v: any) => boolean;
121
121
  switch (op) {
122
122
  case '=':
123
- return r => {
124
- if (doNotFilter(r)) return true;
125
- let v = getVal(r);
123
+ opFn = v => {
126
124
  if (isNil(v) || v === '') v = null;
127
125
  return value.includes(v);
128
126
  };
127
+ break;
129
128
  case '!=':
130
- return r => {
131
- if (doNotFilter(r)) return true;
132
- let v = getVal(r);
129
+ opFn = v => {
133
130
  if (isNil(v) || v === '') v = null;
134
131
  return !value.includes(v);
135
132
  };
133
+ break;
136
134
  case '>':
137
- return r => {
138
- if (doNotFilter(r)) return true;
139
- const v = getVal(r);
140
- return !isNil(v) && v > value;
141
- };
135
+ opFn = v => !isNil(v) && v > value;
136
+ break;
142
137
  case '>=':
143
- return r => {
144
- if (doNotFilter(r)) return true;
145
- const v = getVal(r);
146
- return !isNil(v) && v >= value;
147
- };
138
+ opFn = v => !isNil(v) && v >= value;
139
+ break;
148
140
  case '<':
149
- return r => {
150
- if (doNotFilter(r)) return true;
151
- const v = getVal(r);
152
- return !isNil(v) && v < value;
153
- };
141
+ opFn = v => !isNil(v) && v < value;
142
+ break;
154
143
  case '<=':
155
- return r => {
156
- if (doNotFilter(r)) return true;
157
- const v = getVal(r);
158
- return !isNil(v) && v <= value;
159
- };
144
+ opFn = v => !isNil(v) && v <= value;
145
+ break;
160
146
  case 'like':
161
147
  regExps = value.map(v => new RegExp(escapeRegExp(v), 'i'));
162
- return r => {
163
- if (doNotFilter(r)) return true;
164
- return regExps.some(re => re.test(getVal(r)));
165
- };
148
+ opFn = v => regExps.some(re => re.test(v));
149
+ break;
166
150
  case 'not like':
167
151
  regExps = value.map(v => new RegExp(escapeRegExp(v), 'i'));
168
- return r => {
169
- if (doNotFilter(r)) return true;
170
- return regExps.every(re => !re.test(getVal(r)));
171
- };
152
+ opFn = v => regExps.every(re => !re.test(v));
153
+ break;
172
154
  case 'begins':
173
155
  regExps = value.map(v => new RegExp('^' + escapeRegExp(v), 'i'));
174
- return r => {
175
- if (doNotFilter(r)) return true;
176
- return regExps.some(re => re.test(getVal(r)));
177
- };
156
+ opFn = v => regExps.some(re => re.test(v));
157
+ break;
178
158
  case 'ends':
179
159
  regExps = value.map(v => new RegExp(escapeRegExp(v) + '$', 'i'));
180
- return r => {
181
- if (doNotFilter(r)) return true;
182
- return regExps.some(re => re.test(getVal(r)));
183
- };
160
+ opFn = v => regExps.some(re => re.test(v));
161
+ break;
184
162
  case 'includes':
185
- return r => {
186
- if (doNotFilter(r)) return true;
187
- const v = getVal(r);
188
- return !isNil(v) && v.some(it => value.includes(it));
189
- };
163
+ opFn = v => !isNil(v) && v.some(it => value.includes(it));
164
+ break;
190
165
  case 'excludes':
191
- return r => {
192
- if (doNotFilter(r)) return true;
193
- const v = getVal(r);
194
- return isNil(v) || !v.some(it => value.includes(it));
195
- };
166
+ opFn = v => isNil(v) || !v.some(it => value.includes(it));
167
+ break;
196
168
  default:
197
169
  throw XH.exception(`Unknown operator: ${op}`);
198
170
  }
171
+
172
+ if (!store) return r => opFn(r[field]);
173
+
174
+ return (r: StoreRecord) => {
175
+ const val = r.get(field);
176
+ if (opFn(val)) return true;
177
+
178
+ // Maximize chances of matching. Always pass adds ...
179
+ if (r.isAdd) return true;
180
+
181
+ // ... and check any differing original value as well
182
+ const committedVal = r.committedData[field];
183
+ return committedVal !== val && opFn(committedVal);
184
+ };
199
185
  }
200
186
 
201
187
  override equals(other: Filter): boolean {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "56.3.0",
3
+ "version": "56.4.0",
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",
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistService, XH, Exception, PlainObject, Thunkable} from '@xh/hoist/core';
7
+ import {HoistService, XH, Exception, PlainObject, Thunkable, FetchResponse} from '@xh/hoist/core';
8
8
  import {isLocalDate, SECONDS, ONE_MINUTE, olderThan} from '@xh/hoist/utils/datetime';
9
9
  import {throwIf} from '@xh/hoist/utils/js';
10
10
  import {StatusCodes} from 'http-status-codes';
@@ -61,10 +61,8 @@ export class FetchService extends HoistService {
61
61
  * Send a request via the underlying fetch API.
62
62
  * @returns Promise which resolves to a Fetch Response.
63
63
  */
64
- fetch(opts: FetchOptions): Promise<any> {
65
- return this.managedFetchAsync(opts, aborter =>
66
- this.fetchInternalAsync(opts, aborter)
67
- ) as any;
64
+ fetch(opts: FetchOptions): Promise<FetchResponse> {
65
+ return this.managedFetchAsync(opts, aborter => this.fetchInternalAsync(opts, aborter));
68
66
  }
69
67
 
70
68
  /**
@@ -81,7 +79,7 @@ export class FetchService extends HoistService {
81
79
  aborter
82
80
  );
83
81
  return this.NO_JSON_RESPONSES.includes(r.status) ? null : r.json();
84
- }) as any;
82
+ });
85
83
  }
86
84
 
87
85
  /**
@@ -142,7 +140,10 @@ export class FetchService extends HoistService {
142
140
  //-----------------------
143
141
  // Implementation
144
142
  //-----------------------
145
- private async managedFetchAsync(opts: FetchOptions, fn: (ctl: AbortController) => any) {
143
+ private async managedFetchAsync(
144
+ opts: FetchOptions,
145
+ fn: (ctl: AbortController) => Promise<FetchResponse>
146
+ ): Promise<FetchResponse> {
146
147
  const {autoAborters, defaultTimeout} = this,
147
148
  {autoAbortKey, timeout = defaultTimeout} = opts,
148
149
  aborter = new AbortController();
@@ -177,7 +178,7 @@ export class FetchService extends HoistService {
177
178
  }
178
179
  }
179
180
 
180
- private async fetchInternalAsync(opts, aborter): Promise<any> {
181
+ private async fetchInternalAsync(opts, aborter): Promise<FetchResponse> {
181
182
  const {defaultHeaders} = this;
182
183
  let {url, method, headers, body, params} = opts;
183
184
  throwIf(!url, 'No url specified in call to fetchService.');
@@ -236,7 +237,7 @@ export class FetchService extends HoistService {
236
237
  }
237
238
  }
238
239
 
239
- const ret: any = await fetch(url, fetchOpts);
240
+ const ret = (await fetch(url, fetchOpts)) as FetchResponse;
240
241
 
241
242
  if (!ret.ok) {
242
243
  ret.responseText = await this.safeResponseTextAsync(ret);
@@ -300,8 +301,11 @@ export interface FetchOptions {
300
301
  /** URL for the request. Relative urls will be appended to XH.baseUrl. */
301
302
  url: string;
302
303
 
303
- /** Data to send in the request body (for POSTs/PUTs of JSON).*/
304
- body?: PlainObject;
304
+ /**
305
+ * Data to send in the request body (for POSTs/PUTs of JSON).
306
+ * When using `fetch`, provide a string. Otherwise, provide a PlainObject.
307
+ */
308
+ body?: PlainObject | string;
305
309
 
306
310
  /**
307
311
  * Parameters to encode and append as a query string, or send with the request body
@@ -13,7 +13,7 @@ import {fmtNumber} from '@xh/hoist/format';
13
13
  export function fmtTimeZone(name: string, offset: number): string {
14
14
  if (!name) return '';
15
15
 
16
- return name !== 'GMT'
17
- ? `${name} (GMT${fmtNumber(offset / HOURS, {withPlusSign: true, asHtml: true})})`
18
- : `${name}`;
16
+ return name === 'GMT' || name === 'UTC'
17
+ ? name
18
+ : `${name} (GMT${fmtNumber(offset / HOURS, {withPlusSign: true, asHtml: true})})`;
19
19
  }