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