@stream-io/feeds-client 0.2.2 → 0.2.3
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 +8 -0
- package/dist/@react-bindings/contexts/StreamSearchContext.d.ts +1 -1
- package/dist/@react-bindings/contexts/StreamSearchResultsContext.d.ts +1 -1
- package/dist/@react-bindings/hooks/search-state-hooks/useSearchQuery.d.ts +1 -1
- package/dist/@react-bindings/hooks/search-state-hooks/useSearchResult.d.ts +1 -1
- package/dist/@react-bindings/hooks/search-state-hooks/useSearchSources.d.ts +2 -2
- package/dist/@react-bindings/wrappers/StreamSearch.d.ts +1 -1
- package/dist/@react-bindings/wrappers/StreamSearchResults.d.ts +1 -1
- package/dist/index-react-bindings.browser.cjs +26 -8
- package/dist/index-react-bindings.browser.cjs.map +1 -1
- package/dist/index-react-bindings.browser.js +26 -8
- package/dist/index-react-bindings.browser.js.map +1 -1
- package/dist/index-react-bindings.node.cjs +26 -8
- package/dist/index-react-bindings.node.cjs.map +1 -1
- package/dist/index-react-bindings.node.js +26 -8
- package/dist/index-react-bindings.node.js.map +1 -1
- package/dist/index.browser.cjs +242 -170
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +242 -171
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.node.cjs +242 -170
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +242 -171
- package/dist/index.node.js.map +1 -1
- package/dist/src/common/{ActivitySearchSource.d.ts → search/ActivitySearchSource.d.ts} +3 -3
- package/dist/src/common/{BaseSearchSource.d.ts → search/BaseSearchSource.d.ts} +41 -35
- package/dist/src/common/{FeedSearchSource.d.ts → search/FeedSearchSource.d.ts} +3 -3
- package/dist/src/common/{SearchController.d.ts → search/SearchController.d.ts} +1 -3
- package/dist/src/common/{UserSearchSource.d.ts → search/UserSearchSource.d.ts} +4 -4
- package/dist/src/common/search/index.d.ts +6 -0
- package/dist/src/common/search/types.d.ts +22 -0
- package/dist/src/common/types.d.ts +1 -0
- package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +5 -12
- package/dist/src/gen/models/index.d.ts +58 -26
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -5
- package/package.json +1 -1
- package/src/common/{ActivitySearchSource.ts → search/ActivitySearchSource.ts} +3 -3
- package/src/common/{BaseSearchSource.ts → search/BaseSearchSource.ts} +137 -69
- package/src/common/{FeedSearchSource.ts → search/FeedSearchSource.ts} +3 -3
- package/src/common/{SearchController.ts → search/SearchController.ts} +2 -7
- package/src/common/{UserSearchSource.ts → search/UserSearchSource.ts} +3 -3
- package/src/common/search/index.ts +6 -0
- package/src/common/search/types.ts +21 -0
- package/src/common/types.ts +2 -0
- package/src/feed/event-handlers/activity/activity-utils.test.ts +2 -2
- package/src/feed/event-handlers/activity/handle-activity-added.test.ts +86 -0
- package/src/feed/event-handlers/activity/handle-activity-deleted.test.ts +117 -0
- package/src/feed/event-handlers/activity/handle-activity-deleted.ts +8 -4
- package/src/feed/event-handlers/feed-member/handle-feed-member-added.test.ts +75 -0
- package/src/feed/event-handlers/feed-member/handle-feed-member-removed.test.ts +82 -0
- package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +19 -9
- package/src/feed/event-handlers/feed-member/handle-feed-member-updated.test.ts +84 -0
- package/src/gen/feeds/FeedsApi.ts +6 -0
- package/src/gen/model-decoders/decoders.ts +13 -1
- package/src/gen/models/index.ts +90 -34
- package/src/test-utils/response-generators.ts +107 -0
- package/dist/src/test-utils/index.d.ts +0 -1
- package/dist/src/test-utils/response-generators.d.ts +0 -74
package/index.ts
CHANGED
|
@@ -4,10 +4,6 @@ export * from './src/gen/models';
|
|
|
4
4
|
export * from './src/types';
|
|
5
5
|
export * from './src/common/types';
|
|
6
6
|
export * from './src/common/StateStore';
|
|
7
|
-
export * from './src/common/
|
|
8
|
-
export * from './src/common/SearchController';
|
|
9
|
-
export * from './src/common/ActivitySearchSource';
|
|
10
|
-
export * from './src/common/UserSearchSource';
|
|
11
|
-
export * from './src/common/FeedSearchSource';
|
|
7
|
+
export * from './src/common/search';
|
|
12
8
|
export * from './src/common/Poll';
|
|
13
9
|
export * from './src/utils';
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BaseSearchSource } from './BaseSearchSource';
|
|
2
|
-
import type { SearchSourceOptions } from './
|
|
2
|
+
import type { SearchSourceOptions } from './types';
|
|
3
3
|
|
|
4
|
-
import { FeedsClient } from '
|
|
5
|
-
import { ActivityResponse } from '
|
|
4
|
+
import { FeedsClient } from '../../feeds-client';
|
|
5
|
+
import { ActivityResponse } from '../../gen/models';
|
|
6
6
|
|
|
7
7
|
export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
|
|
8
8
|
readonly type = 'activity' as const;
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { StateStore } from '
|
|
2
|
-
import { debounce, type DebouncedFunc } from '
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { StateStore } from '../StateStore';
|
|
2
|
+
import { debounce, type DebouncedFunc } from '../utils';
|
|
3
|
+
import type {
|
|
4
|
+
QueryReturnValue,
|
|
5
|
+
SearchSourceOptions,
|
|
6
|
+
SearchSourceState,
|
|
7
|
+
SearchSourceType,
|
|
8
|
+
} from './types';
|
|
7
9
|
|
|
8
10
|
export type DebounceOptions = {
|
|
9
11
|
debounceMs: number;
|
|
10
12
|
};
|
|
11
|
-
type DebouncedExecQueryFunction = DebouncedFunc<
|
|
12
|
-
(searchString?: string) => Promise<void>
|
|
13
|
-
>;
|
|
13
|
+
type DebouncedExecQueryFunction = DebouncedFunc<(searchString?: string) => Promise<void>>;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface ISearchSource<T = any> {
|
|
16
16
|
activate(): void;
|
|
17
17
|
|
|
18
|
-
cancelScheduledQuery(): void;
|
|
19
|
-
|
|
20
18
|
canExecuteQuery(newSearchString?: string): boolean;
|
|
21
19
|
|
|
22
20
|
deactivate(): void;
|
|
@@ -33,54 +31,44 @@ export interface SearchSource<T = any> {
|
|
|
33
31
|
|
|
34
32
|
resetState(): void;
|
|
35
33
|
|
|
36
|
-
search(text?: string): Promise<void> | undefined;
|
|
37
|
-
|
|
38
34
|
readonly searchQuery: string;
|
|
39
35
|
|
|
40
|
-
setDebounceOptions(options: DebounceOptions): void;
|
|
41
|
-
|
|
42
36
|
readonly state: StateStore<SearchSourceState<T>>;
|
|
43
37
|
readonly type: SearchSourceType;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
debounceMs?: number;
|
|
59
|
-
pageSize?: number;
|
|
60
|
-
allowEmptySearchString?: boolean;
|
|
61
|
-
};
|
|
40
|
+
export interface SearchSource<T = any> extends ISearchSource<T> {
|
|
41
|
+
cancelScheduledQuery(): void;
|
|
42
|
+
setDebounceOptions(options: DebounceOptions): void;
|
|
43
|
+
search(text?: string): Promise<void> | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SearchSourceSync<T = any> extends ISearchSource<T> {
|
|
47
|
+
cancelScheduledQuery(): void;
|
|
48
|
+
setDebounceOptions(options: DebounceOptions): void;
|
|
49
|
+
search(text?: string): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
62
52
|
const DEFAULT_SEARCH_SOURCE_OPTIONS: Required<SearchSourceOptions> = {
|
|
63
53
|
debounceMs: 300,
|
|
64
54
|
pageSize: 10,
|
|
65
55
|
allowEmptySearchString: false,
|
|
56
|
+
resetOnNewSearchQuery: true,
|
|
66
57
|
} as const;
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
abstract class BaseSearchSourceBase<T> implements ISearchSource<T> {
|
|
69
60
|
state: StateStore<SearchSourceState<T>>;
|
|
70
|
-
|
|
61
|
+
pageSize: number;
|
|
71
62
|
protected allowEmptySearchString: boolean;
|
|
63
|
+
protected resetOnNewSearchQuery: boolean;
|
|
72
64
|
abstract readonly type: SearchSourceType;
|
|
73
|
-
protected searchDebounced!: DebouncedExecQueryFunction;
|
|
74
65
|
|
|
75
66
|
protected constructor(options?: SearchSourceOptions) {
|
|
76
|
-
const {
|
|
77
|
-
...DEFAULT_SEARCH_SOURCE_OPTIONS,
|
|
78
|
-
...options,
|
|
79
|
-
};
|
|
67
|
+
const { pageSize, allowEmptySearchString, resetOnNewSearchQuery } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
|
|
80
68
|
this.pageSize = pageSize;
|
|
81
69
|
this.allowEmptySearchString = allowEmptySearchString;
|
|
70
|
+
this.resetOnNewSearchQuery = resetOnNewSearchQuery;
|
|
82
71
|
this.state = new StateStore<SearchSourceState<T>>(this.initialState);
|
|
83
|
-
this.setDebounceOptions({ debounceMs });
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
get lastQueryError() {
|
|
@@ -132,14 +120,6 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
|
|
|
132
120
|
return this.state.getLatestValue().searchQuery;
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
|
|
136
|
-
|
|
137
|
-
protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
|
|
138
|
-
|
|
139
|
-
setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
|
|
140
|
-
this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
123
|
activate = () => {
|
|
144
124
|
if (this.isActive) return;
|
|
145
125
|
this.state.partialNext({ isActive: true });
|
|
@@ -161,13 +141,16 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
|
|
|
161
141
|
);
|
|
162
142
|
};
|
|
163
143
|
|
|
164
|
-
protected getStateBeforeFirstQuery(
|
|
165
|
-
|
|
166
|
-
|
|
144
|
+
protected getStateBeforeFirstQuery(newSearchString: string): SearchSourceState<T> {
|
|
145
|
+
const initialState = this.initialState;
|
|
146
|
+
const oldItems = this.items;
|
|
147
|
+
|
|
148
|
+
const items = this.resetOnNewSearchQuery ? initialState.items : oldItems;
|
|
167
149
|
return {
|
|
168
150
|
...this.initialState,
|
|
151
|
+
items,
|
|
169
152
|
isActive: this.isActive,
|
|
170
|
-
isLoading: true,
|
|
153
|
+
isLoading: this.resetOnNewSearchQuery ? true : !oldItems,
|
|
171
154
|
searchQuery: newSearchString,
|
|
172
155
|
};
|
|
173
156
|
}
|
|
@@ -184,12 +167,11 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
|
|
|
184
167
|
isLoading: false,
|
|
185
168
|
items: isFirstPage
|
|
186
169
|
? stateUpdate.items
|
|
187
|
-
: [...(this.items ?? []), ...(stateUpdate.items
|
|
170
|
+
: [...(this.items ?? []), ...(stateUpdate.items || [])],
|
|
188
171
|
};
|
|
189
172
|
}
|
|
190
173
|
|
|
191
|
-
|
|
192
|
-
if (!this.canExecuteQuery(newSearchString)) return;
|
|
174
|
+
protected prepareStateForQuery(newSearchString?: string) {
|
|
193
175
|
const hasNewSearchQuery = typeof newSearchString !== 'undefined';
|
|
194
176
|
const searchString = newSearchString ?? this.searchQuery;
|
|
195
177
|
|
|
@@ -199,20 +181,67 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
|
|
|
199
181
|
this.state.partialNext({ isLoading: true });
|
|
200
182
|
}
|
|
201
183
|
|
|
184
|
+
return { searchString, hasNewSearchQuery };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
protected updatePaginationStateFromQuery(result: QueryReturnValue<T>) {
|
|
188
|
+
const { items, next } = result;
|
|
189
|
+
|
|
202
190
|
const stateUpdate: Partial<SearchSourceState<T>> = {};
|
|
191
|
+
if (next || next === null) {
|
|
192
|
+
stateUpdate.next = next;
|
|
193
|
+
stateUpdate.hasNext = !!next;
|
|
194
|
+
} else {
|
|
195
|
+
stateUpdate.offset = (this.offset ?? 0) + items.length;
|
|
196
|
+
stateUpdate.hasNext = items.length === this.pageSize;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return stateUpdate;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
resetState() {
|
|
203
|
+
this.state.next(this.initialState);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resetStateAndActivate() {
|
|
207
|
+
this.resetState();
|
|
208
|
+
this.activate();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export abstract class BaseSearchSource<T>
|
|
213
|
+
extends BaseSearchSourceBase<T>
|
|
214
|
+
implements SearchSource<T>
|
|
215
|
+
{
|
|
216
|
+
protected searchDebounced!: DebouncedExecQueryFunction;
|
|
217
|
+
|
|
218
|
+
constructor(options?: SearchSourceOptions) {
|
|
219
|
+
const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
|
|
220
|
+
super(options);
|
|
221
|
+
this.setDebounceOptions({ debounceMs });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
|
|
225
|
+
|
|
226
|
+
protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
|
|
227
|
+
|
|
228
|
+
setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
|
|
229
|
+
this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
async executeQuery(newSearchString?: string) {
|
|
233
|
+
if (!this.canExecuteQuery(newSearchString)) return;
|
|
234
|
+
|
|
235
|
+
const { hasNewSearchQuery, searchString } =
|
|
236
|
+
this.prepareStateForQuery(newSearchString);
|
|
237
|
+
|
|
238
|
+
let stateUpdate: Partial<SearchSourceState<T>> = {};
|
|
203
239
|
try {
|
|
204
240
|
const results = await this.query(searchString);
|
|
205
241
|
if (!results) return;
|
|
206
|
-
const { items, next } = results;
|
|
207
|
-
|
|
208
|
-
if (typeof next === 'string' || next === null) {
|
|
209
|
-
stateUpdate.next = next;
|
|
210
|
-
stateUpdate.hasNext = !!next;
|
|
211
|
-
} else {
|
|
212
|
-
stateUpdate.offset = (this.offset ?? 0) + items.length;
|
|
213
|
-
stateUpdate.hasNext = items.length === this.pageSize;
|
|
214
|
-
}
|
|
215
242
|
|
|
243
|
+
const { items } = results;
|
|
244
|
+
stateUpdate = this.updatePaginationStateFromQuery(results);
|
|
216
245
|
stateUpdate.items = await this.filterQueryResults(items);
|
|
217
246
|
} catch (e) {
|
|
218
247
|
stateUpdate.lastQueryError = e as Error;
|
|
@@ -226,13 +255,52 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
|
|
|
226
255
|
cancelScheduledQuery() {
|
|
227
256
|
this.searchDebounced.cancel();
|
|
228
257
|
}
|
|
258
|
+
}
|
|
229
259
|
|
|
230
|
-
|
|
231
|
-
|
|
260
|
+
export abstract class BaseSearchSourceSync<T>
|
|
261
|
+
extends BaseSearchSourceBase<T>
|
|
262
|
+
implements SearchSourceSync<T>
|
|
263
|
+
{
|
|
264
|
+
protected searchDebounced!: DebouncedExecQueryFunction;
|
|
265
|
+
|
|
266
|
+
constructor(options?: SearchSourceOptions) {
|
|
267
|
+
const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
|
|
268
|
+
super(options);
|
|
269
|
+
this.setDebounceOptions({ debounceMs });
|
|
232
270
|
}
|
|
233
271
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
272
|
+
protected abstract query(searchQuery: string): QueryReturnValue<T>;
|
|
273
|
+
|
|
274
|
+
protected abstract filterQueryResults(items: T[]): T[];
|
|
275
|
+
|
|
276
|
+
setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
|
|
277
|
+
this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
executeQuery(newSearchString?: string) {
|
|
281
|
+
if (!this.canExecuteQuery(newSearchString)) return;
|
|
282
|
+
|
|
283
|
+
const { hasNewSearchQuery, searchString } =
|
|
284
|
+
this.prepareStateForQuery(newSearchString);
|
|
285
|
+
|
|
286
|
+
let stateUpdate: Partial<SearchSourceState<T>> = {};
|
|
287
|
+
try {
|
|
288
|
+
const results = this.query(searchString);
|
|
289
|
+
if (!results) return;
|
|
290
|
+
|
|
291
|
+
const { items } = results;
|
|
292
|
+
stateUpdate = this.updatePaginationStateFromQuery(results);
|
|
293
|
+
stateUpdate.items = this.filterQueryResults(items);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
stateUpdate.lastQueryError = e as Error;
|
|
296
|
+
} finally {
|
|
297
|
+
this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
search = (searchQuery?: string) => this.searchDebounced(searchQuery);
|
|
302
|
+
|
|
303
|
+
cancelScheduledQuery() {
|
|
304
|
+
this.searchDebounced.cancel();
|
|
237
305
|
}
|
|
238
306
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BaseSearchSource } from './BaseSearchSource';
|
|
2
|
-
import type { SearchSourceOptions } from './
|
|
2
|
+
import type { SearchSourceOptions } from './types';
|
|
3
3
|
|
|
4
|
-
import { FeedsClient } from '
|
|
5
|
-
import { Feed } from '
|
|
4
|
+
import { FeedsClient } from '../../feeds-client';
|
|
5
|
+
import { Feed } from '../../feed';
|
|
6
6
|
|
|
7
7
|
export type FeedSearchSourceOptions = SearchSourceOptions & {
|
|
8
8
|
groupId?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StateStore } from '
|
|
1
|
+
import { StateStore } from '../StateStore';
|
|
2
2
|
import type { SearchSource } from './BaseSearchSource';
|
|
3
3
|
|
|
4
4
|
export type SearchControllerState = {
|
|
@@ -23,8 +23,6 @@ export class SearchController {
|
|
|
23
23
|
/**
|
|
24
24
|
* Not intended for direct use by integrators, might be removed without notice resulting in
|
|
25
25
|
* broken integrations.
|
|
26
|
-
*
|
|
27
|
-
* @internal
|
|
28
26
|
*/
|
|
29
27
|
_internalState: StateStore<InternalSearchControllerState>;
|
|
30
28
|
state: StateStore<SearchControllerState>;
|
|
@@ -39,7 +37,6 @@ export class SearchController {
|
|
|
39
37
|
this._internalState = new StateStore<InternalSearchControllerState>({});
|
|
40
38
|
this.config = { keepSingleActiveSource: true, ...config };
|
|
41
39
|
}
|
|
42
|
-
|
|
43
40
|
get hasNext() {
|
|
44
41
|
return this.sources.some((source) => source.hasNext);
|
|
45
42
|
}
|
|
@@ -117,9 +114,7 @@ export class SearchController {
|
|
|
117
114
|
this.state.partialNext({
|
|
118
115
|
searchQuery,
|
|
119
116
|
});
|
|
120
|
-
await Promise.all(
|
|
121
|
-
searchedSources.map((source) => source.search(searchQuery)),
|
|
122
|
-
);
|
|
117
|
+
await Promise.all(searchedSources.map((source) => source.search(searchQuery)));
|
|
123
118
|
};
|
|
124
119
|
|
|
125
120
|
cancelSearchQueries = () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BaseSearchSource } from './BaseSearchSource';
|
|
2
|
-
import type { SearchSourceOptions } from './
|
|
2
|
+
import type { SearchSourceOptions } from './types';
|
|
3
3
|
|
|
4
|
-
import { FeedsClient } from '
|
|
5
|
-
import { UserResponse } from '
|
|
4
|
+
import { FeedsClient } from '../../feeds-client';
|
|
5
|
+
import { UserResponse } from '../../gen/models';
|
|
6
6
|
|
|
7
7
|
export class UserSearchSource extends BaseSearchSource<UserResponse> {
|
|
8
8
|
readonly type = 'user' as const;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SearchSourceState<T = any> = {
|
|
2
|
+
hasNext: boolean;
|
|
3
|
+
isActive: boolean;
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
items: T[] | undefined;
|
|
6
|
+
searchQuery: string;
|
|
7
|
+
lastQueryError?: Error;
|
|
8
|
+
next?: string | null;
|
|
9
|
+
offset?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SearchSourceOptions = {
|
|
13
|
+
/** The number of milliseconds to debounce the search query. The default interval is 300ms. */
|
|
14
|
+
debounceMs?: number;
|
|
15
|
+
pageSize?: number;
|
|
16
|
+
allowEmptySearchString?: boolean;
|
|
17
|
+
resetOnNewSearchQuery?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SearchSourceType = 'activity' | 'user' | 'feed' | (string & {});
|
|
21
|
+
export type QueryReturnValue<T> = { items: T[]; next?: string | null };
|
package/src/common/types.ts
CHANGED
|
@@ -191,7 +191,7 @@ describe('activity-utils', () => {
|
|
|
191
191
|
|
|
192
192
|
expect(result.changed).toBe(true);
|
|
193
193
|
expect(result.activities).toHaveLength(1);
|
|
194
|
-
expect(result.activities[0].id).toBe('activity2');
|
|
194
|
+
expect(result.activities![0].id).toBe('activity2');
|
|
195
195
|
// Make sure we create a new array
|
|
196
196
|
expect(activities === result.activities).toBe(false);
|
|
197
197
|
});
|
|
@@ -205,7 +205,7 @@ describe('activity-utils', () => {
|
|
|
205
205
|
|
|
206
206
|
expect(result.changed).toBe(false);
|
|
207
207
|
expect(result.activities).toHaveLength(1);
|
|
208
|
-
expect(result.activities[0].id).toBe('activity1');
|
|
208
|
+
expect(result.activities![0].id).toBe('activity1');
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
it('should handle empty activities array', () => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { Feed } from '../../../feed';
|
|
3
|
+
import { FeedsClient } from '../../../feeds-client';
|
|
4
|
+
import { handleActivityAdded } from './handle-activity-added';
|
|
5
|
+
import {
|
|
6
|
+
generateActivityAddedEvent,
|
|
7
|
+
generateActivityResponse,
|
|
8
|
+
generateFeedResponse,
|
|
9
|
+
generateOwnUser,
|
|
10
|
+
getHumanId,
|
|
11
|
+
} from '../../../test-utils/response-generators';
|
|
12
|
+
|
|
13
|
+
describe(handleActivityAdded.name, () => {
|
|
14
|
+
let feed: Feed;
|
|
15
|
+
let client: FeedsClient;
|
|
16
|
+
let currentUserId: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
client = new FeedsClient('mock-api-key');
|
|
20
|
+
currentUserId = getHumanId();
|
|
21
|
+
client.state.partialNext({
|
|
22
|
+
connected_user: generateOwnUser({ id: currentUserId }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const feedResponse = generateFeedResponse({
|
|
26
|
+
id: 'main',
|
|
27
|
+
group_id: 'user',
|
|
28
|
+
created_by: { id: currentUserId },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
feed = new Feed(
|
|
32
|
+
client,
|
|
33
|
+
feedResponse.group_id,
|
|
34
|
+
feedResponse.id,
|
|
35
|
+
feedResponse,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('initializes activities when state is empty and adds new activity to the start', () => {
|
|
40
|
+
const event = generateActivityAddedEvent();
|
|
41
|
+
|
|
42
|
+
const stateBefore = feed.currentState;
|
|
43
|
+
expect(stateBefore.activities).toBeUndefined();
|
|
44
|
+
|
|
45
|
+
const hydrateSpy = vi.spyOn(client, 'hydratePollCache');
|
|
46
|
+
|
|
47
|
+
handleActivityAdded.call(feed, event);
|
|
48
|
+
|
|
49
|
+
const stateAfter = feed.currentState;
|
|
50
|
+
expect(stateAfter.activities).toBeDefined();
|
|
51
|
+
expect(stateAfter.activities).toHaveLength(1);
|
|
52
|
+
expect(stateAfter.activities?.[0]).toBe(event.activity);
|
|
53
|
+
expect(hydrateSpy).toHaveBeenCalledWith([event.activity]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('prepends new activity when activities already exist', () => {
|
|
57
|
+
const existing = generateActivityResponse();
|
|
58
|
+
feed.state.partialNext({ activities: [existing] });
|
|
59
|
+
|
|
60
|
+
const event = generateActivityAddedEvent();
|
|
61
|
+
|
|
62
|
+
handleActivityAdded.call(feed, event);
|
|
63
|
+
|
|
64
|
+
const stateAfter = feed.currentState;
|
|
65
|
+
expect(stateAfter.activities).toHaveLength(2);
|
|
66
|
+
expect(stateAfter.activities?.[0]).toBe(event.activity);
|
|
67
|
+
expect(stateAfter.activities?.[1]).toBe(existing);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not duplicate if activity already exists', () => {
|
|
71
|
+
const existing = generateActivityResponse();
|
|
72
|
+
feed.state.partialNext({ activities: [existing] });
|
|
73
|
+
|
|
74
|
+
const event = generateActivityAddedEvent({
|
|
75
|
+
activity: { id: existing.id },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const stateBefore = feed.currentState;
|
|
79
|
+
handleActivityAdded.call(feed, event);
|
|
80
|
+
const stateAfter = feed.currentState;
|
|
81
|
+
|
|
82
|
+
expect(stateAfter).toBe(stateBefore);
|
|
83
|
+
expect(stateAfter.activities).toHaveLength(1);
|
|
84
|
+
expect(stateAfter.activities?.[0]).toBe(existing);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { Feed } from '../../../feed';
|
|
3
|
+
import { FeedsClient } from '../../../feeds-client';
|
|
4
|
+
import { handleActivityDeleted } from './handle-activity-deleted';
|
|
5
|
+
import {
|
|
6
|
+
generateActivityDeletedEvent,
|
|
7
|
+
generateActivityPinResponse,
|
|
8
|
+
generateActivityResponse,
|
|
9
|
+
generateFeedResponse,
|
|
10
|
+
generateOwnUser,
|
|
11
|
+
getHumanId,
|
|
12
|
+
} from '../../../test-utils/response-generators';
|
|
13
|
+
|
|
14
|
+
describe(handleActivityDeleted.name, () => {
|
|
15
|
+
let feed: Feed;
|
|
16
|
+
let client: FeedsClient;
|
|
17
|
+
let currentUserId: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
client = new FeedsClient('mock-api-key');
|
|
21
|
+
currentUserId = getHumanId();
|
|
22
|
+
client.state.partialNext({
|
|
23
|
+
connected_user: generateOwnUser({ id: currentUserId }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const feedResponse = generateFeedResponse({
|
|
27
|
+
id: 'main',
|
|
28
|
+
group_id: 'user',
|
|
29
|
+
created_by: { id: currentUserId },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
feed = new Feed(
|
|
33
|
+
client,
|
|
34
|
+
feedResponse.group_id,
|
|
35
|
+
feedResponse.id,
|
|
36
|
+
feedResponse,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('removes the activity from activities array when present', () => {
|
|
41
|
+
const activity1 = generateActivityResponse();
|
|
42
|
+
const activity2 = generateActivityResponse();
|
|
43
|
+
feed.state.partialNext({ activities: [activity1, activity2] });
|
|
44
|
+
|
|
45
|
+
const event = generateActivityDeletedEvent({
|
|
46
|
+
activity: { id: activity1.id },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const stateBefore = feed.currentState;
|
|
50
|
+
expect(stateBefore.activities).toHaveLength(2);
|
|
51
|
+
|
|
52
|
+
handleActivityDeleted.call(feed, event);
|
|
53
|
+
|
|
54
|
+
const stateAfter = feed.currentState;
|
|
55
|
+
expect(stateAfter.activities).toHaveLength(1);
|
|
56
|
+
expect(stateAfter.activities?.[0].id).toBe(activity2.id);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('removes the activity from pinned_activities when present', () => {
|
|
60
|
+
const pin1 = generateActivityPinResponse();
|
|
61
|
+
const pin2 = generateActivityPinResponse();
|
|
62
|
+
feed.state.partialNext({ pinned_activities: [pin1, pin2] });
|
|
63
|
+
|
|
64
|
+
const event = generateActivityDeletedEvent({
|
|
65
|
+
activity: { id: pin1.activity.id },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const stateBefore = feed.currentState;
|
|
69
|
+
expect(stateBefore.pinned_activities).toHaveLength(2);
|
|
70
|
+
|
|
71
|
+
handleActivityDeleted.call(feed, event);
|
|
72
|
+
|
|
73
|
+
const stateAfter = feed.currentState;
|
|
74
|
+
|
|
75
|
+
expect(stateAfter.pinned_activities).toHaveLength(1);
|
|
76
|
+
expect(stateAfter.pinned_activities?.[0]).toBe(pin2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('updates both arrays when the activity exists in both activities and pinned_activities', () => {
|
|
80
|
+
const sharedId = getHumanId();
|
|
81
|
+
const activity = generateActivityResponse({ id: sharedId });
|
|
82
|
+
const pinnedActivity = generateActivityPinResponse({
|
|
83
|
+
activity: { id: sharedId },
|
|
84
|
+
});
|
|
85
|
+
feed.state.partialNext({
|
|
86
|
+
activities: [activity],
|
|
87
|
+
pinned_activities: [pinnedActivity],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const event = generateActivityDeletedEvent({
|
|
91
|
+
activity: { id: sharedId },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
handleActivityDeleted.call(feed, event);
|
|
95
|
+
|
|
96
|
+
const stateAfter = feed.currentState;
|
|
97
|
+
expect(stateAfter.activities).toHaveLength(0);
|
|
98
|
+
expect(stateAfter.pinned_activities).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does nothing if the activity is not found in either list', () => {
|
|
102
|
+
const activity = generateActivityResponse();
|
|
103
|
+
const pinnedActivity = generateActivityPinResponse();
|
|
104
|
+
feed.state.partialNext({
|
|
105
|
+
activities: [activity],
|
|
106
|
+
pinned_activities: [pinnedActivity],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const event = generateActivityDeletedEvent({ activity: { id: 'unknown' } });
|
|
110
|
+
|
|
111
|
+
const stateBefore = feed.currentState;
|
|
112
|
+
handleActivityDeleted.call(feed, event);
|
|
113
|
+
const stateAfter = feed.currentState;
|
|
114
|
+
|
|
115
|
+
expect(stateAfter).toBe(stateBefore);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -3,12 +3,14 @@ import type {
|
|
|
3
3
|
ActivityPinResponse,
|
|
4
4
|
ActivityResponse,
|
|
5
5
|
} from '../../../gen/models';
|
|
6
|
-
import type { EventPayload } from '../../../types-internal';
|
|
6
|
+
import type { EventPayload, UpdateStateResult } from '../../../types-internal';
|
|
7
7
|
|
|
8
8
|
export const removeActivityFromState = (
|
|
9
9
|
activityResponse: ActivityResponse,
|
|
10
10
|
activities: ActivityResponse[] | undefined,
|
|
11
|
-
)
|
|
11
|
+
): UpdateStateResult<{
|
|
12
|
+
activities: ActivityResponse[] | undefined;
|
|
13
|
+
}> => {
|
|
12
14
|
const index =
|
|
13
15
|
activities?.findIndex((activity) => activity.id === activityResponse.id) ??
|
|
14
16
|
-1;
|
|
@@ -25,7 +27,9 @@ export const removeActivityFromState = (
|
|
|
25
27
|
export const removePinnedActivityFromState = (
|
|
26
28
|
activityResponse: ActivityResponse,
|
|
27
29
|
pinnedActivities: ActivityPinResponse[] | undefined,
|
|
28
|
-
)
|
|
30
|
+
): UpdateStateResult<{
|
|
31
|
+
pinned_activities: ActivityPinResponse[] | undefined;
|
|
32
|
+
}> => {
|
|
29
33
|
const index =
|
|
30
34
|
pinnedActivities?.findIndex(
|
|
31
35
|
(pinnedActivity) => pinnedActivity.activity.id === activityResponse.id,
|
|
@@ -34,7 +38,7 @@ export const removePinnedActivityFromState = (
|
|
|
34
38
|
if (index !== -1) {
|
|
35
39
|
const newActivities = [...pinnedActivities!];
|
|
36
40
|
newActivities.splice(index, 1);
|
|
37
|
-
return { changed: true,
|
|
41
|
+
return { changed: true, pinned_activities: newActivities };
|
|
38
42
|
} else {
|
|
39
43
|
return { changed: false, pinned_activities: pinnedActivities };
|
|
40
44
|
}
|