Skip to content

Commit

Permalink
#97 - Added ensureUser ability for tenant search
Browse files Browse the repository at this point in the history
  • Loading branch information
estruyf committed Dec 17, 2018
1 parent ca4bd7e commit f385f8a
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 30 deletions.
3 changes: 2 additions & 1 deletion docs/documentation/docs/controls/PeoplePicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The People picker control can be configured with the following properties:
| 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. | |
| 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 |
| principalTypes | PrincipalType[] | no | Define which type of data you want to retrieve: User, SharePoint groups, Security groups. Multiple are possible. | |
| 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 |
| suggestionsLimit | number | no | Maximum number of suggestions to show in the full suggestion list. | 5 |
| resolveDelay | number | no | Add delay to resolve and search users | 200 |

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

| Name | Value |
| ---- | -- -- |
| ---- | ---- |
| User | 1 |
| DistributionList | 2 |
| SecurityGroup | 4 |
Expand Down
5 changes: 4 additions & 1 deletion src/controls/peoplepicker/IPeoplePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,12 @@ export interface IPeoplePickerProps {
showHiddenInUI?: boolean;
/**
* Specify the user / group types to retrieve
*
*/
principalTypes?: PrincipalType[];
/**
* When ensure user property is true, it will return the local user ID on the current site when doing a tenant wide search
*/
ensureUser?: boolean;
}

export interface IPeoplePickerState {
Expand Down
19 changes: 5 additions & 14 deletions src/controls/peoplepicker/PeoplePickerComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,14 @@ import * as React from 'react';
import * as telemetry from '../../common/telemetry';
import styles from './PeoplePickerComponent.module.scss';
import SPPeopleSearchService from "../../services/PeopleSearchService";
import { IPeoplePickerProps, IPeoplePickerState, IPeoplePickerUserItem } from './IPeoplePicker';
import { IPeoplePickerProps, IPeoplePickerState } from './IPeoplePicker';
import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip';
import { NormalPeoplePicker } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePicker';
import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar';
import { SPHttpClient } from '@microsoft/sp-http';
import { assign } from 'office-ui-fabric-react/lib/Utilities';
import { IUsers } from './IUsers';
import { Label } from 'office-ui-fabric-react/lib/components/Label';
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
import { IBasePickerSuggestionsProps } from "office-ui-fabric-react/lib/components/pickers/BasePicker.types";
import { IPersonaWithMenu } from "office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.types";
import { IPersonaProps } from "office-ui-fabric-react/lib/components/Persona/Persona.types";
import { MessageBarType } from "office-ui-fabric-react/lib/components/MessageBar";
import { ValidationState } from 'office-ui-fabric-react/lib/components/pickers/BasePicker.types';
import { Icon } from "office-ui-fabric-react/lib/components/Icon";
import { isEqual, cloneDeep, uniqBy } from "@microsoft/sp-lodash-subset";
import { MockUsers } from "../../services/PeoplePickerMockClient";
import { isEqual, uniqBy } from "@microsoft/sp-lodash-subset";

/**
* PeoplePicker component
Expand Down Expand Up @@ -75,7 +66,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
if (this.props.defaultSelectedUsers && this.props.defaultSelectedUsers.length) {
let selectedPersons: IPersonaProps[] = [];
for (const userValue of this.props.defaultSelectedUsers) {
const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName);
const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
if (userResult) {
selectedPersons.push(userResult);
}
Expand All @@ -85,7 +76,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
selectedPersons
});
} else {
const results = await this.peopleSearchService.searchPeople("", this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName);
const results = await this.peopleSearchService.searchPeople("", this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
this.setState({
mostRecentlyUsedPersons: results.slice(0, this.suggestionsLimit)
});
Expand All @@ -98,7 +89,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
*/
private onSearchFieldChanged = async (searchText: string, currentSelected: IPersonaProps[]): Promise<IPersonaProps[]> => {
if (searchText.length > 2) {
const results = await this.peopleSearchService.searchPeople(searchText, this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName);
const results = await this.peopleSearchService.searchPeople(searchText, this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
// Remove duplicates
const { selectedPersons, mostRecentlyUsedPersons } = this.state;
const filteredPersons = this.removeDuplicates(results, selectedPersons);
Expand Down
68 changes: 57 additions & 11 deletions src/services/PeopleSearchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import { ExtensionContext } from '@microsoft/sp-extension-base';
import { MockUsers, PeoplePickerMockClient } from './PeoplePickerMockClient';
import { PrincipalType, IPeoplePickerUserItem } from "../PeoplePicker";
import { IUsers, IUserInfo } from "../controls/peoplepicker/IUsers";
import { cloneDeep } from "@microsoft/sp-lodash-subset";
import { cloneDeep, findIndex } from "@microsoft/sp-lodash-subset";

/**
* Service implementation to search people in SharePoint
*/
export default class SPPeopleSearchService {
private context: WebPartContext | ExtensionContext;
private cachedPersonas: { [property: string]: IUserInfo[] };
private cachedLocalUsers: { [siteUrl: string]: IUserInfo[] };

/**
* Service constructor
*/
constructor(pageContext: WebPartContext | ExtensionContext) {
this.context = pageContext;
constructor(private context: WebPartContext | ExtensionContext) {
this.cachedPersonas = {};
this.cachedLocalUsers = {};
this.cachedLocalUsers[this.context.pageContext.web.absoluteUrl] = [];
}

/**
Expand All @@ -34,7 +35,7 @@ export default class SPPeopleSearchService {
/**
* Search person by its email or login name
*/
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null): Promise<IPeoplePickerUserItem> {
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem> {
if (Environment.type === EnvironmentType.Local) {
// If the running environment is local, load the data from the mock
const mockUsers = await this.searchPeopleFromMock(email);
Expand All @@ -47,7 +48,7 @@ export default class SPPeopleSearchService {
return (userResults && userResults.length > 0) ? userResults[0] : null;
} else {
/* Global tenant search will be performed */
const userResults = await this.searchTenant(email, 1, principalTypes);
const userResults = await this.searchTenant(email, 1, principalTypes, ensureUser);
return (userResults && userResults.length > 0) ? userResults[0] : null;
}
}
Expand All @@ -56,7 +57,7 @@ export default class SPPeopleSearchService {
/**
* Search All Users from the SharePoint People database
*/
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null): Promise<IPeoplePickerUserItem[]> {
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem[]> {
if (Environment.type === EnvironmentType.Local) {
// If the running environment is local, load the data from the mock
return this.searchPeopleFromMock(query);
Expand All @@ -67,7 +68,7 @@ export default class SPPeopleSearchService {
return await this.localSearch(siteUrl, query, principalTypes, showHiddenInUI, groupName);
} else {
/* Global tenant search will be performed */
return await this.searchTenant(query, maximumSuggestions, principalTypes);
return await this.searchTenant(query, maximumSuggestions, principalTypes, ensureUser);
}
}
}
Expand Down Expand Up @@ -149,7 +150,7 @@ export default class SPPeopleSearchService {
/**
* Tenant search
*/
private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[]): Promise<IPeoplePickerUserItem[]> {
private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean): Promise<IPeoplePickerUserItem[]> {
try {
// If the running env is SharePoint, loads from the peoplepicker web service
const userRequestUrl: string = `${this.context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
Expand Down Expand Up @@ -187,9 +188,23 @@ export default class SPPeopleSearchService {
values = JSON.parse(userDataResp.value);
}

// Filter out "UNVALIDATED_EMAIL_ADDRESS"
values = values.filter(v => !(v.EntityData && v.EntityData.PrincipalType && v.EntityData.PrincipalType === "UNVALIDATED_EMAIL_ADDRESS"));

// Check if local user IDs need to be retrieved
if (ensureUser) {
for (const value of values) {
const id = await this.ensureUser(value.Key || value.EntityData.SPGroupID);
value.Key = id;
}
}

// Filter out NULL keys
values = values.filter(v => v.Key !== null);

const userResults = values.map(element => {
switch (element.EntityType) {
case "User":
case 'User':
let email : string = element.EntityData.Email !== null ? element.EntityData.Email : element.Description;
return {
id: element.Key,
Expand All @@ -216,7 +231,7 @@ export default class SPPeopleSearchService {
} as IPeoplePickerUserItem;
default:
return {
id: element.EntityData.SPGroupID,
id: element.Key,
imageInitials: this.getFullNameInitials(element.DisplayText),
text: element.DisplayText,
secondaryText: element.EntityData.AccountName
Expand All @@ -236,6 +251,37 @@ export default class SPPeopleSearchService {
}
}

/**
* Retrieves the local user ID
*
* @param userId
*/
private async ensureUser(userId: string): Promise<number> {
const siteUrl = this.context.pageContext.web.absoluteUrl;
if (this.cachedLocalUsers && this.cachedLocalUsers[siteUrl]) {
const users = this.cachedLocalUsers[siteUrl];
const userIdx = findIndex(users, u => u.LoginName === userId);
if (userIdx !== -1) {
return users[userIdx].Id;
}
}

const restApi = `${siteUrl}/_api/web/ensureuser`;
const data = await this.context.spHttpClient.post(restApi, SPHttpClient.configurations.v1, {
body: JSON.stringify({ 'logonName': userId })
});

if (data.ok) {
const user: IUserInfo = await data.json();
if (user && user.Id) {
this.cachedLocalUsers[siteUrl].push(user);
return user.Id;
}
}

return null;
}

/**
* Generates Initials from a full name
*/
Expand Down
8 changes: 5 additions & 3 deletions src/webparts/controlsTest/components/ControlsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,16 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC

<PeoplePicker context={this.props.context}
titleText="People Picker (pre-set global users)"
principalTypes={[PrincipalType.User]}
principalTypes={[PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.SecurityGroup, PrincipalType.DistributionList]}
defaultSelectedUsers={["admin@tenant.onmicrosoft.com", "test@tenant.onmicrosoft.com"]}
selectedItems={this._getPeoplePickerItems} />
selectedItems={this._getPeoplePickerItems}
personSelectionLimit={2}
ensureUser={true} />

<PeoplePicker context={this.props.context}
titleText="People Picker (pre-set local users)"
webAbsoluteUrl={this.props.context.pageContext.site.absoluteUrl}
principalTypes={[PrincipalType.User]}
principalTypes={[PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.SecurityGroup, PrincipalType.DistributionList]}
defaultSelectedUsers={["admin@tenant.onmicrosoft.com", "test@tenant.onmicrosoft.com"]}
selectedItems={this._getPeoplePickerItems} />

Expand Down

0 comments on commit f385f8a

Please sign in to comment.