Skip to content

Commit 5f7df59

Browse files
committed
#161 - Fix for finding SharePoint groups
1 parent 41603e2 commit 5f7df59

File tree

10 files changed

+115
-60
lines changed

10 files changed

+115
-60
lines changed

docs/documentation/docs/controls/PeoplePicker.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ The People picker control can be configured with the following properties:
7171
| peoplePickerCntrlclassName | string | no | applies custom styling to the people picker control only | |
7272
| defaultSelectedUsers | string[] | no | Default selected user emails or login names | |
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. | |
74-
| 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 |
7574
| principalTypes | PrincipalType[] | no | Define which type of data you want to retrieve: User, SharePoint groups, Security groups. Multiple are possible. | |
7675
| 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 |
7776
| suggestionsLimit | number | no | Maximum number of suggestions to show in the full suggestion list. | 5 |

src/controls/peoplepicker/IPeoplePicker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface IPeoplePickerProps {
1818
*/
1919
titleText?: string;
2020
/**
21-
* Web Absolute Url of source site
21+
* Web Absolute Url of source site. When this is provided, a search request is done to the local site.
2222
*/
2323
webAbsoluteUrl?: string;
2424
/**
@@ -82,6 +82,7 @@ export interface IPeoplePickerProps {
8282
*/
8383
defaultSelectedUsers?: string[];
8484
/**
85+
* @deprecated
8586
* Show users which are hidden from the UI
8687
*/
8788
showHiddenInUI?: boolean;
@@ -97,7 +98,8 @@ export interface IPeoplePickerProps {
9798

9899
export interface IPeoplePickerState {
99100
mostRecentlyUsedPersons: IPersonaProps[];
100-
showmessageerror: boolean;
101+
showRequiredError: boolean;
102+
errorMessage: string;
101103
resolveDelay : number;
102104

103105
selectedPersons?: IPersonaProps[];

src/controls/peoplepicker/PeoplePickerComponent.tsx

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { isEqual, uniqBy } from "@microsoft/sp-lodash-subset";
1818
export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePickerState> {
1919
private peopleSearchService: SPPeopleSearchService;
2020
private suggestionsLimit: number;
21+
private groupId: number;
2122

2223
constructor(props: IPeoplePickerProps) {
2324
super(props);
@@ -34,8 +35,9 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
3435
this.state = {
3536
selectedPersons: [],
3637
mostRecentlyUsedPersons: [],
37-
showmessageerror: false,
38-
resolveDelay: this.props.resolveDelay || 200
38+
showRequiredError: false,
39+
resolveDelay: this.props.resolveDelay || 200,
40+
errorMessage: null
3941
};
4042
}
4143

@@ -52,7 +54,9 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
5254
* componentWillUpdate lifecycle hook
5355
*/
5456
public componentWillUpdate(nextProps: IPeoplePickerProps, nextState: IPeoplePickerState): void {
55-
if (!isEqual(this.props.defaultSelectedUsers, nextProps.defaultSelectedUsers)) {
57+
if (!isEqual(this.props.defaultSelectedUsers, nextProps.defaultSelectedUsers) ||
58+
this.props.groupName !== nextProps.groupName ||
59+
this.props.webAbsoluteUrl !== nextProps.webAbsoluteUrl) {
5660
this.getInitialPersons();
5761
}
5862
}
@@ -62,11 +66,25 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
6266
* Get initial persons
6367
*/
6468
private async getInitialPersons() {
69+
const { groupName } = this.props;
70+
// Check if a group property was provided, and get the group ID
71+
if (groupName) {
72+
this.groupId = await this.peopleSearchService.getGroupId(this.props.groupName, this.props.webAbsoluteUrl);
73+
if (!this.groupId) {
74+
this.setState({
75+
errorMessage: "Group could not be found."
76+
});
77+
return;
78+
}
79+
} else {
80+
this.groupId = null;
81+
}
82+
6583
// Check for default user values
6684
if (this.props.defaultSelectedUsers && this.props.defaultSelectedUsers.length) {
6785
let selectedPersons: IPersonaProps[] = [];
6886
for (const userValue of this.props.defaultSelectedUsers) {
69-
const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
87+
const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.groupId, this.props.ensureUser);
7088
if (userResult) {
7189
selectedPersons.push(userResult);
7290
}
@@ -75,11 +93,6 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
7593
this.setState({
7694
selectedPersons
7795
});
78-
} else {
79-
const results = await this.peopleSearchService.searchPeople("", this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.props.showHiddenInUI, this.props.groupName, this.props.ensureUser);
80-
this.setState({
81-
mostRecentlyUsedPersons: results.slice(0, this.suggestionsLimit)
82-
});
8396
}
8497
}
8598

@@ -89,7 +102,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
89102
*/
90103
private onSearchFieldChanged = async (searchText: string, currentSelected: IPersonaProps[]): Promise<IPersonaProps[]> => {
91104
if (searchText.length > 2) {
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);
105+
const results = await this.peopleSearchService.searchPeople(searchText, this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.groupId, this.props.ensureUser);
93106
// Remove duplicates
94107
const { selectedPersons, mostRecentlyUsedPersons } = this.state;
95108
const filteredPersons = this.removeDuplicates(results, selectedPersons);
@@ -113,7 +126,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
113126

114127
this.setState({
115128
selectedPersons: items,
116-
showmessageerror: items.length > 0 ? false : true
129+
showRequiredError: items.length > 0 ? false : true
117130
});
118131

119132
if (triggerUpdate) {
@@ -187,7 +200,7 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
187200
}}
188201
selectedItems={this.state.selectedPersons}
189202
itemLimit={this.props.personSelectionLimit || 1}
190-
disabled={this.props.disabled}
203+
disabled={this.props.disabled || !!this.state.errorMessage}
191204
onChange={this.onChange}
192205
resolveDelay={this.state.resolveDelay} />
193206
</div>
@@ -211,10 +224,16 @@ export class PeoplePicker extends React.Component<IPeoplePickerProps, IPeoplePic
211224
}
212225

213226
{
214-
(this.props.isRequired && this.state.showmessageerror) && (
227+
(this.props.isRequired && this.state.showRequiredError) || (this.state.errorMessage) && (
215228
<p className={`ms-TextField-errorMessage ${styles.errorMessage} ${this.props.errorMessageClassName ? this.props.errorMessageClassName : ''}`}>
216229
<Icon iconName='Error' className={styles.errorIcon} />
217-
<span data-automation-id="error-message">{this.props.errorMessage ? this.props.errorMessage : strings.peoplePickerComponentErrorMessage}</span>
230+
{
231+
this.state.errorMessage && <span data-automation-id="error-message">{this.state.errorMessage}</span>
232+
}
233+
234+
{
235+
(this.props.isRequired && this.state.showRequiredError) && <span data-automation-id="error-message">{this.props.errorMessage ? this.props.errorMessage : strings.peoplePickerComponentErrorMessage}</span>
236+
}
218237
</p>
219238
)
220239
}

src/loc/de-de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ define([], () => {
5151
peoplePickerSuggestionsHeaderText: 'Vorgeschlagene Benutzer',
5252
peoplePickerLoadingText: 'Laden',
5353
PeoplePickerSearchText: 'Fetching users',
54+
PeoplePickerGroupNotFound: "Group could not be found.",
5455

5556
ListItemPickerSelectValue: 'Wähle Wert',
5657

src/loc/en-us.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ define([], () => {
5151
peoplePickerSuggestionsHeaderText: 'Suggested People',
5252
peoplePickerLoadingText: 'Loading',
5353
PeoplePickerSearchText: 'Fetching users',
54+
PeoplePickerGroupNotFound: "Group could not be found.",
5455

5556
ListItemPickerSelectValue: 'Select value',
5657

src/loc/fr-fr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ define([], () => {
5151
peoplePickerSuggestionsHeaderText: 'Personnes suggérées',
5252
peoplePickerLoadingText: 'Chargement',
5353
PeoplePickerSearchText: 'Fetching users',
54+
PeoplePickerGroupNotFound: "Group could not be found.",
5455

5556
ListItemPickerSelectValue: 'Sélectionnez une valeur',
5657

src/loc/mystrings.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
declare interface IControlStrings {
2+
PeoplePickerGroupNotFound: string;
23
ListViewFilterLabel: string;
34

45
PeoplePickerSearchText: string;

src/loc/nl-nl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ define([], () => {
5151
peoplePickerSuggestionsHeaderText: 'Voorgestelde personen',
5252
peoplePickerLoadingText: 'Laden',
5353
PeoplePickerSearchText: 'Personen worden opgehaald',
54+
PeoplePickerGroupNotFound: "Groep niet gevonden.",
5455

5556
ListItemPickerSelectValue: 'Selecteer veld',
5657

src/services/PeopleSearchService.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,44 +32,44 @@ export default class SPPeopleSearchService {
3232
return `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${value}&UA=0&size=HR96x96`;
3333
}
3434

35+
/**
36+
* Retrieve the specified group
37+
*
38+
* @param groupName
39+
* @param siteUrl
40+
*/
41+
public async getGroupId(groupName: string, siteUrl: string = null): Promise<number | null> {
42+
if (Environment.type === EnvironmentType.Local) {
43+
return 1;
44+
} else {
45+
const groups = await this.searchTenant(siteUrl, groupName, 1, [PrincipalType.SharePointGroup], false, 0);
46+
return (groups && groups.length > 0) ? parseInt(groups[0].id) : null;
47+
}
48+
}
49+
3550
/**
3651
* Search person by its email or login name
3752
*/
38-
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem> {
53+
public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem> {
3954
if (Environment.type === EnvironmentType.Local) {
4055
// If the running environment is local, load the data from the mock
4156
const mockUsers = await this.searchPeopleFromMock(email);
4257
return (mockUsers && mockUsers.length > 0) ? mockUsers[0] : null;
4358
} else {
44-
// Check the action to perform
45-
if (siteUrl) {
46-
/* Local site search will be performed */
47-
const userResults = await this.localSearch(siteUrl, email, principalTypes, showHiddenInUI, groupName, true);
48-
return (userResults && userResults.length > 0) ? userResults[0] : null;
49-
} else {
50-
/* Global tenant search will be performed */
51-
const userResults = await this.searchTenant(email, 1, principalTypes, ensureUser);
52-
return (userResults && userResults.length > 0) ? userResults[0] : null;
53-
}
59+
const userResults = await this.searchTenant(siteUrl, email, 1, principalTypes, ensureUser, groupId);
60+
return (userResults && userResults.length > 0) ? userResults[0] : null;
5461
}
5562
}
5663

5764
/**
5865
* Search All Users from the SharePoint People database
5966
*/
60-
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, showHiddenInUI: boolean = false, groupName: string = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem[]> {
67+
public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number = null, ensureUser: boolean = false): Promise<IPeoplePickerUserItem[]> {
6168
if (Environment.type === EnvironmentType.Local) {
6269
// If the running environment is local, load the data from the mock
6370
return this.searchPeopleFromMock(query);
6471
} else {
65-
// Check the action to perform
66-
if (siteUrl) {
67-
/* Local site search will be performed */
68-
return await this.localSearch(siteUrl, query, principalTypes, showHiddenInUI, groupName);
69-
} else {
70-
/* Global tenant search will be performed */
71-
return await this.searchTenant(query, maximumSuggestions, principalTypes, ensureUser);
72-
}
72+
return await this.searchTenant(siteUrl, query, maximumSuggestions, principalTypes, ensureUser, groupId);
7373
}
7474
}
7575

@@ -150,25 +150,35 @@ export default class SPPeopleSearchService {
150150
/**
151151
* Tenant search
152152
*/
153-
private async searchTenant(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean): Promise<IPeoplePickerUserItem[]> {
153+
private async searchTenant(siteUrl: string, query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean, groupId: number): Promise<IPeoplePickerUserItem[]> {
154154
try {
155155
// If the running env is SharePoint, loads from the peoplepicker web service
156-
const userRequestUrl: string = `${this.context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
156+
const userRequestUrl: string = `${siteUrl || this.context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
157157
const searchBody = {
158-
'queryParams': {
159-
'AllowEmailAddresses': true,
160-
'AllowMultipleEntities': false,
161-
'AllUrlZones': false,
162-
'MaximumEntitySuggestions': maximumSuggestions,
163-
'PrincipalSource': 15,
158+
queryParams: {
159+
AllowEmailAddresses: true,
160+
AllowMultipleEntities: false,
161+
AllUrlZones: false,
162+
MaximumEntitySuggestions: maximumSuggestions,
163+
PrincipalSource: 15,
164164
// PrincipalType controls the type of entities that are returned in the results.
165165
// Choices are All - 15, Distribution List - 2 , Security Groups - 4, SharePoint Groups - 8, User - 1.
166166
// These values can be combined (example: 13 is security + SP groups + users)
167-
'PrincipalType': !!principalTypes && principalTypes.length > 0 ? principalTypes.reduce((a, b) => a + b, 0) : 1,
168-
'QueryString': query
167+
PrincipalType: !!principalTypes && principalTypes.length > 0 ? principalTypes.reduce((a, b) => a + b, 0) : 1,
168+
QueryString: query
169169
}
170170
};
171171

172+
// Search on the local site when "0"
173+
if (siteUrl) {
174+
searchBody.queryParams["SharePointGroupID"] = 0;
175+
}
176+
177+
// Check if users need to be searched in a specific group
178+
if (groupId) {
179+
searchBody.queryParams["SharePointGroupID"] = groupId;
180+
}
181+
172182
const httpPostOptions: ISPHttpClientOptions = {
173183
headers: {
174184
'accept': 'application/json',
@@ -194,8 +204,11 @@ export default class SPPeopleSearchService {
194204
// Check if local user IDs need to be retrieved
195205
if (ensureUser) {
196206
for (const value of values) {
197-
const id = await this.ensureUser(value.Key || value.EntityData.SPGroupID);
198-
value.Key = id;
207+
// Only ensure the user if it is not a SharePoint group
208+
if (!value.EntityData || (value.EntityData && typeof value.EntityData.SPGroupID === "undefined")) {
209+
const id = await this.ensureUser(value.Key);
210+
value.Key = id;
211+
}
199212
}
200213
}
201214

@@ -231,7 +244,7 @@ export default class SPPeopleSearchService {
231244
} as IPeoplePickerUserItem;
232245
default:
233246
return {
234-
id: element.Key,
247+
id: element.EntityData.SPGroupID,
235248
imageInitials: this.getFullNameInitials(element.DisplayText),
236249
text: element.DisplayText,
237250
secondaryText: element.EntityData.AccountName

src/webparts/controlsTest/components/ControlsTest.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -235,16 +235,21 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
235235
title={this.props.title}
236236
updateProperty={this.props.updateProperty} />
237237

238-
<ListView items={this.state.items}
239-
viewFields={viewFields}
240-
iconFieldName='ServerRelativeUrl'
241-
groupByFields={groupByFields}
242-
compact={true}
243-
selectionMode={SelectionMode.single}
244-
selection={this._getSelection}
245-
showFilter={true}
246-
// defaultFilter="Team"
247-
/>
238+
<PeoplePicker context={this.props.context}
239+
titleText="People Picker (Group not found)"
240+
webAbsoluteUrl={this.props.context.pageContext.site.absoluteUrl}
241+
groupName="Team Site Visitors 123"
242+
ensureUser={true}
243+
principalTypes={[PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.SecurityGroup, PrincipalType.DistributionList]}
244+
defaultSelectedUsers={["[email protected]", "[email protected]"]}
245+
selectedItems={this._getPeoplePickerItems} />
246+
247+
<PeoplePicker context={this.props.context}
248+
titleText="People Picker (search for group)"
249+
groupName="Team Site Visitors"
250+
principalTypes={[PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.SecurityGroup, PrincipalType.DistributionList]}
251+
defaultSelectedUsers={["[email protected]", "[email protected]"]}
252+
selectedItems={this._getPeoplePickerItems} />
248253

249254
<PeoplePicker context={this.props.context}
250255
titleText="People Picker (pre-set global users)"
@@ -296,6 +301,18 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
296301
showtooltip={true} />
297302

298303

304+
<ListView items={this.state.items}
305+
viewFields={viewFields}
306+
iconFieldName='ServerRelativeUrl'
307+
groupByFields={groupByFields}
308+
compact={true}
309+
selectionMode={SelectionMode.single}
310+
selection={this._getSelection}
311+
showFilter={true}
312+
// defaultFilter="Team"
313+
/>
314+
315+
299316
<ChartControl type={ChartType.Bar}
300317
data={{
301318
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],

0 commit comments

Comments
 (0)