diff --git a/docs/documentation/docs/controls/PeoplePicker.md b/docs/documentation/docs/controls/PeoplePicker.md index ff9430f9c..81985135f 100644 --- a/docs/documentation/docs/controls/PeoplePicker.md +++ b/docs/documentation/docs/controls/PeoplePicker.md @@ -61,7 +61,7 @@ The People picker control can be configured with the following properties: | context | BaseComponentContext | yes | Context of the current web part. | | | titleText | string | no | Text to be displayed on the control | | | groupName | string | no | Group from which users are fetched. Leave it blank if need to filter all users. When both groupName and groupId specified groupName takes precedence. | _none_ | -| groupId | number | no | Group from which users are fetched. Leave it blank if need to filter all users. When both groupId and groupName specified groupName takes precedence. | _none_ | +| groupId | number \| string | no | Group from which users are fetched. Leave it blank if need to filter all users. When both groupId and groupName specified groupName takes precedence. If string is specified, Microsoft 365 Group is used | _none_ | | personSelectionLimit | number | no | Defines the limit of people that can be selected in the control | 1 | | required | boolean | no | Set if the control is required or not | false | | disabled | boolean | no | Set if the control is disabled or not | false | @@ -93,4 +93,13 @@ The `PrincipalType` enum can be used to specify the types of information you wan | SecurityGroup | 4 | | SharePointGroup | 8 | + + ## MSGraph Permissions required + +This control requires the following scopes if groupId is of type String: + +at least : GroupMember.Read.All, Directory.Read.All + + ![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/PeoplePicker) + diff --git a/src/common/dal/ListItemRepository.ts b/src/common/dal/ListItemRepository.ts index ce5d22ab6..529140743 100644 --- a/src/common/dal/ListItemRepository.ts +++ b/src/common/dal/ListItemRepository.ts @@ -19,7 +19,7 @@ export class ListItemRepository { const webAbsoluteUrl = !webUrl ? this.SiteUrl : webUrl; let apiUrl = `${webAbsoluteUrl}/_api/web/lists('${listId}')/items?$select=${keyInternalColumnName || 'Id'},${internalColumnName}&$filter=${filterText}&$top=${top}`; if (orderBy) { - apiUrl += `&$orderBy=${orderBy}` + apiUrl += `&$orderBy=${orderBy}`; } const data = await this.SPClient.get(apiUrl, SPHttpClient.configurations.v1); if (data.ok) { diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index 8577d6e68..bff933b4e 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -29,10 +29,10 @@ export interface IPeoplePickerProps { */ groupName?: string; /** - * Id of SharePoint Group + * Id of SharePoint Group (Number) or Office365 Group (String) */ - groupId?: number; - /** + groupId?: number | string; + /** * Maximum number of suggestions to show in the full suggestion list. (default: 5) */ suggestionsLimit?: number; diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index d9966a5ce..3fb908481 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -19,7 +19,7 @@ import uniqBy from 'lodash/uniqBy'; export class PeoplePicker extends React.Component { private peopleSearchService: SPPeopleSearchService; private suggestionsLimit: number; - private groupId: number; + private groupId: number | string; constructor(props: IPeoplePickerProps) { super(props); diff --git a/src/services/PeopleSearchService.ts b/src/services/PeopleSearchService.ts index f59239042..f03c4d386 100644 --- a/src/services/PeopleSearchService.ts +++ b/src/services/PeopleSearchService.ts @@ -5,6 +5,11 @@ import { MockUsers, PeoplePickerMockClient } from './PeoplePickerMockClient'; import { PrincipalType, IPeoplePickerUserItem } from "../PeoplePicker"; import { IUsers, IUserInfo } from "../controls/peoplepicker/IUsers"; import { cloneDeep, findIndex } from "@microsoft/sp-lodash-subset"; +import { sp } from '@pnp/sp'; +import "@pnp/sp/sputilities"; +import { Web } from "@pnp/sp/webs"; +import "@pnp/sp/webs"; +import "@pnp/sp/site-users/web"; /** * Service implementation to search people in SharePoint @@ -20,6 +25,8 @@ export default class SPPeopleSearchService { this.cachedPersonas = {}; this.cachedLocalUsers = {}; this.cachedLocalUsers[this.context.pageContext.web.absoluteUrl] = []; + // Setup PnPjs + sp.setup(this.context); } /** @@ -62,7 +69,7 @@ export default class SPPeopleSearchService { /** * Search person by its email or login name */ - public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number = null, ensureUser: boolean = false): Promise { + public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number | 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); @@ -76,7 +83,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, groupId: number = null, ensureUser: boolean = false): Promise { + public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number | 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); @@ -162,7 +169,7 @@ export default class SPPeopleSearchService { /** * Tenant search */ - private async searchTenant(siteUrl: string, query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean, groupId: number): Promise { + private async searchTenant(siteUrl: string, query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean, groupId: number | string): Promise { try { // If the running env is SharePoint, loads from the peoplepicker web service const userRequestUrl: string = `${siteUrl || this.context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`; @@ -183,11 +190,49 @@ export default class SPPeopleSearchService { searchBody.queryParams["SharePointGroupID"] = 0; } - // Check if users need to be searched in a specific group - if (groupId) { + // Check if users need to be searched in a specific SharePoint Group + if (groupId && typeof(groupId) === 'number') { searchBody.queryParams["SharePointGroupID"] = groupId; } + // Check if users need to be searched in a specific Office365 Group + else if(groupId && typeof(groupId) === 'string') { + const graphUserRequestUrl = `/groups/${groupId}/members?$count=true&$search="displayName:${query}" OR "mail:${query}"`; + const graphClient = await this.context.msGraphClientFactory.getClient(); + const graphUserResponse = await graphClient.api(graphUserRequestUrl).header('ConsistencyLevel', 'eventual').get(); + + if(graphUserResponse.value && graphUserResponse.value.length > 0) { + + // Get user loginName from user email + const _users = []; + const batch = Web(this.context.pageContext.web.absoluteUrl).createBatch(); + for (const value of graphUserResponse.value) { + sp.web.inBatch(batch).ensureUser(value.userPrincipalName).then(u => _users.push(u.data)); + } + + await batch.execute(); + + let userResult: IPeoplePickerUserItem[] = []; + for (const user of _users) { + userResult.push({ + id: ensureUser ? user.Id : user.LoginName, + loginName: user.LoginName, + imageUrl: this.generateUserPhotoLink(user.Email), + imageInitials: this.getFullNameInitials(user.Title), + text: user.Title, // name + secondaryText: user.Email, // email + tertiaryText: '', // status + optionalText: '' // anything + }); + } + + return userResult; + } + + //Nothing to return + return []; + } + const httpPostOptions: ISPHttpClientOptions = { headers: { 'accept': 'application/json',