diff --git a/config/package-solution.json b/config/package-solution.json index 61bc940..4c5f918 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.0.0", + "version": "1.0.1.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 0259cf2..43f3cfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "organization-chart-webpart", - "version": "1.0.0", + "version": "1.0.1", "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 4bd3f68..874a212 100644 --- a/src/webparts/organizationChart/OrganizationChartDataService.ts +++ b/src/webparts/organizationChart/OrganizationChartDataService.ts @@ -41,7 +41,6 @@ export class OrganizationChartDataService { cacheMinutes: number ): Promise { const account: string = normalizeAccount(accountName); - if (!account) { return Promise.reject(new Error('A user account is required.')); } const state: ITreeLoadState = { count: 0, maximum: normalizeMaxPeople(maxPeople), @@ -49,11 +48,28 @@ export class OrganizationChartDataService { truncated: false, visited: {} }; - return this.buildNode(account, 0, state, cacheMinutes).then((root: IOrganizationNode): IOrganizationChartData => ({ - root: root, - peopleCount: state.count, - truncated: state.truncated - })); + const rootRequest: Promise = account + ? this.getProfile(account, cacheMinutes) + : this.getMyProfile(cacheMinutes); + return rootRequest + .then((profile: IUserProfile): Promise => + this.buildProfileNode(profile, 0, state, cacheMinutes)) + .then((root: IOrganizationNode): IOrganizationChartData => ({ + root: root, + peopleCount: state.count, + truncated: state.truncated + })); + } + + public getMyProfile(cacheMinutes: number): Promise { + const cacheKey: string = '__current_user__'; + const cached: IProfileCacheEntry = OrganizationChartDataService.cache[cacheKey]; + if (cached && cached.expires > new Date().getTime()) { return Promise.resolve(cached.profile); } + return this.requestProfile( + this.webUrl + '/_api/SP.UserProfiles.PeopleManager/GetMyProperties', + cacheKey, + cacheMinutes + ); } public getProfile(accountName: string, cacheMinutes: number): Promise { @@ -64,6 +80,10 @@ export class OrganizationChartDataService { const escaped: string = account.replace(/'/g, "''"); const url: string = this.webUrl + "/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v='" + encodeURIComponent(escaped) + "'"; + return this.requestProfile(url, key, cacheMinutes); + } + + 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 if (response.ok) { return response.json(); } @@ -74,10 +94,14 @@ export class OrganizationChartDataService { const profile: IUserProfile = this.mapProfile(raw || {}); const minutes: number = normalizeCacheMinutes(cacheMinutes); if (minutes > 0) { - OrganizationChartDataService.cache[key] = { + OrganizationChartDataService.cache[cacheKey] = { profile: profile, expires: new Date().getTime() + minutes * 60000 }; + if (profile.accountName) { + OrganizationChartDataService.cache[profile.accountName.toLowerCase()] = + OrganizationChartDataService.cache[cacheKey]; + } } return profile; }); @@ -95,26 +119,39 @@ export class OrganizationChartDataService { if (state.count >= state.maximum) { state.truncated = true; } return Promise.resolve(undefined as IOrganizationNode); } + return this.getProfile(account, cacheMinutes).then((profile: IUserProfile): Promise => + this.buildProfileNode(profile, depth, state, cacheMinutes)); + } + + private buildProfileNode( + profile: IUserProfile, + depth: number, + state: ITreeLoadState, + cacheMinutes: number + ): Promise { + const key: string = normalizeAccount(profile.accountName).toLowerCase(); + if (!key || state.visited[key] || state.count >= state.maximum) { + if (state.count >= state.maximum) { state.truncated = true; } + return Promise.resolve(undefined as IOrganizationNode); + } state.visited[key] = true; state.count++; - return this.getProfile(account, cacheMinutes).then((profile: IUserProfile): Promise => { - const node: IOrganizationNode = { profile: profile, children: [] }; - if (depth >= state.maximumDepth) { - if (profile.directReports.length) { state.truncated = true; } - return Promise.resolve(node); - } - const accounts: string[] = uniqueAccounts(profile.directReports, profile.accountName, state.maximum); - const requests: Array> = accounts.map( - (childAccount: string): Promise => - this.buildNode(childAccount, depth + 1, state, cacheMinutes) - .catch((): IOrganizationNode => undefined as IOrganizationNode) - ); - return Promise.all(requests).then((children: IOrganizationNode[]): IOrganizationNode => { - node.children = children.filter((child: IOrganizationNode): boolean => !!child) - .sort((left: IOrganizationNode, right: IOrganizationNode): number => - left.profile.displayName.localeCompare(right.profile.displayName)); - return node; - }); + const node: IOrganizationNode = { profile: profile, children: [] }; + if (depth >= state.maximumDepth) { + if (profile.directReports.length) { state.truncated = true; } + return Promise.resolve(node); + } + const accounts: string[] = uniqueAccounts(profile.directReports, profile.accountName, state.maximum); + const requests: Array> = accounts.map( + (childAccount: string): Promise => + this.buildNode(childAccount, depth + 1, state, cacheMinutes) + .catch((): IOrganizationNode => undefined as IOrganizationNode) + ); + return Promise.all(requests).then((children: IOrganizationNode[]): IOrganizationNode => { + node.children = children.filter((child: IOrganizationNode): boolean => !!child) + .sort((left: IOrganizationNode, right: IOrganizationNode): number => + left.profile.displayName.localeCompare(right.profile.displayName)); + return node; }); } diff --git a/src/webparts/organizationChart/OrganizationChartWebPart.ts b/src/webparts/organizationChart/OrganizationChartWebPart.ts index 990563d..f617f04 100644 --- a/src/webparts/organizationChart/OrganizationChartWebPart.ts +++ b/src/webparts/organizationChart/OrganizationChartWebPart.ts @@ -37,9 +37,8 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart= 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('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.');