Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added filter for Microsoft 365 Group to PeoplePicker #1027

Merged
merged 7 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/documentation/docs/controls/PeoplePicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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,


![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/PeoplePicker)

6 changes: 3 additions & 3 deletions src/controls/peoplepicker/IPeoplePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/controls/peoplepicker/PeoplePickerComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import uniqBy from 'lodash/uniqBy';
export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePickerState> {
private peopleSearchService: SPPeopleSearchService;
private suggestionsLimit: number;
private groupId: number;
private groupId: number | string;

constructor(props: IPeoplePickerProps) {
super(props);
Expand Down
53 changes: 48 additions & 5 deletions src/services/PeopleSearchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,7 +67,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<IPeoplePickerUserItem> {
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number | 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 @@ -76,7 +81,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<IPeoplePickerUserItem[]> {
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number | 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 Down Expand Up @@ -162,7 +167,7 @@ export default class SPPeopleSearchService {
/**
* Tenant search
*/
private async searchTenant(siteUrl: string, query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean, groupId: number): Promise<IPeoplePickerUserItem[]> {
private async searchTenant(siteUrl: string, query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean, groupId: number | string): Promise<IPeoplePickerUserItem[]> {
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`;
Expand All @@ -183,11 +188,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 = `https://graph.microsoft.com/v1.0/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',
Expand Down
24 changes: 18 additions & 6 deletions src/webparts/controlsTest/ControlsTestWebPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
ThemeProvider,
} from "@microsoft/sp-component-base";
import ControlsTest from './components/ControlsTest';
import ControlsTest_SingleComponent from './components/ControlsTest_SingleComponent';
import { sp } from '@pnp/sp';
/**
* Web part to test the React controls
*/
Expand All @@ -27,7 +29,7 @@ export default class ControlsTestWebPart extends BaseClientSideWebPart<IControls
private _themeVariant: IReadonlyTheme | undefined;
protected async onInit(): Promise<void> {


sp.setup(this.context);
this._themeProvider = this.context.serviceScope.consume(
ThemeProvider.serviceKey
);
Expand Down Expand Up @@ -78,18 +80,28 @@ export default class ControlsTestWebPart extends BaseClientSideWebPart<IControls

const element: React.ReactElement<IControlsTestProps> = React.createElement(

ControlsTest,
{
// ControlsTest,
// {

themeVariant: this._themeVariant,
// themeVariant: this._themeVariant,
// context: this.context,
// description: this.properties.description,
// title: this.properties.title,
// displayMode: this.displayMode,
// updateProperty: (value: string) => {
// this.properties.title = value;
// },
// totalPages: this.properties.totalPages
// }
ControlsTest_SingleComponent,
{
context: this.context,
description: this.properties.description,
title: this.properties.title,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
},
totalPages: this.properties.totalPages
}
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ListPicker } from '../../../ListPicker';
import { IFrameDialog } from '../../../IFrameDialog';
import { IFramePanel } from '../../../IFramePanel';
import { PanelType } from 'office-ui-fabric-react/lib/Panel';
import { Environment, EnvironmentType, DisplayMode } from '@microsoft/sp-core-library';
import { Environment, EnvironmentType, DisplayMode, Guid } from '@microsoft/sp-core-library';
import { SecurityTrimmedControl, PermissionLevel } from '../../../SecurityTrimmedControl';
import { SPPermission } from '@microsoft/sp-page-context';
import { PeoplePicker, PrincipalType } from '../../../PeoplePicker';
Expand Down Expand Up @@ -460,15 +460,12 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC

return (
<div className={styles.controlsTest}>
<DynamicForm
<PeoplePicker
context={this.props.context}
listId={"b1416fca-dc77-4198-a082-62a7657dcfa9"}
//contentTypeId={"0x0104003FD42C31C9FABD4E8D2821289F34BBEF"}
//listItemId={13}
onCancelled={() => { console.log('Cancelled'); }}
onSubmitted={async (listItem) => { console.log(listItem); }}>

</DynamicForm>
onChange={users => console.log(users)}
ensureUser
groupId={2}
/>
</div>
);
}
Expand Down