Skip to content

Commit f385f8a

Browse files
committed
#97 - Added ensureUser ability for tenant search
1 parent ca4bd7e commit f385f8a

File tree

5 files changed

+73
-30
lines changed

5 files changed

+73
-30
lines changed

docs/documentation/docs/controls/PeoplePicker.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ The People picker control can be configured with the following properties:
7373
| webAbsoluteUrl | string | no | Specify the site URL on which you want to perform the user query call. If not provided, the people picker will perform a tenant wide people/group search. When provided it will search users/groups on the provided site. | |
7474
| showHiddenInUI | boolean | no | Show users which are hidden from the UI. By default these users/groups hidden for the UI will not be shown. | false |
7575
| principalTypes | PrincipalType[] | no | Define which type of data you want to retrieve: User, SharePoint groups, Security groups. Multiple are possible. | |
76+
| ensureUser | boolean | no | When ensure user property is true, it will return the local user ID on the current site when doing a tenant wide search. | false |
7677
| suggestionsLimit | number | no | Maximum number of suggestions to show in the full suggestion list. | 5 |
7778
| resolveDelay | number | no | Add delay to resolve and search users | 200 |
7879

@@ -81,7 +82,7 @@ Enum `PrincipalType`
8182
The `PrincipalType` enum can be used to specify the types of information you want to query: User, Security groups, and/or SharePoint groups.
8283

8384
| Name | Value |
84-
| ---- | -- -- |
85+
| ---- | ---- |
8586
| User | 1 |
8687
| DistributionList | 2 |
8788
| SecurityGroup | 4 |

src/controls/peoplepicker/IPeoplePicker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ export interface IPeoplePickerProps {
8787
showHiddenInUI?: boolean;
8888
/**
8989
* Specify the user / group types to retrieve
90-
*
9190
*/
9291
principalTypes?: PrincipalType[];
92+
/**
93+
* When ensure user property is true, it will return the local user ID on the current site when doing a tenant wide search
94+
*/
95+
ensureUser?: boolean;
9396
}
9497

9598
export interface IPeoplePickerState {

src/controls/peoplepicker/PeoplePickerComponent.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,14 @@ import * as React from 'react';
33
import * as telemetry from '../../common/telemetry';
44
import styles from './PeoplePickerComponent.module.scss';
55
import SPPeopleSearchService from "../../services/PeopleSearchService";
6-
import { IPeoplePickerProps, IPeoplePickerState, IPeoplePickerUserItem } from './IPeoplePicker';
6+
import { IPeoplePickerProps, IPeoplePickerState } from './IPeoplePicker';
77
import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip';
88
import { NormalPeoplePicker } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePicker';
9-
import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar';
10-
import { SPHttpClient } from '@microsoft/sp-http';
11-
import { assign } from 'office-ui-fabric-react/lib/Utilities';
12-
import { IUsers } from './IUsers';
139
import { Label } from 'office-ui-fabric-react/lib/components/Label';
14-
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
1510
import { IBasePickerSuggestionsProps } from "office-ui-fabric-react/lib/components/pickers/BasePicker.types";
16-
import { IPersonaWithMenu } from "office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.types";
1711
import { IPersonaProps } from "office-ui-fabric-react/lib/components/Persona/Persona.types";
18-
import { MessageBarType } from "office-ui-fabric-react/lib/components/MessageBar";
19-
import { ValidationState } from 'office-ui-fabric-react/lib/components/pickers/BasePicker.types';
2012
import { Icon } from "office-ui-fabric-react/lib/components/Icon";
21-
import { isEqual, cloneDeep, uniqBy } from "@microsoft/sp-lodash-subset";
22-
import { MockUsers } from "../../services/PeoplePickerMockClient";
13+
import { isEqual, uniqBy } from "@microsoft/sp-lodash-subset";
2314

2415
/**
2516
* PeoplePicker component
@@ -75,7 +66,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
7566
if (this.props.defaultSelectedUsers && this.props.defaultSelectedUsers.length) {
7667
let selectedPersons: IPersonaProps[] = [];
7768
for (const userValue of this.props.defaultSelectedUsers) {
78-
const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName);
69+
const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
7970
if (userResult) {
8071
selectedPersons.push(userResult);
8172
}
@@ -85,7 +76,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
8576
selectedPersons
8677
});
8778
} else {
88-
const results = await this.peopleSearchService.searchPeople("", this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName);
79+
const results = await this.peopleSearchService.searchPeople("", this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
8980
this.setState({
9081
mostRecentlyUsedPersons: results.slice(0, this.suggestionsLimit)
9182
});
@@ -98,7 +89,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
9889
*/
9990
private onSearchFieldChanged = async (searchText: string, currentSelected: IPersonaProps[]): Promise<IPersonaProps[]> => {
10091
if (searchText.length > 2) {
101-
const results = await this.peopleSearchService.searchPeople(searchText, this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName);
92+
const results = await this.peopleSearchService.searchPeople(searchText, this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
10293
// Remove duplicates
10394
const { selectedPersons, mostRecentlyUsedPersons } = this.state;
10495
const filteredPersons = this.removeDuplicates(results, selectedPersons);

src/services/PeopleSearchService.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ import { ExtensionContext } from '@microsoft/sp-extension-base';
55
import { MockUsers, PeoplePickerMockClient } from './PeoplePickerMockClient';
66
import { PrincipalType, IPeoplePickerUserItem } from "../PeoplePicker";
77
import { IUsers, IUserInfo } from "../controls/peoplepicker/IUsers";
8-
import { cloneDeep } from "@microsoft/sp-lodash-subset";
8+
import { cloneDeep, findIndex } from "@microsoft/sp-lodash-subset";
99

1010
/**
1111
* Service implementation to search people in SharePoint
1212
*/
1313
export default class SPPeopleSearchService {
14-
private context: WebPartContext | ExtensionContext;
1514
private cachedPersonas: { [property: string]: IUserInfo[] };
15+
private cachedLocalUsers: { [siteUrl: string]: IUserInfo[] };
1616

1717
/**
1818
* Service constructor
1919
*/
20-
constructor(pageContext: WebPartContext | ExtensionContext) {
21-
this.context = pageContext;
20+
constructor(private context: WebPartContext | ExtensionContext) {
2221
this.cachedPersonas = {};
22+
this.cachedLocalUsers = {};
23+
this.cachedLocalUsers[this.context.pageContext.web.absoluteUrl] = [];
2324
}
2425

2526
/**
@@ -34,7 +35,7 @@ export default class SPPeopleSearchService {
3435
/**
3536
* Search person by its email or login name
3637
*/
37-
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null): Promise<IPeoplePickerUserItem> {
38+
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem> {
3839
if (Environment.type === EnvironmentType.Local) {
3940
// If the running environment is local, load the data from the mock
4041
const mockUsers = await this.searchPeopleFromMock(email);
@@ -47,7 +48,7 @@ export default class SPPeopleSearchService {
4748
return (userResults && userResults.length > 0) ? userResults[0] : null;
4849
} else {
4950
/* Global tenant search will be performed */
50-
const userResults = await this.searchTenant(email, 1, principalTypes);
51+
const userResults = await this.searchTenant(email, 1, principalTypes, ensureUser);
5152
return (userResults && userResults.length > 0) ? userResults[0] : null;
5253
}
5354
}
@@ -56,7 +57,7 @@ export default class SPPeopleSearchService {
5657
/**
5758
* Search All Users from the SharePoint People database
5859
*/
59-
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null): Promise<IPeoplePickerUserItem[]> {
60+
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem[]> {
6061
if (Environment.type === EnvironmentType.Local) {
6162
// If the running environment is local, load the data from the mock
6263
return this.searchPeopleFromMock(query);
@@ -67,7 +68,7 @@ export default class SPPeopleSearchService {
6768
return await this.localSearch(siteUrl, query, principalTypes, showHiddenInUI, groupName);
6869
} else {
6970
/* Global tenant search will be performed */
70-
return await this.searchTenant(query, maximumSuggestions, principalTypes);
71+
return await this.searchTenant(query, maximumSuggestions, principalTypes, ensureUser);
7172
}
7273
}
7374
}
@@ -149,7 +150,7 @@ export default class SPPeopleSearchService {
149150
/**
150151
* Tenant search
151152
*/
152-
private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[]): Promise<IPeoplePickerUserItem[]> {
153+
private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean): Promise<IPeoplePickerUserItem[]> {
153154
try {
154155
// If the running env is SharePoint, loads from the peoplepicker web service
155156
const userRequestUrl: string = `${this.context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
@@ -187,9 +188,23 @@ export default class SPPeopleSearchService {
187188
values = JSON.parse(userDataResp.value);
188189
}
189190

191+
// Filter out "UNVALIDATED_EMAIL_ADDRESS"
192+
values = values.filter(v => !(v.EntityData && v.EntityData.PrincipalType && v.EntityData.PrincipalType === "UNVALIDATED_EMAIL_ADDRESS"));
193+
194+
// Check if local user IDs need to be retrieved
195+
if (ensureUser) {
196+
for (const value of values) {
197+
const id = await this.ensureUser(value.Key || value.EntityData.SPGroupID);
198+
value.Key = id;
199+
}
200+
}
201+
202+
// Filter out NULL keys
203+
values = values.filter(v => v.Key !== null);
204+
190205
const userResults = values.map(element => {
191206
switch (element.EntityType) {
192-
case "User":
207+
case 'User':
193208
let email : string = element.EntityData.Email !== null ? element.EntityData.Email : element.Description;
194209
return {
195210
id: element.Key,
@@ -216,7 +231,7 @@ export default class SPPeopleSearchService {
216231
} as IPeoplePickerUserItem;
217232
default:
218233
return {
219-
id: element.EntityData.SPGroupID,
234+
id: element.Key,
220235
imageInitials: this.getFullNameInitials(element.DisplayText),
221236
text: element.DisplayText,
222237
secondaryText: element.EntityData.AccountName
@@ -236,6 +251,37 @@ export default class SPPeopleSearchService {
236251
}
237252
}
238253

254+
/**
255+
* Retrieves the local user ID
256+
*
257+
* @param userId
258+
*/
259+
private async ensureUser(userId: string): Promise<number> {
260+
const siteUrl = this.context.pageContext.web.absoluteUrl;
261+
if (this.cachedLocalUsers && this.cachedLocalUsers[siteUrl]) {
262+
const users = this.cachedLocalUsers[siteUrl];
263+
const userIdx = findIndex(users, u => u.LoginName === userId);
264+
if (userIdx !== -1) {
265+
return users[userIdx].Id;
266+
}
267+
}
268+
269+
const restApi = `${siteUrl}/_api/web/ensureuser`;
270+
const data = await this.context.spHttpClient.post(restApi, SPHttpClient.configurations.v1, {
271+
body: JSON.stringify({ 'logonName': userId })
272+
});
273+
274+
if (data.ok) {
275+
const user: IUserInfo = await data.json();
276+
if (user && user.Id) {
277+
this.cachedLocalUsers[siteUrl].push(user);
278+
return user.Id;
279+
}
280+
}
281+
282+
return null;
283+
}
284+
239285
/**
240286
* Generates Initials from a full name
241287
*/

src/webparts/controlsTest/components/ControlsTest.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,14 +236,16 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
236236

237237
<PeoplePicker context={this.props.context}
238238
titleText="People Picker (pre-set global users)"
239-
principalTypes={[PrincipalType.User]}
239+
principalTypes={[PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.SecurityGroup, PrincipalType.DistributionList]}
240240
defaultSelectedUsers={["[email protected]", "[email protected]"]}
241-
selectedItems={this._getPeoplePickerItems} />
241+
selectedItems={this._getPeoplePickerItems}
242+
personSelectionLimit={2}
243+
ensureUser={true} />
242244

243245
<PeoplePicker context={this.props.context}
244246
titleText="People Picker (pre-set local users)"
245247
webAbsoluteUrl={this.props.context.pageContext.site.absoluteUrl}
246-
principalTypes={[PrincipalType.User]}
248+
principalTypes={[PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.SecurityGroup, PrincipalType.DistributionList]}
247249
defaultSelectedUsers={["[email protected]", "[email protected]"]}
248250
selectedItems={this._getPeoplePickerItems} />
249251

0 commit comments

Comments
 (0)