@intellegens/cornerstone-client 0.0.9999-alpha-15 → 0.0.9999-alpha-17
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/dist/adapters/SearchAdapter/index.d.ts +32 -0
- package/dist/adapters/SearchAdapter/index.js +234 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/package.json +3 -1
- package/src/adapters/SearchAdapter/index.ts +289 -0
- package/src/adapters/index.ts +1 -0
- package/tests/CollectionViewAdapter.test.ts +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { IIdentifiable } from '../../data';
|
|
2
|
+
export declare class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
3
|
+
private _readClient;
|
|
4
|
+
private _isLoading;
|
|
5
|
+
private _searchResults;
|
|
6
|
+
private _totalItemCount;
|
|
7
|
+
private _searchTerm;
|
|
8
|
+
private _selectedType?;
|
|
9
|
+
private _lastInputSearchTerm?;
|
|
10
|
+
private _lastInputType?;
|
|
11
|
+
private _searchTriggerLength;
|
|
12
|
+
private _searchTriggerDelay;
|
|
13
|
+
private _triggeredSearch;
|
|
14
|
+
private _selection;
|
|
15
|
+
private _multiSelectMode;
|
|
16
|
+
constructor(controllerName: string, multiSelect: boolean, searchTriggerLength: number, searchTriggerDelay: number, dataChangedCallback: (isLoading: boolean, data: TDto[] | undefined, error: string | undefined) => void);
|
|
17
|
+
getSelectedItems(): TDto[];
|
|
18
|
+
getSelectedType(): string | undefined;
|
|
19
|
+
getSelectMode(): boolean;
|
|
20
|
+
addToSelection(item: TDto): void;
|
|
21
|
+
removeFromSelection(item: TDto): void;
|
|
22
|
+
search(searchTerm: string, type?: string): Promise<TDto[]>;
|
|
23
|
+
clearSearch(): void;
|
|
24
|
+
private readonly _dataChangedCallback;
|
|
25
|
+
private _fetchCurrentPageDataTimeout?;
|
|
26
|
+
private _fetchCurrentPageDataPromises;
|
|
27
|
+
private _currentAbortController?;
|
|
28
|
+
private _fetchCurrentPageData;
|
|
29
|
+
private _fetchCurrentPageDataDebounced;
|
|
30
|
+
private _parseToSearchDefinition;
|
|
31
|
+
private _sortByName;
|
|
32
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { ReadSelectedComparisonOperator, ReadSelectedLogicalOperator, ReadSelectedOrderingDirection, ReadSelectedPropertyType, } from '../../data';
|
|
2
|
+
import { ApiReadControllerClient } from '../../services';
|
|
3
|
+
export class SearchAdapter {
|
|
4
|
+
_readClient;
|
|
5
|
+
_isLoading = false;
|
|
6
|
+
_searchResults = [];
|
|
7
|
+
_totalItemCount = 1000;
|
|
8
|
+
_searchTerm = '';
|
|
9
|
+
_selectedType;
|
|
10
|
+
_lastInputSearchTerm;
|
|
11
|
+
_lastInputType;
|
|
12
|
+
_searchTriggerLength;
|
|
13
|
+
_searchTriggerDelay;
|
|
14
|
+
_triggeredSearch = false;
|
|
15
|
+
_selection;
|
|
16
|
+
_multiSelectMode = true;
|
|
17
|
+
constructor(controllerName, multiSelect, searchTriggerLength, searchTriggerDelay, dataChangedCallback) {
|
|
18
|
+
this._readClient = new ApiReadControllerClient(controllerName);
|
|
19
|
+
this._searchTriggerLength = searchTriggerLength;
|
|
20
|
+
this._searchTriggerDelay = searchTriggerDelay;
|
|
21
|
+
this._multiSelectMode = multiSelect;
|
|
22
|
+
this._dataChangedCallback = dataChangedCallback;
|
|
23
|
+
this._fetchCurrentPageData();
|
|
24
|
+
}
|
|
25
|
+
//#region Public methods
|
|
26
|
+
getSelectedItems() {
|
|
27
|
+
return this._selection || [];
|
|
28
|
+
}
|
|
29
|
+
getSelectedType() {
|
|
30
|
+
return this._selectedType;
|
|
31
|
+
}
|
|
32
|
+
getSelectMode() {
|
|
33
|
+
return this._multiSelectMode;
|
|
34
|
+
}
|
|
35
|
+
addToSelection(item) {
|
|
36
|
+
if (!this._selection) {
|
|
37
|
+
this._selection = [];
|
|
38
|
+
}
|
|
39
|
+
if (this._multiSelectMode) {
|
|
40
|
+
if (!this._selection.find(s => s.id === item.id)) {
|
|
41
|
+
this._selection.push(item);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// single-select mode - replace the item and return the old one to shown results
|
|
46
|
+
if (this._selection.length > 0) {
|
|
47
|
+
this._searchResults = [...this._searchResults, ...this._selection];
|
|
48
|
+
}
|
|
49
|
+
this._selection = [item];
|
|
50
|
+
}
|
|
51
|
+
this._searchResults = this._searchResults.filter(d => d.id !== item.id);
|
|
52
|
+
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
53
|
+
}
|
|
54
|
+
removeFromSelection(item) {
|
|
55
|
+
if (!this._selection) {
|
|
56
|
+
this._selection = [];
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this._selection = this._selection.filter(s => s.id !== item.id);
|
|
60
|
+
this._searchResults = [...this._searchResults, item];
|
|
61
|
+
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
62
|
+
}
|
|
63
|
+
search(searchTerm, type) {
|
|
64
|
+
if (this._lastInputSearchTerm === searchTerm && this._lastInputType === type) {
|
|
65
|
+
// no changes to search condition
|
|
66
|
+
return Promise.resolve(this._searchResults);
|
|
67
|
+
}
|
|
68
|
+
this._lastInputSearchTerm = searchTerm;
|
|
69
|
+
this._lastInputType = type;
|
|
70
|
+
if (type !== undefined && type !== null) {
|
|
71
|
+
// has type selected -> single-select mode
|
|
72
|
+
if (searchTerm.length >= this._searchTriggerLength) {
|
|
73
|
+
// include search by string
|
|
74
|
+
this._searchTerm = searchTerm;
|
|
75
|
+
this._selectedType = type;
|
|
76
|
+
this._triggeredSearch = true;
|
|
77
|
+
return this._fetchCurrentPageData();
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// search all by type
|
|
81
|
+
this._searchTerm = '';
|
|
82
|
+
this._selectedType = type;
|
|
83
|
+
this._triggeredSearch = true;
|
|
84
|
+
return this._fetchCurrentPageData();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (searchTerm.length >= this._searchTriggerLength) {
|
|
88
|
+
// no type involved -> search by input string only
|
|
89
|
+
this._searchTerm = searchTerm;
|
|
90
|
+
this._selectedType = type;
|
|
91
|
+
this._triggeredSearch = true;
|
|
92
|
+
return this._fetchCurrentPageData();
|
|
93
|
+
}
|
|
94
|
+
else if (this._triggeredSearch && searchTerm.length < this._searchTriggerLength) {
|
|
95
|
+
// user has deleted part of the search term after having seen some results - fetch everything to show
|
|
96
|
+
this._searchTerm = '';
|
|
97
|
+
this._selectedType = type;
|
|
98
|
+
return this._fetchCurrentPageData();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// no type, searchTerm < this._searchTriggerLength
|
|
102
|
+
return Promise.resolve([]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
clearSearch() {
|
|
106
|
+
this._selection = [];
|
|
107
|
+
this._selectedType = undefined;
|
|
108
|
+
this._searchTerm = '';
|
|
109
|
+
this._dataChangedCallback(this._isLoading, [], undefined);
|
|
110
|
+
this._triggeredSearch = false;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region Private methods
|
|
114
|
+
_dataChangedCallback;
|
|
115
|
+
_fetchCurrentPageDataTimeout;
|
|
116
|
+
_fetchCurrentPageDataPromises = [];
|
|
117
|
+
_currentAbortController;
|
|
118
|
+
async _fetchCurrentPageData() {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
this._fetchCurrentPageDataPromises.push({ resolve, reject });
|
|
121
|
+
if (this._fetchCurrentPageDataTimeout !== undefined) {
|
|
122
|
+
clearTimeout(this._fetchCurrentPageDataTimeout);
|
|
123
|
+
}
|
|
124
|
+
this._fetchCurrentPageDataTimeout = setTimeout(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const result = await this._fetchCurrentPageDataDebounced();
|
|
127
|
+
const promises = this._fetchCurrentPageDataPromises.splice(0, this._fetchCurrentPageDataPromises.length);
|
|
128
|
+
for (const p of promises)
|
|
129
|
+
p.resolve(result);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const promises = this._fetchCurrentPageDataPromises.splice(0, this._fetchCurrentPageDataPromises.length);
|
|
133
|
+
for (const p of promises)
|
|
134
|
+
p.reject(err);
|
|
135
|
+
}
|
|
136
|
+
}, this._searchTriggerDelay);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async _fetchCurrentPageDataDebounced() {
|
|
140
|
+
this._isLoading = true;
|
|
141
|
+
this._dataChangedCallback(this._isLoading, undefined, undefined);
|
|
142
|
+
let abortController;
|
|
143
|
+
let caughtError;
|
|
144
|
+
try {
|
|
145
|
+
// Cancel any in-flight request before starting a new one
|
|
146
|
+
if (this._currentAbortController) {
|
|
147
|
+
try {
|
|
148
|
+
this._currentAbortController.abort();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Ignore abort errors as they are expected when debouncing
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
abortController = new AbortController();
|
|
155
|
+
this._currentAbortController = abortController;
|
|
156
|
+
const definition = this._parseToSearchDefinition();
|
|
157
|
+
const response = await this._readClient.readSelected(definition, abortController?.signal);
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw response.error;
|
|
160
|
+
}
|
|
161
|
+
// Filter out selected items from results
|
|
162
|
+
const selectedIds = new Set((this._selection || []).map(item => item.id));
|
|
163
|
+
this._searchResults = response.result.filter(item => !selectedIds.has(item.id));
|
|
164
|
+
return this._searchResults;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
caughtError = error;
|
|
168
|
+
// Do not log abort-related errors as they are expected when debouncing
|
|
169
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
170
|
+
return Promise.reject(error);
|
|
171
|
+
}
|
|
172
|
+
console.error('Error fetching data:', error);
|
|
173
|
+
return Promise.reject(error);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
this._isLoading = false;
|
|
177
|
+
let errorName;
|
|
178
|
+
if (caughtError && !(caughtError instanceof DOMException && caughtError.name === 'AbortError')) {
|
|
179
|
+
errorName = caughtError instanceof Error ? caughtError.name : undefined;
|
|
180
|
+
}
|
|
181
|
+
this._dataChangedCallback(this._isLoading, this._searchResults, errorName);
|
|
182
|
+
// Clear the abort controller only if it belongs to this request (avoid racing with a newer one)
|
|
183
|
+
if (abortController && this._currentAbortController === abortController) {
|
|
184
|
+
this._currentAbortController = undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
_parseToSearchDefinition() {
|
|
189
|
+
const definition = {
|
|
190
|
+
paginationDefinition: {
|
|
191
|
+
skip: 0,
|
|
192
|
+
limit: this._totalItemCount,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
// Apply ordering
|
|
196
|
+
definition.orderingDefinition = {
|
|
197
|
+
order: [
|
|
198
|
+
{
|
|
199
|
+
propertyPath: ['Name'],
|
|
200
|
+
direction: ReadSelectedOrderingDirection.Ascending,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
// Apply search
|
|
205
|
+
const propertyCriteria = [
|
|
206
|
+
{
|
|
207
|
+
propertyName: 'Name',
|
|
208
|
+
comparisonOperator: ReadSelectedComparisonOperator.IContains,
|
|
209
|
+
valueType: ReadSelectedPropertyType.String,
|
|
210
|
+
value: this._searchTerm,
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
if (this._selectedType !== undefined) {
|
|
214
|
+
propertyCriteria.push({
|
|
215
|
+
propertyName: 'Type',
|
|
216
|
+
comparisonOperator: ReadSelectedComparisonOperator.Equal,
|
|
217
|
+
valueType: ReadSelectedPropertyType.String,
|
|
218
|
+
value: this._selectedType,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
definition.searchDefinition = {
|
|
222
|
+
logicalOperator: ReadSelectedLogicalOperator.And,
|
|
223
|
+
propertyCriteria: propertyCriteria,
|
|
224
|
+
};
|
|
225
|
+
return definition;
|
|
226
|
+
}
|
|
227
|
+
_sortByName(array) {
|
|
228
|
+
return array.sort((a, b) => {
|
|
229
|
+
const nameA = a.name?.toLowerCase() || '';
|
|
230
|
+
const nameB = b.name?.toLowerCase() || '';
|
|
231
|
+
return nameA.localeCompare(nameB);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
package/dist/adapters/index.d.ts
CHANGED
package/dist/adapters/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellegens/cornerstone-client",
|
|
3
|
-
"version": "0.0.9999-alpha-
|
|
3
|
+
"version": "0.0.9999-alpha-17",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishable": true,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@eslint/js": "^9.21.0",
|
|
18
|
+
"@jest/globals": "^30.2.0",
|
|
18
19
|
"@types/express": "^5.0.0",
|
|
19
20
|
"@types/jest": "^29.5.12",
|
|
20
21
|
"@types/node": "^22.13.9",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"jest": "^29.7.0",
|
|
26
27
|
"jest-environment-jsdom": "^29.7.0",
|
|
27
28
|
"prettier": "^3.5.2",
|
|
29
|
+
"rxjs": "~7.8.0",
|
|
28
30
|
"ts-jest": "^29.1.2",
|
|
29
31
|
"tsc-alias": "^1.8.11",
|
|
30
32
|
"typescript-eslint": "^8.26.0",
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IIdentifiable,
|
|
3
|
+
ReadSelectedComparisonOperator,
|
|
4
|
+
ReadSelectedDefinitionDto,
|
|
5
|
+
ReadSelectedLogicalOperator,
|
|
6
|
+
ReadSelectedOrderingDirection,
|
|
7
|
+
ReadSelectedPropertyType,
|
|
8
|
+
ReadSelectedSearchDefinitionDto,
|
|
9
|
+
ReadSelectedSearchPropertyDefinitionDto,
|
|
10
|
+
} from '@data';
|
|
11
|
+
import { ApiReadControllerClient } from '@services';
|
|
12
|
+
|
|
13
|
+
export class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
14
|
+
private _readClient!: ApiReadControllerClient<TKey, TDto, TDto>;
|
|
15
|
+
|
|
16
|
+
private _isLoading = false;
|
|
17
|
+
private _searchResults: TDto[] = [];
|
|
18
|
+
private _totalItemCount: number = 1000;
|
|
19
|
+
|
|
20
|
+
private _searchTerm: string = '';
|
|
21
|
+
private _selectedType?: string;
|
|
22
|
+
private _lastInputSearchTerm?: string;
|
|
23
|
+
private _lastInputType?: string;
|
|
24
|
+
|
|
25
|
+
private _searchTriggerLength!: number;
|
|
26
|
+
private _searchTriggerDelay!: number;
|
|
27
|
+
private _triggeredSearch = false;
|
|
28
|
+
|
|
29
|
+
private _selection!: TDto[];
|
|
30
|
+
private _multiSelectMode = true;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
controllerName: string,
|
|
34
|
+
multiSelect: boolean,
|
|
35
|
+
searchTriggerLength: number,
|
|
36
|
+
searchTriggerDelay: number,
|
|
37
|
+
dataChangedCallback: (isLoading: boolean, data: TDto[] | undefined, error: string | undefined) => void,
|
|
38
|
+
) {
|
|
39
|
+
this._readClient = new ApiReadControllerClient<TKey, TDto, TDto>(controllerName);
|
|
40
|
+
|
|
41
|
+
this._searchTriggerLength = searchTriggerLength;
|
|
42
|
+
this._searchTriggerDelay = searchTriggerDelay;
|
|
43
|
+
this._multiSelectMode = multiSelect;
|
|
44
|
+
|
|
45
|
+
this._dataChangedCallback = dataChangedCallback;
|
|
46
|
+
this._fetchCurrentPageData();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#region Public methods
|
|
50
|
+
|
|
51
|
+
public getSelectedItems(): TDto[] {
|
|
52
|
+
return this._selection || [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public getSelectedType(): string | undefined {
|
|
56
|
+
return this._selectedType;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public getSelectMode(): boolean {
|
|
60
|
+
return this._multiSelectMode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public addToSelection(item: TDto): void {
|
|
64
|
+
if (!this._selection) {
|
|
65
|
+
this._selection = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this._multiSelectMode) {
|
|
69
|
+
if (!this._selection.find(s => s.id === item.id)) {
|
|
70
|
+
this._selection.push(item);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// single-select mode - replace the item and return the old one to shown results
|
|
74
|
+
if (this._selection.length > 0) {
|
|
75
|
+
this._searchResults = [...this._searchResults, ...this._selection];
|
|
76
|
+
}
|
|
77
|
+
this._selection = [item];
|
|
78
|
+
}
|
|
79
|
+
this._searchResults = this._searchResults.filter(d => d.id !== item.id);
|
|
80
|
+
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public removeFromSelection(item: TDto): void {
|
|
84
|
+
if (!this._selection) {
|
|
85
|
+
this._selection = [];
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._selection = this._selection.filter(s => s.id !== item.id);
|
|
90
|
+
this._searchResults = [...this._searchResults, item];
|
|
91
|
+
|
|
92
|
+
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public search(searchTerm: string, type?: string) {
|
|
96
|
+
if (this._lastInputSearchTerm === searchTerm && this._lastInputType === type) {
|
|
97
|
+
// no changes to search condition
|
|
98
|
+
return Promise.resolve(this._searchResults);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._lastInputSearchTerm = searchTerm;
|
|
102
|
+
this._lastInputType = type;
|
|
103
|
+
|
|
104
|
+
if (type !== undefined && type !== null) {
|
|
105
|
+
// has type selected -> single-select mode
|
|
106
|
+
if (searchTerm.length >= this._searchTriggerLength) {
|
|
107
|
+
// include search by string
|
|
108
|
+
this._searchTerm = searchTerm;
|
|
109
|
+
this._selectedType = type;
|
|
110
|
+
this._triggeredSearch = true;
|
|
111
|
+
return this._fetchCurrentPageData();
|
|
112
|
+
} else {
|
|
113
|
+
// search all by type
|
|
114
|
+
this._searchTerm = '';
|
|
115
|
+
this._selectedType = type;
|
|
116
|
+
this._triggeredSearch = true;
|
|
117
|
+
return this._fetchCurrentPageData();
|
|
118
|
+
}
|
|
119
|
+
} else if (searchTerm.length >= this._searchTriggerLength) {
|
|
120
|
+
// no type involved -> search by input string only
|
|
121
|
+
this._searchTerm = searchTerm;
|
|
122
|
+
this._selectedType = type;
|
|
123
|
+
this._triggeredSearch = true;
|
|
124
|
+
return this._fetchCurrentPageData();
|
|
125
|
+
} else if (this._triggeredSearch && searchTerm.length < this._searchTriggerLength) {
|
|
126
|
+
// user has deleted part of the search term after having seen some results - fetch everything to show
|
|
127
|
+
this._searchTerm = '';
|
|
128
|
+
this._selectedType = type;
|
|
129
|
+
return this._fetchCurrentPageData();
|
|
130
|
+
} else {
|
|
131
|
+
// no type, searchTerm < this._searchTriggerLength
|
|
132
|
+
return Promise.resolve([]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public clearSearch(): void {
|
|
137
|
+
this._selection = [];
|
|
138
|
+
this._selectedType = undefined;
|
|
139
|
+
this._searchTerm = '';
|
|
140
|
+
this._dataChangedCallback(this._isLoading, [], undefined);
|
|
141
|
+
this._triggeredSearch = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
|
|
146
|
+
//#region Private methods
|
|
147
|
+
|
|
148
|
+
private readonly _dataChangedCallback!: (isLoading: boolean, data: TDto[] | undefined, errorMessage: string | undefined) => void;
|
|
149
|
+
|
|
150
|
+
private _fetchCurrentPageDataTimeout?: any;
|
|
151
|
+
|
|
152
|
+
private _fetchCurrentPageDataPromises: {
|
|
153
|
+
resolve: (value: TDto[] | PromiseLike<TDto[]>) => void;
|
|
154
|
+
reject: (reason?: unknown) => void;
|
|
155
|
+
}[] = [];
|
|
156
|
+
|
|
157
|
+
private _currentAbortController?: AbortController;
|
|
158
|
+
|
|
159
|
+
private async _fetchCurrentPageData(): Promise<TDto[]> {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
this._fetchCurrentPageDataPromises.push({ resolve, reject });
|
|
162
|
+
if (this._fetchCurrentPageDataTimeout !== undefined) {
|
|
163
|
+
clearTimeout(this._fetchCurrentPageDataTimeout);
|
|
164
|
+
}
|
|
165
|
+
this._fetchCurrentPageDataTimeout = setTimeout(async () => {
|
|
166
|
+
try {
|
|
167
|
+
const result = await this._fetchCurrentPageDataDebounced();
|
|
168
|
+
const promises = this._fetchCurrentPageDataPromises.splice(0, this._fetchCurrentPageDataPromises.length);
|
|
169
|
+
for (const p of promises) p.resolve(result);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const promises = this._fetchCurrentPageDataPromises.splice(0, this._fetchCurrentPageDataPromises.length);
|
|
172
|
+
for (const p of promises) p.reject(err);
|
|
173
|
+
}
|
|
174
|
+
}, this._searchTriggerDelay);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async _fetchCurrentPageDataDebounced(): Promise<TDto[]> {
|
|
179
|
+
this._isLoading = true;
|
|
180
|
+
this._dataChangedCallback(this._isLoading, undefined, undefined);
|
|
181
|
+
let abortController: AbortController | undefined;
|
|
182
|
+
let caughtError: unknown;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Cancel any in-flight request before starting a new one
|
|
186
|
+
if (this._currentAbortController) {
|
|
187
|
+
try {
|
|
188
|
+
this._currentAbortController.abort();
|
|
189
|
+
} catch {
|
|
190
|
+
// Ignore abort errors as they are expected when debouncing
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
abortController = new AbortController();
|
|
194
|
+
this._currentAbortController = abortController;
|
|
195
|
+
|
|
196
|
+
const definition = this._parseToSearchDefinition();
|
|
197
|
+
const response = await this._readClient.readSelected(definition, abortController?.signal);
|
|
198
|
+
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
throw response.error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Filter out selected items from results
|
|
204
|
+
const selectedIds = new Set((this._selection || []).map(item => item.id));
|
|
205
|
+
this._searchResults = response.result.filter(item => !selectedIds.has(item.id));
|
|
206
|
+
|
|
207
|
+
return this._searchResults;
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
caughtError = error;
|
|
210
|
+
|
|
211
|
+
// Do not log abort-related errors as they are expected when debouncing
|
|
212
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
213
|
+
return Promise.reject(error);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.error('Error fetching data:', error);
|
|
217
|
+
return Promise.reject(error);
|
|
218
|
+
} finally {
|
|
219
|
+
this._isLoading = false;
|
|
220
|
+
|
|
221
|
+
let errorName: string | undefined;
|
|
222
|
+
if (caughtError && !(caughtError instanceof DOMException && caughtError.name === 'AbortError')) {
|
|
223
|
+
errorName = caughtError instanceof Error ? caughtError.name : undefined;
|
|
224
|
+
}
|
|
225
|
+
this._dataChangedCallback(this._isLoading, this._searchResults, errorName);
|
|
226
|
+
|
|
227
|
+
// Clear the abort controller only if it belongs to this request (avoid racing with a newer one)
|
|
228
|
+
|
|
229
|
+
if (abortController && this._currentAbortController === abortController) {
|
|
230
|
+
this._currentAbortController = undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private _parseToSearchDefinition(): ReadSelectedDefinitionDto<TDto> {
|
|
236
|
+
const definition: ReadSelectedDefinitionDto<TDto> = {
|
|
237
|
+
paginationDefinition: {
|
|
238
|
+
skip: 0,
|
|
239
|
+
limit: this._totalItemCount,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Apply ordering
|
|
244
|
+
definition.orderingDefinition = {
|
|
245
|
+
order: [
|
|
246
|
+
{
|
|
247
|
+
propertyPath: ['Name'],
|
|
248
|
+
direction: ReadSelectedOrderingDirection.Ascending,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Apply search
|
|
254
|
+
const propertyCriteria: ReadSelectedSearchPropertyDefinitionDto<TDto, keyof TDto>[] = [
|
|
255
|
+
{
|
|
256
|
+
propertyName: 'Name' as any,
|
|
257
|
+
comparisonOperator: ReadSelectedComparisonOperator.IContains,
|
|
258
|
+
valueType: ReadSelectedPropertyType.String,
|
|
259
|
+
value: this._searchTerm,
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
if (this._selectedType !== undefined) {
|
|
264
|
+
propertyCriteria.push({
|
|
265
|
+
propertyName: 'Type',
|
|
266
|
+
comparisonOperator: ReadSelectedComparisonOperator.Equal,
|
|
267
|
+
valueType: ReadSelectedPropertyType.String,
|
|
268
|
+
value: this._selectedType,
|
|
269
|
+
} as ReadSelectedSearchPropertyDefinitionDto<TDto, keyof TDto>);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
definition.searchDefinition = {
|
|
273
|
+
logicalOperator: ReadSelectedLogicalOperator.And,
|
|
274
|
+
propertyCriteria: propertyCriteria,
|
|
275
|
+
} as ReadSelectedSearchDefinitionDto<TDto>;
|
|
276
|
+
|
|
277
|
+
return definition;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private _sortByName(array: TDto[]): TDto[] {
|
|
281
|
+
return array.sort((a, b) => {
|
|
282
|
+
const nameA = (a as any).name?.toLowerCase() || '';
|
|
283
|
+
const nameB = (b as any).name?.toLowerCase() || '';
|
|
284
|
+
return nameA.localeCompare(nameB);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -87,7 +87,7 @@ describe('CollectionViewAdapter', () => {
|
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
const callback = jest.fn();
|
|
90
|
-
const adapter = new CollectionViewAdapter<number, TestDto>('ControllerName', callback, options);
|
|
90
|
+
const adapter = new CollectionViewAdapter<number, TestDto, TestDto>('ControllerName', callback, options);
|
|
91
91
|
|
|
92
92
|
return { adapter, callback };
|
|
93
93
|
};
|