Skip to content

Commit a76e054

Browse files
Added mgt-people-picker support for filtering group members when using group-id (#741)
* Update azure-pipelines.yml for Azure Pipelines * Adding support for group filtering * Adding support for group filtering * Adding new method documentation * Fixing typo * Bringing back the right port number * Adding support for transitive search + group * Adding the react type for transitiveSearch * Merging getUserFromGroup and findUsersFromGroup * Removing references to getPeopleFromGroup * Removing commented code * Update packages/mgt/src/components/mgt-people-picker/mgt-people-picker.ts * Adding any and getting unique people from result * Simplifying the unique people function * Update packages/mgt/src/components/mgt-people-picker/mgt-people-picker.ts Co-authored-by: Nikola Metulev <[email protected]> * Refactoring for a findGroupMembers + simpler logic * Apply suggestions from code review Co-authored-by: Nikola Metulev <[email protected]>
1 parent 0924089 commit a76e054

File tree

7 files changed

+210
-61
lines changed

7 files changed

+210
-61
lines changed

packages/mgt-react/src/generated/react.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type PeoplePickerProps = {
5454
groupId?: string;
5555
type?: PersonType;
5656
groupType?: GroupType;
57+
transitiveSearch?: boolean;
5758
people?: IDynamicPerson[];
5859
defaultSelectedUserIds?: string[];
5960
placeholder?: string;

packages/mgt/src/components/mgt-people-picker/mgt-people-picker.ts

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { User } from '@microsoft/microsoft-graph-types';
99
import { customElement, html, internalProperty, property, TemplateResult } from 'lit-element';
1010
import { classMap } from 'lit-html/directives/class-map';
1111
import { repeat } from 'lit-html/directives/repeat';
12-
import { findGroups, GroupType } from '../../graph/graph.groups';
13-
import { findPeople, getPeople, getPeopleFromGroup, PersonType } from '../../graph/graph.people';
14-
import { findUsers, getUser, getUsersForUserIds } from '../../graph/graph.user';
12+
import { findGroups, findGroupsFromGroup, GroupType } from '../../graph/graph.groups';
13+
import { findPeople, getPeople, PersonType } from '../../graph/graph.people';
14+
import { findUsers, findGroupMembers, getUser, getUsersForUserIds } from '../../graph/graph.user';
1515
import { IDynamicPerson } from '../../graph/types';
1616
import { Providers, ProviderState, MgtTemplatedComponent } from '@microsoft/mgt-element';
1717
import '../../styles/style-helper';
@@ -182,6 +182,16 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
182182
this.requestStateUpdate(true);
183183
}
184184

185+
/**
186+
* whether the return should contain a flat list of all nested members
187+
* @type {boolean}
188+
*/
189+
@property({
190+
attribute: 'transitive-search',
191+
type: Boolean
192+
})
193+
public transitiveSearch: boolean;
194+
185195
/**
186196
* containing object of IDynamicPerson.
187197
* @type {IDynamicPerson[]}
@@ -644,7 +654,14 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
644654
if (this.groupId) {
645655
if (this._groupPeople === null) {
646656
try {
647-
this._groupPeople = await getPeopleFromGroup(graph, this.groupId);
657+
this._groupPeople = await findGroupMembers(
658+
graph,
659+
null,
660+
this.groupId,
661+
this.showMax,
662+
this.type,
663+
this.transitiveSearch
664+
);
648665
} catch (_) {
649666
this._groupPeople = [];
650667
}
@@ -671,42 +688,48 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
671688

672689
if (input) {
673690
people = [];
674-
if (this.type === PersonType.person || this.type === PersonType.any) {
675-
try {
676-
people = (await findPeople(graph, input, this.showMax)) || [];
677-
} catch (e) {
678-
// nop
679-
}
680691

681-
if (people.length < this.showMax) {
692+
if (this.groupId) {
693+
people =
694+
(await findGroupMembers(graph, input, this.groupId, this.showMax, this.type, this.transitiveSearch)) || [];
695+
} else {
696+
if (this.type === PersonType.person || this.type === PersonType.any) {
682697
try {
683-
const users = (await findUsers(graph, input, this.showMax)) || [];
698+
people = (await findPeople(graph, input, this.showMax)) || [];
699+
} catch (e) {
700+
// nop
701+
}
684702

685-
// make sure only unique people
686-
const peopleIds = new Set(people.map(p => p.id));
687-
for (const user of users) {
688-
if (!peopleIds.has(user.id)) {
689-
people.push(user);
703+
if (people.length < this.showMax) {
704+
try {
705+
const users = (await findUsers(graph, input, this.showMax)) || [];
706+
707+
// make sure only unique people
708+
const peopleIds = new Set(people.map(p => p.id));
709+
for (const user of users) {
710+
if (!peopleIds.has(user.id)) {
711+
people.push(user);
712+
}
690713
}
714+
} catch (e) {
715+
// nop
691716
}
717+
}
718+
}
719+
if ((this.type === PersonType.group || this.type === PersonType.any) && people.length < this.showMax) {
720+
let groups = [];
721+
try {
722+
groups = (await findGroups(graph, input, this.showMax, this.groupType)) || [];
723+
people = people.concat(groups);
692724
} catch (e) {
693725
// nop
694726
}
695727
}
696728
}
697-
698-
if ((this.type === PersonType.group || this.type === PersonType.any) && people.length < this.showMax) {
699-
people = [];
700-
try {
701-
const groups = (await findGroups(graph, input, this.showMax, this.groupType)) || [];
702-
people = people.concat(groups);
703-
} catch (e) {
704-
// nop
705-
}
706-
}
707729
}
708730
}
709731

732+
//people = this.getUniquePeople(people);
710733
this._foundPeople = this.filterPeople(people);
711734
}
712735

packages/mgt/src/components/mgt-people/mgt-people.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
99
import { customElement, html, property, TemplateResult } from 'lit-element';
1010
import { repeat } from 'lit-html/directives/repeat';
11-
import { getPeople, getPeopleFromGroup } from '../../graph/graph.people';
11+
import { getPeople, PersonType } from '../../graph/graph.people';
1212
import { getUsersPresenceByPeople } from '../../graph/graph.presence';
13-
import { getUsersForPeopleQueries, getUsersForUserIds } from '../../graph/graph.user';
13+
import { findGroupMembers, getUsersForPeopleQueries, getUsersForUserIds } from '../../graph/graph.user';
1414
import { IDynamicPerson } from '../../graph/types';
1515
import { Providers, ProviderState, MgtTemplatedComponent, arraysAreEqual } from '@microsoft/mgt-element';
1616
import '../../styles/style-helper';
@@ -315,7 +315,7 @@ export class MgtPeople extends MgtTemplatedComponent {
315315

316316
// populate people
317317
if (this.groupId) {
318-
this.people = await getPeopleFromGroup(graph, this.groupId);
318+
this.people = await findGroupMembers(graph, null, this.groupId, this.showMax, PersonType.person);
319319
} else if (this.userIds) {
320320
this.people = await getUsersForUserIds(graph, this.userIds);
321321
} else if (this.peopleQueries) {

packages/mgt/src/graph/graph.groups.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,88 @@ export async function findGroups(
160160

161161
return result ? result.value : null;
162162
}
163+
164+
/**
165+
* Searches the Graph for Groups
166+
*
167+
* @export
168+
* @param {IGraph} graph
169+
* @param {string} query - what to search for
170+
* @param {string} groupId - what to search for
171+
* @param {number} [top=10] - number of groups to return
172+
* @param {boolean} [transitive=false] - whether the return should contain a flat list of all nested members
173+
* @param {GroupType} [groupTypes=GroupType.any] - the type of group to search for
174+
* @returns {Promise<Group[]>} An array of Groups
175+
*/
176+
export async function findGroupsFromGroup(
177+
graph: IGraph,
178+
query: string,
179+
groupId: string,
180+
top: number = 10,
181+
transitive: boolean = false,
182+
groupTypes: GroupType = GroupType.any
183+
): Promise<Group[]> {
184+
const scopes = 'Group.Read.All';
185+
186+
let cache: CacheStore<CacheGroupQuery>;
187+
const key = `${groupId}:${query || '*'}:${groupTypes}:${transitive}`;
188+
189+
if (groupsCacheEnabled()) {
190+
cache = CacheService.getCache(cacheSchema, 'groupsQuery');
191+
const cacheGroupQuery = await cache.getValue(key);
192+
if (cacheGroupQuery && getGroupsInvalidationTime() > Date.now() - cacheGroupQuery.timeCached) {
193+
if (cacheGroupQuery.top >= top) {
194+
// if request is less than the cache's requests, return a slice of the results
195+
return cacheGroupQuery.groups.map(x => JSON.parse(x)).slice(0, top + 1);
196+
}
197+
// if the new request needs more results than what's presently in the cache, graph must be called again
198+
}
199+
}
200+
201+
const apiUrl = `groups/${groupId}/${transitive ? 'transitiveMembers' : 'members'}/microsoft.graph.group`;
202+
let filterQuery = '';
203+
if (query !== '') {
204+
filterQuery = `(startswith(displayName,'${query}') or startswith(mailNickname,'${query}') or startswith(mail,'${query}'))`;
205+
}
206+
207+
if (groupTypes !== GroupType.any) {
208+
const filterGroups = [];
209+
210+
// tslint:disable-next-line:no-bitwise
211+
if (GroupType.unified === (groupTypes & GroupType.unified)) {
212+
filterGroups.push("groupTypes/any(c:c+eq+'Unified')");
213+
}
214+
215+
// tslint:disable-next-line:no-bitwise
216+
if (GroupType.security === (groupTypes & GroupType.security)) {
217+
filterGroups.push('(mailEnabled eq false and securityEnabled eq true)');
218+
}
219+
220+
// tslint:disable-next-line:no-bitwise
221+
if (GroupType.mailenabledsecurity === (groupTypes & GroupType.mailenabledsecurity)) {
222+
filterGroups.push('(mailEnabled eq true and securityEnabled eq true)');
223+
}
224+
225+
// tslint:disable-next-line:no-bitwise
226+
if (GroupType.distribution === (groupTypes & GroupType.distribution)) {
227+
filterGroups.push('(mailEnabled eq true and securityEnabled eq false)');
228+
}
229+
230+
filterQuery += (query !== '' ? ' and ' : '') + filterGroups.join(' or ');
231+
}
232+
233+
const result = await graph
234+
.api(apiUrl)
235+
.filter(filterQuery)
236+
.count(true)
237+
.top(top)
238+
.header('ConsistencyLevel', 'eventual')
239+
.middlewareOptions(prepScopes(scopes))
240+
.get();
241+
242+
if (groupsCacheEnabled() && result) {
243+
cache.putValue(key, { groups: result.value.map(x => JSON.stringify(x)), top: top });
244+
}
245+
246+
return result ? result.value : null;
247+
}

packages/mgt/src/graph/graph.people.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -191,36 +191,6 @@ export async function getPeople(graph: IGraph): Promise<Person[]> {
191191
return people ? people.value : null;
192192
}
193193

194-
/**
195-
* async promise to the Graph for People, defined by a group id
196-
*
197-
* @param {string} groupId
198-
* @returns {(Promise<Person[]>)}
199-
* @memberof Graph
200-
*/
201-
export async function getPeopleFromGroup(graph: IGraph, groupId: string): Promise<Person[]> {
202-
const scopes = 'people.read';
203-
let cache: CacheStore<CacheGroupPeople>;
204-
205-
if (peopleCacheEnabled()) {
206-
cache = CacheService.getCache<CacheGroupPeople>(cacheSchema, groupStore);
207-
const peopleItem = await cache.getValue(groupId);
208-
if (peopleItem && getPeopleInvalidationTime() > Date.now() - peopleItem.timeCached) {
209-
return peopleItem.people.map(peopleStr => JSON.parse(peopleStr));
210-
}
211-
}
212-
213-
const uri = `/groups/${groupId}/members`;
214-
const people = await graph
215-
.api(uri)
216-
.middlewareOptions(prepScopes(scopes))
217-
.get();
218-
if (peopleCacheEnabled()) {
219-
cache.putValue(groupId, { people: people.value.map(ppl => JSON.stringify(ppl)) });
220-
}
221-
return people ? people.value : null;
222-
}
223-
224194
/**
225195
* returns a promise that resolves after specified time
226196
* @param time in milliseconds

packages/mgt/src/graph/graph.user.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IGraph } from '@microsoft/mgt-element';
99
import { User } from '@microsoft/microsoft-graph-types';
1010
import { CacheItem, CacheSchema, CacheService, CacheStore } from '../utils/Cache';
1111
import { prepScopes } from '../utils/GraphHelpers';
12-
import { findPeople } from './graph.people';
12+
import { findPeople, PersonType } from './graph.people';
1313
import {
1414
getPhotoForResource,
1515
getPhotoFromCache,
@@ -420,3 +420,68 @@ export async function findUsers(graph: IGraph, query: string, top: number = 10):
420420
}
421421
return graphResult ? graphResult.value : null;
422422
}
423+
424+
/**
425+
* async promise, returns all matching Graph users who are member of the specified group
426+
*
427+
* @param {string} query
428+
* @param {string} groupId - the group to query
429+
* @param {number} [top=10] - number of people to return
430+
* @param {PersonType} [personType=PersonType.person] - the type of person to search for
431+
* @param {boolean} [transitive=false] - whether the return should contain a flat list of all nested members
432+
* @returns {(Promise<User[]>)}
433+
*/
434+
export async function findGroupMembers(
435+
graph: IGraph,
436+
query: string,
437+
groupId: string,
438+
top: number = 10,
439+
personType: PersonType = PersonType.person,
440+
transitive: boolean = false
441+
): Promise<User[]> {
442+
const scopes = ['user.read.all', 'people.read'];
443+
const item = { maxResults: top, results: null };
444+
445+
let cache: CacheStore<CacheUserQuery>;
446+
const key = `${groupId || '*'}:${query || '*'}:${personType}:${transitive}`;
447+
448+
if (usersCacheEnabled()) {
449+
cache = CacheService.getCache<CacheUserQuery>(cacheSchema, queryStore);
450+
const result: CacheUserQuery = await cache.getValue(key);
451+
452+
if (result && getUserInvalidationTime() > Date.now() - result.timeCached) {
453+
return result.results.map(userStr => JSON.parse(userStr));
454+
}
455+
}
456+
457+
let filter: string = '';
458+
if (query) {
459+
filter = `startswith(displayName,'${query}') or startswith(givenName,'${query}') or startswith(surname,'${query}') or startswith(mail,'${query}') or startswith(userPrincipalName,'${query}')`;
460+
}
461+
462+
let apiUrl: string = `/groups/${groupId}/${transitive ? 'transitiveMembers' : 'members'}`;
463+
if (personType === PersonType.person) {
464+
apiUrl += `/microsoft.graph.user`;
465+
} else if (personType === PersonType.group) {
466+
apiUrl += `/microsoft.graph.group`;
467+
if (query) {
468+
filter = `startswith(displayName,'${query}') or startswith(mail,'${query}')`;
469+
}
470+
}
471+
472+
const graphResult = await graph
473+
.api(apiUrl)
474+
.count(true)
475+
.top(top)
476+
.filter(filter)
477+
.header('ConsistencyLevel', 'eventual')
478+
.middlewareOptions(prepScopes(...scopes))
479+
.get();
480+
481+
if (usersCacheEnabled() && graphResult) {
482+
item.results = graphResult.value.map(userStr => JSON.stringify(userStr));
483+
cache.putValue(key, item);
484+
}
485+
486+
return graphResult ? graphResult.value : null;
487+
}

stories/components/peoplePicker.stories.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export const pickPeopleAndGroups = () => html`
7474
<!-- type can be "any", "person", "group" -->
7575
`;
7676

77+
export const pickPeopleAndGroupsNested = () => html`
78+
<mgt-people-picker type="any" transitive-search="true"></mgt-people-picker>
79+
<!-- type can be "any", "person", "group" -->
80+
`;
81+
7782
export const pickGroups = () => html`
7883
<mgt-people-picker type="group"></mgt-people-picker>
7984
<!-- type can be "any", "person", "group" -->

0 commit comments

Comments
 (0)