@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 +20 -0
- package/admin/tabs/server/logViewer/LogDisplay.ts +4 -3
- package/cmp/grid/Grid.ts +2 -2
- package/cmp/grid/columns/Column.ts +1 -1
- package/core/model/Hooks.ts +2 -2
- package/data/filter/FieldFilter.ts +40 -54
- package/package.json +1 -1
- package/svc/FetchService.ts +15 -11
- package/utils/impl/TimeZone.ts +3 -3
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
|
|
94
|
-
|
|
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: {
|
|
706
|
+
headerComponentParams: {xhColumn: this},
|
|
707
707
|
suppressColumnsToolPanel: this.excludeFromChooser,
|
|
708
708
|
suppressFiltersToolPanel: this.excludeFromChooser,
|
|
709
709
|
enableCellChangeFlash: this.highlightOnChange,
|
package/core/model/Hooks.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
package/svc/FetchService.ts
CHANGED
|
@@ -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<
|
|
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
|
-
})
|
|
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(
|
|
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<
|
|
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
|
|
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
|
-
/**
|
|
304
|
-
|
|
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
|
package/utils/impl/TimeZone.ts
CHANGED
|
@@ -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
|
|
17
|
-
?
|
|
18
|
-
: `${name}`;
|
|
16
|
+
return name === 'GMT' || name === 'UTC'
|
|
17
|
+
? name
|
|
18
|
+
: `${name} (GMT${fmtNumber(offset / HOURS, {withPlusSign: true, asHtml: true})})`;
|
|
19
19
|
}
|