@equinor/fusion-framework-react-components-people-provider 2.0.5-next.0 → 2.0.6
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/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +11 -8
- package/CHANGELOG.md +0 -1035
- package/src/PeopleResolverComponent.tsx +0 -52
- package/src/PeopleResolverProvider.tsx +0 -38
- package/src/PersonController.ts +0 -431
- package/src/create-resolver.ts +0 -58
- package/src/index.ts +0 -1
- package/src/make-resolver.tsx +0 -19
- package/src/types.ts +0 -6
- package/src/version.ts +0 -2
- package/tsconfig.json +0 -18
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { type PropsWithChildren, useEffect, useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
PersonProviderElement,
|
|
5
|
-
PersonAvatarElement,
|
|
6
|
-
PersonCardElement,
|
|
7
|
-
type PersonResolver,
|
|
8
|
-
PersonListItemElement,
|
|
9
|
-
PersonSelectElement,
|
|
10
|
-
} from '@equinor/fusion-wc-person';
|
|
11
|
-
export { PersonResolver } from '@equinor/fusion-wc-person';
|
|
12
|
-
|
|
13
|
-
declare module 'react' {
|
|
14
|
-
namespace JSX {
|
|
15
|
-
interface IntrinsicElements {
|
|
16
|
-
'fwc-person-provider': React.DetailedHTMLProps<
|
|
17
|
-
React.HTMLAttributes<PersonProviderElement>,
|
|
18
|
-
PersonProviderElement
|
|
19
|
-
>;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
PersonProviderElement;
|
|
25
|
-
PersonAvatarElement;
|
|
26
|
-
PersonCardElement;
|
|
27
|
-
PersonListItemElement;
|
|
28
|
-
PersonSelectElement;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Wraps the `fwc-person-provider` web component, wiring the given `PersonResolver` into it
|
|
32
|
-
* once the underlying custom element is ready.
|
|
33
|
-
*
|
|
34
|
-
* @param props - Component props
|
|
35
|
-
* @param props.resolver - The resolver used to fetch person data, photos, and search results
|
|
36
|
-
* @param props.children - Elements rendered inside the person provider, typically person-related components
|
|
37
|
-
* @returns The rendered `fwc-person-provider` element wrapping the given children
|
|
38
|
-
*/
|
|
39
|
-
export const PeopleResolverComponent = (props: PropsWithChildren<{ resolver: PersonResolver }>) => {
|
|
40
|
-
const { resolver, children } = props;
|
|
41
|
-
const ref = useRef<PersonProviderElement | null>(null);
|
|
42
|
-
|
|
43
|
-
// when the element is ready, set the resolver
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
// Only assign the resolver once the custom element ref has mounted
|
|
46
|
-
if (ref.current && resolver) {
|
|
47
|
-
ref.current.resolver = resolver;
|
|
48
|
-
}
|
|
49
|
-
}, [resolver]);
|
|
50
|
-
|
|
51
|
-
return <fwc-person-provider ref={ref}>{children}</fwc-person-provider>;
|
|
52
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { type PropsWithChildren, type ReactNode, Suspense, useMemo } from 'react';
|
|
2
|
-
import type { ServicesModule } from '@equinor/fusion-framework-module-services';
|
|
3
|
-
import { useModule } from '@equinor/fusion-framework-react-module';
|
|
4
|
-
import { makeResolver } from './make-resolver';
|
|
5
|
-
import type { PersonControllerOptions } from './PersonController';
|
|
6
|
-
|
|
7
|
-
type PeopleResolverProviderProps = PropsWithChildren<{
|
|
8
|
-
readonly options?: PersonControllerOptions;
|
|
9
|
-
readonly fallback?: ReactNode;
|
|
10
|
-
}>;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Provides person resolution to its children by wiring up a `PersonResolver` built from the
|
|
14
|
-
* framework's `services` module.
|
|
15
|
-
*
|
|
16
|
-
* @param props - Component props
|
|
17
|
-
* @param props.children - Elements that will have access to the person resolver
|
|
18
|
-
* @param props.options - Optional controller options, such as a fallback image for missing photos
|
|
19
|
-
* @param props.fallback - Optional fallback rendered while the resolver component is suspended
|
|
20
|
-
* @returns The rendered people resolver provider wrapping the given children
|
|
21
|
-
* @throws Error if the `services` module has not been registered on the framework
|
|
22
|
-
*/
|
|
23
|
-
export const PeopleResolverProvider = (props: PeopleResolverProviderProps) => {
|
|
24
|
-
const { children, options, fallback } = props;
|
|
25
|
-
const services = useModule<ServicesModule>('services');
|
|
26
|
-
// Fail fast when the services module has not been registered on the framework
|
|
27
|
-
if (!services) {
|
|
28
|
-
throw Error('missing service module');
|
|
29
|
-
}
|
|
30
|
-
const Component = useMemo(() => makeResolver(services, options), [services, options]);
|
|
31
|
-
return (
|
|
32
|
-
<Suspense fallback={fallback || null}>
|
|
33
|
-
<Component>{children}</Component>
|
|
34
|
-
</Suspense>
|
|
35
|
-
);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export default PeopleResolverProvider;
|
package/src/PersonController.ts
DELETED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
import { EMPTY, type Observable, concat, from, fromEvent, of } from 'rxjs';
|
|
2
|
-
import { catchError, filter, find, map, switchMap, take, takeUntil } from 'rxjs/operators';
|
|
3
|
-
|
|
4
|
-
import type { ApiPerson, PeopleApiClient } from '@equinor/fusion-framework-module-services/people';
|
|
5
|
-
import { isApiPerson } from '@equinor/fusion-framework-module-services/people/utils';
|
|
6
|
-
import type { ApiResponse as GetPersonApiResponse } from '@equinor/fusion-framework-module-services/people/get';
|
|
7
|
-
import type { ApiResponse as QueryPersonApiResponse } from '@equinor/fusion-framework-module-services/people/query';
|
|
8
|
-
import type { ApiResponse as SuggestPersonApiResponse } from '@equinor/fusion-framework-module-services/people/suggest';
|
|
9
|
-
import type { ApiResponse as ResolvePersonApiResponse } from '@equinor/fusion-framework-module-services/people/resolve';
|
|
10
|
-
import { Query } from '@equinor/fusion-query';
|
|
11
|
-
import { queryValue } from '@equinor/fusion-query/operators';
|
|
12
|
-
|
|
13
|
-
import type { ApiProviderError } from '@equinor/fusion-framework-module-services/provider';
|
|
14
|
-
|
|
15
|
-
type GetPersonResult = GetPersonApiResponse<
|
|
16
|
-
'v4',
|
|
17
|
-
{ azureId: ''; expand: ['positions', 'manager'] }
|
|
18
|
-
>;
|
|
19
|
-
|
|
20
|
-
type PersonSearchResult = QueryPersonApiResponse<'v2'>;
|
|
21
|
-
|
|
22
|
-
type MatcherArgs = { upn: string; azureId?: string } | { upn?: string; azureId: string };
|
|
23
|
-
|
|
24
|
-
type ResolverArgs<T> = T extends object
|
|
25
|
-
? { [K in keyof T]: T[K] } & { signal?: AbortSignal }
|
|
26
|
-
: { signal?: AbortSignal };
|
|
27
|
-
|
|
28
|
-
const personMatcher =
|
|
29
|
-
(args: MatcherArgs) =>
|
|
30
|
-
<T extends { azureUniqueId?: string; upn?: string }>(value: T): value is T => {
|
|
31
|
-
const { azureId, upn } = args;
|
|
32
|
-
// Both identifiers must match when both are supplied to avoid a false-positive match
|
|
33
|
-
if (azureId && upn) {
|
|
34
|
-
return (
|
|
35
|
-
value.upn?.toLocaleLowerCase() === upn.toLocaleLowerCase() &&
|
|
36
|
-
value.azureUniqueId === azureId
|
|
37
|
-
);
|
|
38
|
-
} else if (azureId) {
|
|
39
|
-
return value.azureUniqueId === azureId;
|
|
40
|
-
} else if (upn) {
|
|
41
|
-
return value.upn?.toLocaleLowerCase() === upn.toLocaleLowerCase();
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export interface IPersonController {
|
|
47
|
-
getPerson(args: ResolverArgs<MatcherArgs>): Observable<GetPersonResult>;
|
|
48
|
-
getPersonInfo(args: ResolverArgs<MatcherArgs>): Observable<ApiPerson<'v2'>>;
|
|
49
|
-
getPhoto(args: ResolverArgs<MatcherArgs>): Observable<string>;
|
|
50
|
-
search(args: ResolverArgs<{ search: string }>): Observable<PersonSearchResult>;
|
|
51
|
-
suggest(
|
|
52
|
-
args: ResolverArgs<{ search: string; systemAccounts: boolean }>,
|
|
53
|
-
): Observable<SuggestPersonApiResponse>;
|
|
54
|
-
resolve(args: ResolverArgs<{ resolveIds: string[] }>): Observable<ResolvePersonApiResponse>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type PersonControllerOptions = {
|
|
58
|
-
fallbackImage?: Blob;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Default implementation of {@link IPersonController}, backed by a {@link PeopleApiClient} and
|
|
63
|
-
* a set of {@link Query} caches for people, searches, photos, suggestions, and resolves.
|
|
64
|
-
*/
|
|
65
|
-
export class PersonController implements IPersonController {
|
|
66
|
-
#personQuery: Query<GetPersonResult, ResolverArgs<{ azureId: string }>>;
|
|
67
|
-
#personSearchQuery: Query<PersonSearchResult, ResolverArgs<{ search: string }>>;
|
|
68
|
-
#personPhotoQuery: Query<Blob, ResolverArgs<{ azureId: string }>>;
|
|
69
|
-
#personSuggestQuery: Query<
|
|
70
|
-
SuggestPersonApiResponse,
|
|
71
|
-
ResolverArgs<{ search: string; systemAccounts: boolean }>
|
|
72
|
-
>;
|
|
73
|
-
#personResolveQuery: Query<ResolvePersonApiResponse, ResolverArgs<{ resolveIds: string[] }>>;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param client - The people API client used to fetch person data, photos, and search results
|
|
77
|
-
* @param options - Optional controller options, such as a fallback image for missing photos
|
|
78
|
-
* @throws Error if the photo request fails for a reason other than a fallback-eligible 404
|
|
79
|
-
*/
|
|
80
|
-
constructor(client: PeopleApiClient, options?: PersonControllerOptions) {
|
|
81
|
-
const expire = 3 * 60 * 1000;
|
|
82
|
-
this.#personQuery = new Query({
|
|
83
|
-
expire,
|
|
84
|
-
queueOperator: 'merge',
|
|
85
|
-
key: ({ azureId }) => azureId,
|
|
86
|
-
client: {
|
|
87
|
-
fn: ({ azureId }, signal): Observable<GetPersonResult> => {
|
|
88
|
-
// Filter out expired positions from the fetched person's result before returning it
|
|
89
|
-
return client
|
|
90
|
-
.get('v4', 'json$', { azureId, expand: ['manager', 'positions'] }, { signal })
|
|
91
|
-
.pipe(
|
|
92
|
-
map((result) => {
|
|
93
|
-
const { positions = [] } = result;
|
|
94
|
-
// Drop positions that have already expired so stale data isn't shown
|
|
95
|
-
const activePositions = positions.filter((x) => new Date(x.appliesTo) > new Date());
|
|
96
|
-
return {
|
|
97
|
-
...result,
|
|
98
|
-
positions: activePositions,
|
|
99
|
-
};
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
this.#personSearchQuery = new Query({
|
|
106
|
-
expire,
|
|
107
|
-
queueOperator: 'merge',
|
|
108
|
-
key: ({ search }) => search,
|
|
109
|
-
client: {
|
|
110
|
-
fn: ({ search }, signal) => {
|
|
111
|
-
return client.query('v2', 'json$', { search }, { signal });
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
this.#personPhotoQuery = new Query({
|
|
116
|
-
expire,
|
|
117
|
-
queueOperator: 'merge',
|
|
118
|
-
key: ({ azureId }) => azureId,
|
|
119
|
-
client: {
|
|
120
|
-
fn: ({ azureId }, signal): Observable<Blob> => {
|
|
121
|
-
// Extract the blob from the response, falling back to a placeholder image on 404
|
|
122
|
-
return client.photo('v2', 'blob$', { azureId }, { signal }).pipe(
|
|
123
|
-
map((result) => {
|
|
124
|
-
return result.blob;
|
|
125
|
-
}),
|
|
126
|
-
catchError((err) => {
|
|
127
|
-
// Fall back to a placeholder image when the person genuinely has no photo
|
|
128
|
-
if (
|
|
129
|
-
(err as Error).name === 'ApiProviderError' &&
|
|
130
|
-
(err as ApiProviderError).response?.status === 404 &&
|
|
131
|
-
options?.fallbackImage
|
|
132
|
-
) {
|
|
133
|
-
return of(options?.fallbackImage);
|
|
134
|
-
}
|
|
135
|
-
throw err;
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
this.#personSuggestQuery = new Query({
|
|
142
|
-
expire,
|
|
143
|
-
queueOperator: 'merge',
|
|
144
|
-
key: ({ search, systemAccounts }) => `${search}-${systemAccounts}`,
|
|
145
|
-
client: {
|
|
146
|
-
fn: ({ search, systemAccounts }, signal) => {
|
|
147
|
-
const types = ['Person'];
|
|
148
|
-
// System accounts are opt-in since they're excluded from suggestions by default
|
|
149
|
-
if (systemAccounts) {
|
|
150
|
-
types.push('SystemAccount');
|
|
151
|
-
}
|
|
152
|
-
return client.suggest('json$', {
|
|
153
|
-
method: 'POST',
|
|
154
|
-
body: JSON.stringify({ queryString: search, types }),
|
|
155
|
-
signal,
|
|
156
|
-
});
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
this.#personResolveQuery = new Query({
|
|
161
|
-
expire,
|
|
162
|
-
queueOperator: 'merge',
|
|
163
|
-
key: ({ resolveIds }) => JSON.stringify([...resolveIds].sort()),
|
|
164
|
-
client: {
|
|
165
|
-
fn: ({ resolveIds }, signal) => {
|
|
166
|
-
return client.resolve('json$', {
|
|
167
|
-
method: 'POST',
|
|
168
|
-
body: JSON.stringify({ identifiers: resolveIds }),
|
|
169
|
-
signal,
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Suggest persons matching the given search string.
|
|
178
|
-
* Search string can be a part of display name, mail, upn or the full azureId.
|
|
179
|
-
* If systemAccounts is true, it will also include system accounts in the result.
|
|
180
|
-
*
|
|
181
|
-
* @param args - The search string, whether to include system accounts, and an optional abort signal
|
|
182
|
-
* @returns An observable emitting the matching suggestions
|
|
183
|
-
*/
|
|
184
|
-
public suggest(
|
|
185
|
-
args: ResolverArgs<{ search: string; systemAccounts: boolean }>,
|
|
186
|
-
): Observable<SuggestPersonApiResponse> {
|
|
187
|
-
const { search, systemAccounts, signal } = args;
|
|
188
|
-
// Unwrap the query result to just its value
|
|
189
|
-
return this.#personSuggestQuery.query({ search, systemAccounts }, { signal }).pipe(queryValue);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Resolve person details for given identifiers, which can be a mix of azureIds and upns.
|
|
194
|
-
*
|
|
195
|
-
* @param args - The identifiers to resolve and an optional abort signal
|
|
196
|
-
* @returns An observable emitting the resolved person details
|
|
197
|
-
*/
|
|
198
|
-
public resolve(
|
|
199
|
-
args: ResolverArgs<{ resolveIds: string[] }>,
|
|
200
|
-
): Observable<ResolvePersonApiResponse> {
|
|
201
|
-
const { resolveIds, signal } = args;
|
|
202
|
-
// Unwrap the query result to just its value
|
|
203
|
-
return this.#personResolveQuery.query({ resolveIds }, { signal }).pipe(queryValue);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Search for persons matching the given search string.
|
|
208
|
-
*
|
|
209
|
-
* @param args - The search string and an optional abort signal
|
|
210
|
-
* @returns An observable emitting the search results
|
|
211
|
-
*/
|
|
212
|
-
public search(args: { search: string; signal?: AbortSignal }): Observable<PersonSearchResult> {
|
|
213
|
-
const { search, signal } = args;
|
|
214
|
-
// Unwrap the query result to just its value
|
|
215
|
-
return this.#personSearchQuery.query({ search }, { signal }).pipe(queryValue);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Fetch the photo of a person, resolving by azureId when available, falling back to upn.
|
|
220
|
-
*
|
|
221
|
-
* TODO(#5088): why does this need to have data?!?
|
|
222
|
-
*
|
|
223
|
-
* @param args - A matcher (azureId and/or upn) plus an optional abort signal
|
|
224
|
-
* @returns An observable emitting the object URL of the person's photo
|
|
225
|
-
* @throws Error if neither azureId nor upn is provided
|
|
226
|
-
*/
|
|
227
|
-
public getPhoto(args: ResolverArgs<MatcherArgs>): Observable<string> {
|
|
228
|
-
const { azureId, upn, signal } = args;
|
|
229
|
-
|
|
230
|
-
// Prefer resolving by azureId when available, falling back to upn
|
|
231
|
-
if (azureId) {
|
|
232
|
-
return this._getPersonPhotoByAzureId(azureId, signal);
|
|
233
|
-
} else if (upn) {
|
|
234
|
-
return this._getPersonPhotoByUpn(upn, signal);
|
|
235
|
-
}
|
|
236
|
-
throw Error('invalid args provided');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Fetch full person details, resolving by azureId when available, falling back to upn.
|
|
241
|
-
*
|
|
242
|
-
* @param args - A matcher (azureId and/or upn) plus an optional abort signal
|
|
243
|
-
* @returns An observable emitting the person details
|
|
244
|
-
* @throws Error if neither azureId nor upn is provided
|
|
245
|
-
*/
|
|
246
|
-
public getPerson(args: ResolverArgs<MatcherArgs>): Observable<GetPersonResult> {
|
|
247
|
-
const { azureId, upn, signal } = args;
|
|
248
|
-
// Prefer resolving by azureId when available, falling back to upn
|
|
249
|
-
if (azureId) {
|
|
250
|
-
return this._getPersonByAzureId(azureId, signal);
|
|
251
|
-
} else if (upn) {
|
|
252
|
-
return this._getPersonByUpn(upn, signal);
|
|
253
|
-
}
|
|
254
|
-
throw Error('invalid args provided');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Fetch v2 person info, resolving by azureId when available, falling back to upn.
|
|
259
|
-
*
|
|
260
|
-
* @param args - A matcher (azureId and/or upn) plus an optional abort signal
|
|
261
|
-
* @returns An observable emitting the v2 person info
|
|
262
|
-
* @throws Error if neither azureId nor upn is provided
|
|
263
|
-
*/
|
|
264
|
-
public getPersonInfo(args: ResolverArgs<MatcherArgs>): Observable<ApiPerson<'v2'>> {
|
|
265
|
-
const { azureId, upn, signal } = args;
|
|
266
|
-
// Prefer resolving by azureId when available, falling back to upn
|
|
267
|
-
if (azureId) {
|
|
268
|
-
return this._getPersonInfoById(azureId, signal);
|
|
269
|
-
} else if (upn) {
|
|
270
|
-
return this._getPersonInfoByUpn(upn, signal);
|
|
271
|
-
}
|
|
272
|
-
throw Error('invalid args provided');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Resolve a full v4 person by upn, via cache and live lookup.
|
|
277
|
-
*
|
|
278
|
-
* @param upn - The person's upn
|
|
279
|
-
* @param signal - Optional abort signal
|
|
280
|
-
* @returns An observable emitting the resolved v4 person
|
|
281
|
-
*/
|
|
282
|
-
protected _getPersonByUpn(upn: string, signal?: AbortSignal): Observable<GetPersonResult> {
|
|
283
|
-
const abort$ = signal ? fromEvent(signal, 'abort') : EMPTY;
|
|
284
|
-
// Resolve the v2 person info by upn, then use its azureId to fetch the full v4 person
|
|
285
|
-
const personByAzureId$ = this._getPersonInfoByUpn(upn, signal).pipe(
|
|
286
|
-
filter(isApiPerson('v2')),
|
|
287
|
-
switchMap(({ azureUniqueId: azureId }) => {
|
|
288
|
-
return this._getPersonByAzureId(azureId, signal);
|
|
289
|
-
}),
|
|
290
|
-
);
|
|
291
|
-
// Emit from cache first, then fall back to the live lookup, stopping once a v4 person arrives
|
|
292
|
-
return concat(this._personCache$({ upn }), personByAzureId$).pipe(
|
|
293
|
-
filter(isApiPerson('v4')),
|
|
294
|
-
takeUntil(abort$),
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Fetch a full v4 person by azureId.
|
|
300
|
-
*
|
|
301
|
-
* @param azureId - The person's azureId
|
|
302
|
-
* @param signal - Optional abort signal
|
|
303
|
-
* @returns An observable emitting the fetched v4 person
|
|
304
|
-
*/
|
|
305
|
-
public _getPersonByAzureId(azureId: string, signal?: AbortSignal): Observable<GetPersonResult> {
|
|
306
|
-
// Unwrap the query result to just its value
|
|
307
|
-
return this.#personQuery.query({ azureId }, { signal }).pipe(queryValue);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Resolve v2 person info by azureId, via cache and live lookup.
|
|
312
|
-
*
|
|
313
|
-
* @param azureId - The person's azureId
|
|
314
|
-
* @param signal - Optional abort signal
|
|
315
|
-
* @returns An observable emitting the resolved v2 person info
|
|
316
|
-
*/
|
|
317
|
-
protected _getPersonInfoById(azureId: string, signal?: AbortSignal): Observable<ApiPerson<'v2'>> {
|
|
318
|
-
const abort$ = signal ? fromEvent(signal, 'abort') : EMPTY;
|
|
319
|
-
// Emit from caches first, then fall back to a live lookup, keeping only v2 persons
|
|
320
|
-
return concat(
|
|
321
|
-
this._personCache$({ azureId }),
|
|
322
|
-
this._queryCache$({ azureId }),
|
|
323
|
-
this._getPersonByAzureId(azureId, signal),
|
|
324
|
-
).pipe(filter(isApiPerson('v2')), takeUntil(abort$));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Resolve v2 person info by upn, via cache and search-based lookup.
|
|
329
|
-
*
|
|
330
|
-
* @param upn - The person's upn
|
|
331
|
-
* @param signal - Optional abort signal
|
|
332
|
-
* @returns An observable emitting the resolved v2 person info
|
|
333
|
-
*/
|
|
334
|
-
protected _getPersonInfoByUpn(upn: string, signal?: AbortSignal): Observable<ApiPerson<'v2'>> {
|
|
335
|
-
const matcher = personMatcher({ upn });
|
|
336
|
-
const abort$ = signal ? fromEvent(signal, 'abort') : EMPTY;
|
|
337
|
-
// Search by upn, then narrow the results down to the single matching entry, if any
|
|
338
|
-
const searchMatch$ = this.#personSearchQuery.query({ search: upn }, { signal }).pipe(
|
|
339
|
-
map((x) => {
|
|
340
|
-
// Narrow the search results down to the one entry matching this upn/azureId
|
|
341
|
-
const match = x.value.find(matcher);
|
|
342
|
-
return match;
|
|
343
|
-
}),
|
|
344
|
-
find(isApiPerson('v2')),
|
|
345
|
-
);
|
|
346
|
-
// Emit from caches first, then fall back to the search-based lookup, keeping only v2 persons
|
|
347
|
-
return concat(this._personCache$({ upn }), this._queryCache$({ upn }), searchMatch$).pipe(
|
|
348
|
-
find(isApiPerson('v2')),
|
|
349
|
-
filter(isApiPerson('v2')),
|
|
350
|
-
takeUntil(abort$),
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Fetch a person's photo by azureId as an object URL.
|
|
356
|
-
*
|
|
357
|
-
* @param azureId - The person's azureId
|
|
358
|
-
* @param signal - Optional abort signal
|
|
359
|
-
* @returns An observable emitting the photo's object URL
|
|
360
|
-
*/
|
|
361
|
-
protected _getPersonPhotoByAzureId(azureId: string, signal?: AbortSignal): Observable<string> {
|
|
362
|
-
// Take just the first emission and convert the blob result to an object URL
|
|
363
|
-
return this.#personPhotoQuery.query({ azureId }, { signal }).pipe(
|
|
364
|
-
take(1),
|
|
365
|
-
map((result) => URL.createObjectURL(result.value)),
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Fetch a person's photo by upn, resolving their azureId first.
|
|
371
|
-
*
|
|
372
|
-
* @param upn - The person's upn
|
|
373
|
-
* @param signal - Optional abort signal
|
|
374
|
-
* @returns An observable emitting the photo's object URL
|
|
375
|
-
*/
|
|
376
|
-
protected _getPersonPhotoByUpn(upn: string, signal?: AbortSignal) {
|
|
377
|
-
// Take just the first emission, then fetch the photo using its resolved azureId
|
|
378
|
-
return this._getPersonInfoByUpn(upn, signal).pipe(
|
|
379
|
-
take(1),
|
|
380
|
-
switchMap((x) => this._getPersonPhotoByAzureId(x.azureUniqueId, signal)),
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Search the person-query cache for a matching v4 person.
|
|
386
|
-
*
|
|
387
|
-
* @param args - A matcher (azureId and/or upn) used to find the cached entry
|
|
388
|
-
* @returns An observable emitting the matching cached v4 person, if any
|
|
389
|
-
*/
|
|
390
|
-
protected _personCache$(args: MatcherArgs): Observable<GetPersonResult> {
|
|
391
|
-
const mather = personMatcher(args);
|
|
392
|
-
// Search the person-query cache for the first matching v4 entry
|
|
393
|
-
return this.#personQuery.cache.state$.pipe(
|
|
394
|
-
take(1),
|
|
395
|
-
map((x) => {
|
|
396
|
-
// Search each cached query result entry for the one matching this person
|
|
397
|
-
const match = Object.values(x).find((x) => mather(x.value));
|
|
398
|
-
return match?.value;
|
|
399
|
-
}),
|
|
400
|
-
find(isApiPerson('v4')),
|
|
401
|
-
filter(isApiPerson('v4')),
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Search the search-query cache for a matching v2 person.
|
|
407
|
-
*
|
|
408
|
-
* @param args - A matcher (azureId and/or upn) used to find the cached entry
|
|
409
|
-
* @returns An observable emitting the matching cached v2 person, if any
|
|
410
|
-
*/
|
|
411
|
-
protected _queryCache$(args: MatcherArgs): Observable<ApiPerson<'v2'>> {
|
|
412
|
-
const mather = personMatcher(args);
|
|
413
|
-
// Search the search-query cache for the first matching v2 entry
|
|
414
|
-
return this.#personSearchQuery.cache.state$.pipe(
|
|
415
|
-
take(1),
|
|
416
|
-
switchMap((entry) => {
|
|
417
|
-
// Expand the cache entry's records and find the one matching this person
|
|
418
|
-
return from(Object.values(entry)).pipe(
|
|
419
|
-
map((x) => {
|
|
420
|
-
// Search each cached entry's results for the one matching this person
|
|
421
|
-
const match = x.value.find((x) => mather(x));
|
|
422
|
-
return match;
|
|
423
|
-
}),
|
|
424
|
-
find(isApiPerson('v2')),
|
|
425
|
-
);
|
|
426
|
-
}),
|
|
427
|
-
find(isApiPerson('v2')),
|
|
428
|
-
filter(isApiPerson('v2')),
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
}
|
package/src/create-resolver.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { firstValueFrom, map } from 'rxjs';
|
|
2
|
-
|
|
3
|
-
import type { PersonDetails, PersonInfo, PersonResolver } from '@equinor/fusion-wc-person';
|
|
4
|
-
import type { IPersonController } from './PersonController';
|
|
5
|
-
|
|
6
|
-
export const createResolver = (controller: IPersonController): PersonResolver => ({
|
|
7
|
-
getDetails(args) {
|
|
8
|
-
// Remap the controller result to the public PersonDetails shape
|
|
9
|
-
return firstValueFrom(
|
|
10
|
-
controller.getPerson(args).pipe(
|
|
11
|
-
map((x) => {
|
|
12
|
-
// Controller result uses azureUniqueId; remap to azureId and cast to the public PersonDetails shape.
|
|
13
|
-
return { ...x, azureId: x.azureUniqueId } as unknown as PersonDetails;
|
|
14
|
-
}),
|
|
15
|
-
),
|
|
16
|
-
);
|
|
17
|
-
},
|
|
18
|
-
getInfo(args) {
|
|
19
|
-
// Remap the controller result to the public PersonInfo shape
|
|
20
|
-
return firstValueFrom(
|
|
21
|
-
controller.getPersonInfo(args).pipe(
|
|
22
|
-
map((x) => {
|
|
23
|
-
// Controller result uses azureUniqueId; remap to azureId and cast to the public PersonInfo shape.
|
|
24
|
-
return { ...x, azureId: x.azureUniqueId } as unknown as PersonInfo;
|
|
25
|
-
}),
|
|
26
|
-
),
|
|
27
|
-
);
|
|
28
|
-
},
|
|
29
|
-
getPhoto(args) {
|
|
30
|
-
return firstValueFrom(controller.getPhoto(args));
|
|
31
|
-
},
|
|
32
|
-
search(args) {
|
|
33
|
-
// Remap each result's azureUniqueId to azureId and cast to the public PersonInfo shape.
|
|
34
|
-
return firstValueFrom(
|
|
35
|
-
controller.search(args).pipe(
|
|
36
|
-
map((x) => {
|
|
37
|
-
// Remap each result's azureUniqueId to azureId and cast to the public PersonInfo shape.
|
|
38
|
-
const mapped = x.map((x) => {
|
|
39
|
-
// Controller result uses azureUniqueId; remap to azureId and cast to the public PersonInfo shape.
|
|
40
|
-
return {
|
|
41
|
-
...x,
|
|
42
|
-
azureId: x.azureUniqueId,
|
|
43
|
-
} as unknown as PersonInfo;
|
|
44
|
-
});
|
|
45
|
-
return mapped;
|
|
46
|
-
}),
|
|
47
|
-
),
|
|
48
|
-
);
|
|
49
|
-
},
|
|
50
|
-
suggest(args) {
|
|
51
|
-
return firstValueFrom(controller.suggest(args));
|
|
52
|
-
},
|
|
53
|
-
resolve(args) {
|
|
54
|
-
return firstValueFrom(controller.resolve(args));
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export default createResolver;
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { PeopleResolverProvider, default } from './PeopleResolverProvider';
|
package/src/make-resolver.tsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { lazy } from 'react';
|
|
2
|
-
import { PeopleResolverComponent } from './PeopleResolverComponent';
|
|
3
|
-
import { PersonController, type PersonControllerOptions } from './PersonController';
|
|
4
|
-
import { createResolver } from './create-resolver';
|
|
5
|
-
import type { IApiProvider } from '@equinor/fusion-framework-module-services';
|
|
6
|
-
|
|
7
|
-
export const makeResolver = (services: IApiProvider, options?: PersonControllerOptions) => {
|
|
8
|
-
return lazy(async () => {
|
|
9
|
-
const client = await services.createPeopleClient();
|
|
10
|
-
const controller = new PersonController(client, options);
|
|
11
|
-
const resolver = createResolver(controller);
|
|
12
|
-
const Component = ({ children }: { readonly children?: React.ReactNode }) => (
|
|
13
|
-
<PeopleResolverComponent resolver={resolver}>{children}</PeopleResolverComponent>
|
|
14
|
-
);
|
|
15
|
-
return {
|
|
16
|
-
default: Component,
|
|
17
|
-
};
|
|
18
|
-
});
|
|
19
|
-
};
|
package/src/types.ts
DELETED
package/src/version.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.react.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "dist/esm",
|
|
5
|
-
"rootDir": "src",
|
|
6
|
-
"declarationDir": "./dist/types"
|
|
7
|
-
},
|
|
8
|
-
"references": [
|
|
9
|
-
{
|
|
10
|
-
"path": "../../framework"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"path": "../../../modules/services"
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
"include": ["src/**/*"],
|
|
17
|
-
"exclude": ["node_modules", "lib"]
|
|
18
|
-
}
|