@intellegens/cornerstone-client 0.0.9999-alpha-19 → 0.0.9999-alpha-21
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 +40 -23
- package/dist/adapters/SearchAdapter/index.js +104 -123
- package/dist/data/api/interface/ICommonIdentifiable.d.ts +7 -0
- package/dist/data/api/interface/ICommonIdentifiable.js +1 -0
- package/package.json +1 -1
- package/src/adapters/SearchAdapter/index.ts +124 -139
- package/src/data/api/interface/ICommonIdentifiable.ts +9 -0
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
import { IIdentifiable } from '../../data';
|
|
2
|
-
export
|
|
2
|
+
export type SearchAdapterOptions = {
|
|
3
|
+
controllerName: string;
|
|
4
|
+
typesToSearch: string[];
|
|
5
|
+
multiselect: boolean;
|
|
6
|
+
searchTriggerMinLength: number;
|
|
7
|
+
searchDebounceDelay: number;
|
|
8
|
+
limitSearchToSelectedType: boolean;
|
|
9
|
+
resultsLimit: number;
|
|
10
|
+
};
|
|
11
|
+
declare class SingularEventTarget<T> {
|
|
12
|
+
private _target;
|
|
13
|
+
addEventListener(callback: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean): void;
|
|
14
|
+
dispatchEvent(value: T): void;
|
|
15
|
+
}
|
|
16
|
+
export interface IGlobalSearchable<TKey> extends IIdentifiable<TKey> {
|
|
17
|
+
type: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
3
20
|
private _readClient;
|
|
21
|
+
private _options;
|
|
4
22
|
private _isLoading;
|
|
23
|
+
private _currentAbortController?;
|
|
24
|
+
constructor(options: SearchAdapterOptions);
|
|
25
|
+
onChange: SingularEventTarget<string>;
|
|
26
|
+
private _emitChange;
|
|
27
|
+
get multiselect(): boolean;
|
|
28
|
+
private _searchText;
|
|
29
|
+
get searchText(): string;
|
|
30
|
+
set searchText(searchText: string);
|
|
5
31
|
private _searchResults;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
|
|
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;
|
|
32
|
+
get searchResults(): TDto[];
|
|
33
|
+
protected set searchResults(searchResults: TDto[]);
|
|
34
|
+
private _selectedItems;
|
|
35
|
+
get selectedItems(): TDto[];
|
|
36
|
+
set selectedItems(selectedItems: TDto[]);
|
|
20
37
|
addToSelection(item: TDto): void;
|
|
21
38
|
removeFromSelection(item: TDto): void;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private _fetchCurrentPageDataDebounced;
|
|
39
|
+
private _lastSearchedValue;
|
|
40
|
+
private _typesToSearch;
|
|
41
|
+
private _trySearch;
|
|
42
|
+
private _fetchResultsDataTimeout?;
|
|
43
|
+
private _fetchResultsPromises;
|
|
44
|
+
private _fetchResults;
|
|
45
|
+
private _fetchResultsDebounced;
|
|
30
46
|
private _parseToSearchDefinition;
|
|
31
47
|
private _sortByName;
|
|
32
48
|
}
|
|
49
|
+
export {};
|
|
@@ -1,144 +1,122 @@
|
|
|
1
1
|
import { ReadSelectedComparisonOperator, ReadSelectedLogicalOperator, ReadSelectedOrderingDirection, ReadSelectedPropertyType, } from '../../data';
|
|
2
2
|
import { ApiReadControllerClient } from '../../services';
|
|
3
|
+
class SingularEventTarget {
|
|
4
|
+
_target = new EventTarget();
|
|
5
|
+
addEventListener(callback, options) {
|
|
6
|
+
this._target.addEventListener('change', callback, options);
|
|
7
|
+
}
|
|
8
|
+
dispatchEvent(value) {
|
|
9
|
+
this._target.dispatchEvent(new CustomEvent('change', { detail: value }));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
export class SearchAdapter {
|
|
13
|
+
// inputs
|
|
4
14
|
_readClient;
|
|
15
|
+
_options;
|
|
5
16
|
_isLoading = false;
|
|
17
|
+
_currentAbortController;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this._options = options;
|
|
20
|
+
this._readClient = new ApiReadControllerClient(options.controllerName);
|
|
21
|
+
}
|
|
22
|
+
onChange = new SingularEventTarget();
|
|
23
|
+
_emitChange() {
|
|
24
|
+
this.onChange.dispatchEvent('onChange');
|
|
25
|
+
}
|
|
26
|
+
get multiselect() {
|
|
27
|
+
return this._options.multiselect;
|
|
28
|
+
}
|
|
29
|
+
_searchText = '';
|
|
30
|
+
get searchText() {
|
|
31
|
+
return this._searchText;
|
|
32
|
+
}
|
|
33
|
+
set searchText(searchText) {
|
|
34
|
+
this._searchText = searchText;
|
|
35
|
+
this._trySearch();
|
|
36
|
+
this._emitChange();
|
|
37
|
+
}
|
|
6
38
|
_searchResults = [];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
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;
|
|
39
|
+
get searchResults() {
|
|
40
|
+
return this._searchResults;
|
|
41
|
+
}
|
|
42
|
+
set searchResults(searchResults) {
|
|
43
|
+
this._searchResults = [...searchResults];
|
|
44
|
+
this._emitChange();
|
|
45
|
+
}
|
|
46
|
+
_selectedItems = [];
|
|
47
|
+
get selectedItems() {
|
|
48
|
+
return this._selectedItems;
|
|
49
|
+
}
|
|
50
|
+
set selectedItems(selectedItems) {
|
|
51
|
+
this._selectedItems = [...selectedItems];
|
|
52
|
+
this._emitChange();
|
|
34
53
|
}
|
|
35
54
|
addToSelection(item) {
|
|
36
|
-
if (!this.
|
|
37
|
-
this.
|
|
55
|
+
if (!this._selectedItems) {
|
|
56
|
+
this.selectedItems = [];
|
|
38
57
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
// selection mode
|
|
59
|
+
if (this._options.multiselect) {
|
|
60
|
+
if (!this._selectedItems.find(s => s.id === item.id)) {
|
|
61
|
+
this.selectedItems = [...this.selectedItems, item];
|
|
42
62
|
}
|
|
43
63
|
}
|
|
44
64
|
else {
|
|
45
65
|
// single-select mode - replace the item and return the old one to shown results
|
|
46
|
-
if (this.
|
|
47
|
-
this.
|
|
66
|
+
if (this._selectedItems.length > 0) {
|
|
67
|
+
this.searchResults = this._sortByName([...this._searchResults, ...this._selectedItems]); // return the to-be-replaced selected item back to shown results list
|
|
48
68
|
}
|
|
49
|
-
this.
|
|
69
|
+
this.selectedItems = [item];
|
|
50
70
|
}
|
|
51
|
-
this.
|
|
52
|
-
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
71
|
+
this.searchResults = this._sortByName(this._searchResults.filter(d => d.id !== item.id)); // remove the new selection from the shown results
|
|
53
72
|
}
|
|
54
73
|
removeFromSelection(item) {
|
|
55
|
-
if (!this.
|
|
56
|
-
this.
|
|
74
|
+
if (!this._selectedItems) {
|
|
75
|
+
this.selectedItems = [];
|
|
57
76
|
return;
|
|
58
77
|
}
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
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
|
-
}
|
|
78
|
+
this.selectedItems = this._selectedItems.filter(s => s.id !== item.id);
|
|
79
|
+
this.searchResults = this._sortByName([...this._searchResults, item]);
|
|
104
80
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
81
|
+
_lastSearchedValue = '';
|
|
82
|
+
_typesToSearch = [];
|
|
83
|
+
async _trySearch() {
|
|
84
|
+
const options = this._options;
|
|
85
|
+
this.searchResults = [];
|
|
86
|
+
if (this._searchText.length < options.searchTriggerMinLength)
|
|
87
|
+
return;
|
|
88
|
+
if (this._searchText == this._lastSearchedValue)
|
|
89
|
+
return;
|
|
90
|
+
this._lastSearchedValue = this._searchText;
|
|
91
|
+
this._typesToSearch =
|
|
92
|
+
options.limitSearchToSelectedType && this._selectedItems.length ? [this._selectedItems[0].type] : this._options.typesToSearch;
|
|
93
|
+
this.searchResults = this._sortByName(await this._fetchResults(options.searchDebounceDelay));
|
|
94
|
+
}
|
|
95
|
+
_fetchResultsDataTimeout;
|
|
96
|
+
_fetchResultsPromises = [];
|
|
97
|
+
async _fetchResults(debounceDelay) {
|
|
119
98
|
return new Promise((resolve, reject) => {
|
|
120
|
-
this.
|
|
121
|
-
if (this.
|
|
122
|
-
clearTimeout(this.
|
|
99
|
+
this._fetchResultsPromises.push({ resolve, reject });
|
|
100
|
+
if (this._fetchResultsDataTimeout !== undefined) {
|
|
101
|
+
clearTimeout(this._fetchResultsDataTimeout);
|
|
123
102
|
}
|
|
124
|
-
this.
|
|
103
|
+
this._fetchResultsDataTimeout = setTimeout(async () => {
|
|
125
104
|
try {
|
|
126
|
-
const result = await this.
|
|
127
|
-
const promises = this.
|
|
105
|
+
const result = await this._fetchResultsDebounced();
|
|
106
|
+
const promises = this._fetchResultsPromises.splice(0, this._fetchResultsPromises.length);
|
|
128
107
|
for (const p of promises)
|
|
129
108
|
p.resolve(result);
|
|
130
109
|
}
|
|
131
110
|
catch (err) {
|
|
132
|
-
const promises = this.
|
|
111
|
+
const promises = this._fetchResultsPromises.splice(0, this._fetchResultsPromises.length);
|
|
133
112
|
for (const p of promises)
|
|
134
113
|
p.reject(err);
|
|
135
114
|
}
|
|
136
|
-
},
|
|
115
|
+
}, debounceDelay);
|
|
137
116
|
});
|
|
138
117
|
}
|
|
139
|
-
async
|
|
118
|
+
async _fetchResultsDebounced() {
|
|
140
119
|
this._isLoading = true;
|
|
141
|
-
this._dataChangedCallback(this._isLoading, undefined, undefined);
|
|
142
120
|
let abortController;
|
|
143
121
|
let caughtError;
|
|
144
122
|
try {
|
|
@@ -158,10 +136,9 @@ export class SearchAdapter {
|
|
|
158
136
|
if (!response.ok) {
|
|
159
137
|
throw response.error;
|
|
160
138
|
}
|
|
161
|
-
// Filter out selected items from results
|
|
162
|
-
const selectedIds = new Set((this.
|
|
163
|
-
|
|
164
|
-
return this._searchResults;
|
|
139
|
+
// Filter out selected items from new results
|
|
140
|
+
const selectedIds = new Set((this.selectedItems || []).map(item => item.id));
|
|
141
|
+
return response.result.filter(item => !selectedIds.has(item.id));
|
|
165
142
|
}
|
|
166
143
|
catch (error) {
|
|
167
144
|
caughtError = error;
|
|
@@ -178,7 +155,6 @@ export class SearchAdapter {
|
|
|
178
155
|
if (caughtError && !(caughtError instanceof DOMException && caughtError.name === 'AbortError')) {
|
|
179
156
|
errorName = caughtError instanceof Error ? caughtError.name : undefined;
|
|
180
157
|
}
|
|
181
|
-
this._dataChangedCallback(this._isLoading, this._searchResults, errorName);
|
|
182
158
|
// Clear the abort controller only if it belongs to this request (avoid racing with a newer one)
|
|
183
159
|
if (abortController && this._currentAbortController === abortController) {
|
|
184
160
|
this._currentAbortController = undefined;
|
|
@@ -189,7 +165,7 @@ export class SearchAdapter {
|
|
|
189
165
|
const definition = {
|
|
190
166
|
paginationDefinition: {
|
|
191
167
|
skip: 0,
|
|
192
|
-
limit: this.
|
|
168
|
+
limit: this._options.resultsLimit,
|
|
193
169
|
},
|
|
194
170
|
};
|
|
195
171
|
// Apply ordering
|
|
@@ -207,21 +183,26 @@ export class SearchAdapter {
|
|
|
207
183
|
propertyName: 'Name',
|
|
208
184
|
comparisonOperator: ReadSelectedComparisonOperator.IContains,
|
|
209
185
|
valueType: ReadSelectedPropertyType.String,
|
|
210
|
-
value: this.
|
|
186
|
+
value: this._searchText,
|
|
211
187
|
},
|
|
212
188
|
];
|
|
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
189
|
definition.searchDefinition = {
|
|
222
190
|
logicalOperator: ReadSelectedLogicalOperator.And,
|
|
223
191
|
propertyCriteria: propertyCriteria,
|
|
224
192
|
};
|
|
193
|
+
if (this._typesToSearch !== undefined && this._typesToSearch.length > 0) {
|
|
194
|
+
definition.searchDefinition.searches = [
|
|
195
|
+
{
|
|
196
|
+
logicalOperator: ReadSelectedLogicalOperator.Or,
|
|
197
|
+
propertyCriteria: this._typesToSearch.map(type => ({
|
|
198
|
+
propertyName: 'Type',
|
|
199
|
+
comparisonOperator: ReadSelectedComparisonOperator.IEqual,
|
|
200
|
+
valueType: ReadSelectedPropertyType.String,
|
|
201
|
+
value: type,
|
|
202
|
+
})),
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
225
206
|
return definition;
|
|
226
207
|
}
|
|
227
208
|
_sortByName(array) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -10,174 +10,157 @@ import {
|
|
|
10
10
|
} from '@data';
|
|
11
11
|
import { ApiReadControllerClient } from '@services';
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export type SearchAdapterOptions = {
|
|
14
|
+
controllerName: string;
|
|
15
|
+
typesToSearch: string[]; // provide known searchable types
|
|
16
|
+
multiselect: boolean;
|
|
17
|
+
searchTriggerMinLength: number;
|
|
18
|
+
searchDebounceDelay: number;
|
|
19
|
+
limitSearchToSelectedType: boolean; // when item is selected, limit results to items of same type
|
|
20
|
+
resultsLimit: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class SingularEventTarget<T> {
|
|
24
|
+
private _target = new EventTarget();
|
|
25
|
+
|
|
26
|
+
public addEventListener(callback: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean): void {
|
|
27
|
+
this._target.addEventListener('change', callback, options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public dispatchEvent(value: T) {
|
|
31
|
+
this._target.dispatchEvent(new CustomEvent('change', { detail: value }));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IGlobalSearchable<TKey> extends IIdentifiable<TKey> {
|
|
36
|
+
type: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
40
|
+
// inputs
|
|
14
41
|
private _readClient!: ApiReadControllerClient<TKey, TDto, TDto>;
|
|
42
|
+
private _options!: SearchAdapterOptions;
|
|
15
43
|
|
|
16
44
|
private _isLoading = false;
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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();
|
|
45
|
+
private _currentAbortController?: AbortController;
|
|
46
|
+
|
|
47
|
+
constructor(options: SearchAdapterOptions) {
|
|
48
|
+
this._options = options;
|
|
49
|
+
this._readClient = new ApiReadControllerClient<TKey, TDto, TDto>(options.controllerName);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
public onChange = new SingularEventTarget<string>();
|
|
53
|
+
private _emitChange() {
|
|
54
|
+
this.onChange.dispatchEvent('onChange');
|
|
55
|
+
}
|
|
50
56
|
|
|
51
|
-
public
|
|
52
|
-
return this.
|
|
57
|
+
public get multiselect() {
|
|
58
|
+
return this._options.multiselect;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
private _searchText: string = '';
|
|
62
|
+
public get searchText() {
|
|
63
|
+
return this._searchText;
|
|
64
|
+
}
|
|
65
|
+
public set searchText(searchText: string) {
|
|
66
|
+
this._searchText = searchText;
|
|
67
|
+
this._trySearch();
|
|
68
|
+
this._emitChange();
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
private _searchResults: TDto[] = [];
|
|
72
|
+
public get searchResults() {
|
|
73
|
+
return this._searchResults;
|
|
74
|
+
}
|
|
75
|
+
protected set searchResults(searchResults: TDto[]) {
|
|
76
|
+
this._searchResults = [...searchResults];
|
|
77
|
+
this._emitChange();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private _selectedItems: TDto[] = [];
|
|
81
|
+
public get selectedItems() {
|
|
82
|
+
return this._selectedItems;
|
|
83
|
+
}
|
|
84
|
+
public set selectedItems(selectedItems: TDto[]) {
|
|
85
|
+
this._selectedItems = [...selectedItems];
|
|
86
|
+
this._emitChange();
|
|
61
87
|
}
|
|
62
88
|
|
|
63
89
|
public addToSelection(item: TDto): void {
|
|
64
|
-
if (!this.
|
|
65
|
-
this.
|
|
90
|
+
if (!this._selectedItems) {
|
|
91
|
+
this.selectedItems = [];
|
|
66
92
|
}
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
// selection mode
|
|
95
|
+
if (this._options.multiselect) {
|
|
96
|
+
if (!this._selectedItems.find(s => s.id === item.id)) {
|
|
97
|
+
this.selectedItems = [...this.selectedItems, item];
|
|
71
98
|
}
|
|
72
99
|
} else {
|
|
73
100
|
// single-select mode - replace the item and return the old one to shown results
|
|
74
|
-
if (this.
|
|
75
|
-
this.
|
|
101
|
+
if (this._selectedItems.length > 0) {
|
|
102
|
+
this.searchResults = this._sortByName([...this._searchResults, ...this._selectedItems]); // return the to-be-replaced selected item back to shown results list
|
|
76
103
|
}
|
|
77
|
-
this.
|
|
104
|
+
this.selectedItems = [item];
|
|
78
105
|
}
|
|
79
|
-
this.
|
|
80
|
-
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
106
|
+
this.searchResults = this._sortByName(this._searchResults.filter(d => d.id !== item.id)); // remove the new selection from the shown results
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
public removeFromSelection(item: TDto): void {
|
|
84
|
-
if (!this.
|
|
85
|
-
this.
|
|
110
|
+
if (!this._selectedItems) {
|
|
111
|
+
this.selectedItems = [];
|
|
86
112
|
return;
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
this.
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
this._dataChangedCallback(this._isLoading, this._sortByName(this._searchResults), undefined);
|
|
115
|
+
this.selectedItems = this._selectedItems.filter(s => s.id !== item.id);
|
|
116
|
+
this.searchResults = this._sortByName([...this._searchResults, item]);
|
|
93
117
|
}
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
}
|
|
119
|
+
private _lastSearchedValue = '';
|
|
120
|
+
private _typesToSearch: string[] = [];
|
|
135
121
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this._selectedType = undefined;
|
|
139
|
-
this._searchTerm = '';
|
|
140
|
-
this._dataChangedCallback(this._isLoading, [], undefined);
|
|
141
|
-
this._triggeredSearch = false;
|
|
142
|
-
}
|
|
122
|
+
private async _trySearch() {
|
|
123
|
+
const options = this._options;
|
|
143
124
|
|
|
144
|
-
|
|
125
|
+
this.searchResults = [];
|
|
126
|
+
if (this._searchText.length < options.searchTriggerMinLength) return;
|
|
127
|
+
if (this._searchText == this._lastSearchedValue) return;
|
|
128
|
+
this._lastSearchedValue = this._searchText;
|
|
145
129
|
|
|
146
|
-
|
|
130
|
+
this._typesToSearch =
|
|
131
|
+
options.limitSearchToSelectedType && this._selectedItems.length ? [(this._selectedItems[0] as TDto).type] : this._options.typesToSearch;
|
|
147
132
|
|
|
148
|
-
|
|
133
|
+
this.searchResults = this._sortByName(await this._fetchResults(options.searchDebounceDelay));
|
|
134
|
+
}
|
|
149
135
|
|
|
150
|
-
private
|
|
136
|
+
private _fetchResultsDataTimeout?: any;
|
|
151
137
|
|
|
152
|
-
private
|
|
138
|
+
private _fetchResultsPromises: {
|
|
153
139
|
resolve: (value: TDto[] | PromiseLike<TDto[]>) => void;
|
|
154
140
|
reject: (reason?: unknown) => void;
|
|
155
141
|
}[] = [];
|
|
156
142
|
|
|
157
|
-
private
|
|
158
|
-
|
|
159
|
-
private async _fetchCurrentPageData(): Promise<TDto[]> {
|
|
143
|
+
private async _fetchResults(debounceDelay: number): Promise<TDto[]> {
|
|
160
144
|
return new Promise((resolve, reject) => {
|
|
161
|
-
this.
|
|
162
|
-
if (this.
|
|
163
|
-
clearTimeout(this.
|
|
145
|
+
this._fetchResultsPromises.push({ resolve, reject });
|
|
146
|
+
if (this._fetchResultsDataTimeout !== undefined) {
|
|
147
|
+
clearTimeout(this._fetchResultsDataTimeout);
|
|
164
148
|
}
|
|
165
|
-
this.
|
|
149
|
+
this._fetchResultsDataTimeout = setTimeout(async () => {
|
|
166
150
|
try {
|
|
167
|
-
const result = await this.
|
|
168
|
-
const promises = this.
|
|
151
|
+
const result = await this._fetchResultsDebounced();
|
|
152
|
+
const promises = this._fetchResultsPromises.splice(0, this._fetchResultsPromises.length);
|
|
169
153
|
for (const p of promises) p.resolve(result);
|
|
170
154
|
} catch (err) {
|
|
171
|
-
const promises = this.
|
|
155
|
+
const promises = this._fetchResultsPromises.splice(0, this._fetchResultsPromises.length);
|
|
172
156
|
for (const p of promises) p.reject(err);
|
|
173
157
|
}
|
|
174
|
-
},
|
|
158
|
+
}, debounceDelay);
|
|
175
159
|
});
|
|
176
160
|
}
|
|
177
161
|
|
|
178
|
-
private async
|
|
162
|
+
private async _fetchResultsDebounced(): Promise<TDto[]> {
|
|
179
163
|
this._isLoading = true;
|
|
180
|
-
this._dataChangedCallback(this._isLoading, undefined, undefined);
|
|
181
164
|
let abortController: AbortController | undefined;
|
|
182
165
|
let caughtError: unknown;
|
|
183
166
|
|
|
@@ -200,11 +183,9 @@ export class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
|
200
183
|
throw response.error;
|
|
201
184
|
}
|
|
202
185
|
|
|
203
|
-
// Filter out selected items from results
|
|
204
|
-
const selectedIds = new Set((this.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return this._searchResults;
|
|
186
|
+
// Filter out selected items from new results
|
|
187
|
+
const selectedIds = new Set((this.selectedItems || []).map(item => item.id));
|
|
188
|
+
return response.result.filter(item => !selectedIds.has(item.id));
|
|
208
189
|
} catch (error: unknown) {
|
|
209
190
|
caughtError = error;
|
|
210
191
|
|
|
@@ -222,10 +203,8 @@ export class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
|
222
203
|
if (caughtError && !(caughtError instanceof DOMException && caughtError.name === 'AbortError')) {
|
|
223
204
|
errorName = caughtError instanceof Error ? caughtError.name : undefined;
|
|
224
205
|
}
|
|
225
|
-
this._dataChangedCallback(this._isLoading, this._searchResults, errorName);
|
|
226
206
|
|
|
227
207
|
// Clear the abort controller only if it belongs to this request (avoid racing with a newer one)
|
|
228
|
-
|
|
229
208
|
if (abortController && this._currentAbortController === abortController) {
|
|
230
209
|
this._currentAbortController = undefined;
|
|
231
210
|
}
|
|
@@ -236,7 +215,7 @@ export class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
|
236
215
|
const definition: ReadSelectedDefinitionDto<TDto> = {
|
|
237
216
|
paginationDefinition: {
|
|
238
217
|
skip: 0,
|
|
239
|
-
limit: this.
|
|
218
|
+
limit: this._options.resultsLimit,
|
|
240
219
|
},
|
|
241
220
|
};
|
|
242
221
|
|
|
@@ -256,24 +235,32 @@ export class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
|
256
235
|
propertyName: 'Name' as any,
|
|
257
236
|
comparisonOperator: ReadSelectedComparisonOperator.IContains,
|
|
258
237
|
valueType: ReadSelectedPropertyType.String,
|
|
259
|
-
value: this.
|
|
238
|
+
value: this._searchText,
|
|
260
239
|
},
|
|
261
240
|
];
|
|
262
241
|
|
|
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
242
|
definition.searchDefinition = {
|
|
273
243
|
logicalOperator: ReadSelectedLogicalOperator.And,
|
|
274
244
|
propertyCriteria: propertyCriteria,
|
|
275
245
|
} as ReadSelectedSearchDefinitionDto<TDto>;
|
|
276
246
|
|
|
247
|
+
if (this._typesToSearch !== undefined && this._typesToSearch.length > 0) {
|
|
248
|
+
definition.searchDefinition.searches = [
|
|
249
|
+
{
|
|
250
|
+
logicalOperator: ReadSelectedLogicalOperator.Or,
|
|
251
|
+
propertyCriteria: this._typesToSearch.map(
|
|
252
|
+
type =>
|
|
253
|
+
({
|
|
254
|
+
propertyName: 'Type',
|
|
255
|
+
comparisonOperator: ReadSelectedComparisonOperator.IEqual,
|
|
256
|
+
valueType: ReadSelectedPropertyType.String,
|
|
257
|
+
value: type,
|
|
258
|
+
}) as ReadSelectedSearchPropertyDefinitionDto<TDto, keyof TDto>,
|
|
259
|
+
),
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
|
|
277
264
|
return definition;
|
|
278
265
|
}
|
|
279
266
|
|
|
@@ -284,6 +271,4 @@ export class SearchAdapter<TKey, TDto extends IIdentifiable<TKey>> {
|
|
|
284
271
|
return nameA.localeCompare(nameB);
|
|
285
272
|
});
|
|
286
273
|
}
|
|
287
|
-
|
|
288
|
-
//#endregion
|
|
289
274
|
}
|