diff --git a/config/package-solution.json b/config/package-solution.json index 4c5f918..fb51557 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -4,7 +4,7 @@ "name": "organization-chart-webpart", "title": "organization-chart-webpart", "id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e", - "version": "1.0.1.0", + "version": "1.0.2.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 43f3cfc..e3aa960 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "organization-chart-webpart", - "version": "1.0.1", + "version": "1.0.2", "private": true, "main": "lib/index.js", "engines": { "node": ">=6.9.0 <9.0.0" }, diff --git a/src/webparts/organizationChart/OrganizationChartDataService.ts b/src/webparts/organizationChart/OrganizationChartDataService.ts index 874a212..d8762ab 100644 --- a/src/webparts/organizationChart/OrganizationChartDataService.ts +++ b/src/webparts/organizationChart/OrganizationChartDataService.ts @@ -29,6 +29,17 @@ interface ITreeLoadState { visited: { [account: string]: boolean }; } +interface IPeoplePickerEntityData { + AccountName?: string; + Email?: string; +} + +interface IPeoplePickerEntity { + EntityData?: IPeoplePickerEntityData; + IsResolved?: boolean; + Key?: string; +} + export class OrganizationChartDataService { private static cache: { [account: string]: IProfileCacheEntry } = {}; @@ -49,7 +60,7 @@ export class OrganizationChartDataService { visited: {} }; const rootRequest: Promise = account - ? this.getProfile(account, cacheMinutes) + ? this.getConfiguredProfile(account, cacheMinutes) : this.getMyProfile(cacheMinutes); return rootRequest .then((profile: IUserProfile): Promise => @@ -83,6 +94,79 @@ export class OrganizationChartDataService { return this.requestProfile(url, key, cacheMinutes); } + private getConfiguredProfile(accountName: string, cacheMinutes: number): Promise { + return this.resolveAccountNames(accountName) + .catch((): string[] => [accountName]) + .then((accounts: string[]): Promise => + this.getFirstAvailableProfile(accounts, cacheMinutes, 0)); + } + + private getFirstAvailableProfile( + accounts: string[], + cacheMinutes: number, + index: number, + previousError?: Error + ): Promise { + if (index >= accounts.length) { + return Promise.reject(previousError || new Error('The configured user could not be resolved.')); + } + return this.getProfile(accounts[index], cacheMinutes) + .catch((error: Error): Promise => + this.getFirstAvailableProfile(accounts, cacheMinutes, index + 1, error)); + } + + private resolveAccountNames(accountName: string): Promise { + const url: string = this.webUrl + + '/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser'; + const body: string = JSON.stringify({ + queryParams: { + __metadata: { type: 'SP.UI.ApplicationPages.ClientPeoplePickerQueryParameters' }, + AllowEmailAddresses: true, + AllowMultipleEntities: false, + AllUrlZones: false, + MaximumEntitySuggestions: 10, + PrincipalSource: 15, + PrincipalType: 1, + QueryString: accountName + } + }); + const headers: any = { // tslint:disable-line:no-any + 'Accept': 'application/json;odata=verbose', + 'Content-Type': 'application/json;odata=verbose', + 'OData-Version': '3.0' + }; + return this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, { headers: headers, body: body }) + .then((response: SPHttpClientResponse): Promise => { // tslint:disable-line:no-any + if (response.ok) { return response.json(); } + return response.text().then((text: string): Promise => // tslint:disable-line:no-any + Promise.reject(new Error('People Picker request failed (' + response.status + '): ' + text))); + }).then((payload: any): string[] => { // tslint:disable-line:no-any + const data: any = payload && payload.d !== undefined ? payload.d : payload; // tslint:disable-line:no-any + const serialized: string = String( + data && data.ClientPeoplePickerSearchUser !== undefined + ? data.ClientPeoplePickerSearchUser + : (data && data.value !== undefined ? data.value : '[]') + ); + let entities: IPeoplePickerEntity[] = []; + try { entities = JSON.parse(serialized) as IPeoplePickerEntity[]; } catch (error) { entities = []; } + const input: string = normalizeAccount(accountName).toLowerCase(); + const matching: IPeoplePickerEntity[] = entities.filter((entity: IPeoplePickerEntity): boolean => { + const account: string = normalizeAccount(entity.EntityData && entity.EntityData.AccountName).toLowerCase(); + const email: string = normalizeAccount(entity.EntityData && entity.EntityData.Email).toLowerCase(); + const key: string = normalizeAccount(entity.Key).toLowerCase(); + return entity.IsResolved !== false && (account === input || email === input || key === input); + }); + const selected: IPeoplePickerEntity[] = matching.length ? matching : (entities.length === 1 ? entities : []); + const candidates: string[] = []; + selected.forEach((entity: IPeoplePickerEntity): void => { + candidates.push(normalizeAccount(entity.Key)); + candidates.push(normalizeAccount(entity.EntityData && entity.EntityData.AccountName)); + }); + candidates.push(normalizeAccount(accountName)); + return uniqueAccounts(candidates, '', candidates.length || 1); + }); + } + private requestProfile(url: string, cacheKey: string, cacheMinutes: number): Promise { return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) .then((response: SPHttpClientResponse): Promise => { // tslint:disable-line:no-any diff --git a/tests/validate-project.js b/tests/validate-project.js index c5ce4a3..62bd099 100644 --- a/tests/validate-project.js +++ b/tests/validate-project.js @@ -27,6 +27,8 @@ assert(manifest.componentType === 'WebPart' && manifest.alias === 'OrganizationC assert(service.indexOf('/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor') >= 0, 'User Profile REST access is missing.'); assert(service.indexOf('/_api/SP.UserProfiles.PeopleManager/GetMyProperties') >= 0, 'Reliable current-user profile access is missing.'); +assert(service.indexOf('ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser') >= 0, + 'Configured-account resolution through the SharePoint People Picker is missing.'); assert(service.indexOf('profile.directReports') >= 0 && service.indexOf('this.buildNode(childAccount, depth + 1') >= 0, 'Recursive downward organization loading is missing.'); assert(webPart.indexOf("PropertyPaneTextField('startUser'") >= 0, 'Starting-person configuration is missing.');