diff --git a/docs/documentation/docs/controls/PeoplePicker.md b/docs/documentation/docs/controls/PeoplePicker.md index 031172b4d..87846ed05 100644 --- a/docs/documentation/docs/controls/PeoplePicker.md +++ b/docs/documentation/docs/controls/PeoplePicker.md @@ -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 | @@ -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 | diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index f29e53c9f..72d1c7fbe 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -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 { diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index 94c28ce64..ffce6e597 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -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 @@ -75,7 +66,7 @@ export class PeoplePicker extends React.Component => { 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); diff --git a/src/services/PeopleSearchService.ts b/src/services/PeopleSearchService.ts index b527d8651..9f818cdff 100644 --- a/src/services/PeopleSearchService.ts +++ b/src/services/PeopleSearchService.ts @@ -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] = []; } /** @@ -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 { + public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise { if (Environment.type === EnvironmentType.Local) { // If the running environment is local, load the data from the mock const mockUsers = await this.searchPeopleFromMock(email); @@ -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; } } @@ -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 { + public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise { if (Environment.type === EnvironmentType.Local) { // If the running environment is local, load the data from the mock return this.searchPeopleFromMock(query); @@ -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); } } } @@ -149,7 +150,7 @@ export default class SPPeopleSearchService { /** * Tenant search */ - private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[]): Promise { + private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean): Promise { 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`; @@ -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, @@ -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 @@ -236,6 +251,37 @@ export default class SPPeopleSearchService { } } + /** + * Retrieves the local user ID + * + * @param userId + */ + private async ensureUser(userId: string): Promise { + 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 */ diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index ed6934a08..f0597cb9f 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -236,14 +236,16 @@ export default class ControlsTest extends React.Component + selectedItems={this._getPeoplePickerItems} + personSelectionLimit={2} + ensureUser={true} />