feat: Update version to 1.0.2 and enhance profile retrieval with People Picker integration

This commit is contained in:
Torsten Brendgen
2026-08-24 14:13:08 +02:00
parent 83dd10f255
commit 4e5dafabfc
4 changed files with 89 additions and 3 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "organization-chart-webpart", "name": "organization-chart-webpart",
"title": "organization-chart-webpart", "title": "organization-chart-webpart",
"id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e", "id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e",
"version": "1.0.1.0", "version": "1.0.2.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "organization-chart-webpart", "name": "organization-chart-webpart",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "node": ">=6.9.0 <9.0.0" }, "engines": { "node": ">=6.9.0 <9.0.0" },
@@ -29,6 +29,17 @@ interface ITreeLoadState {
visited: { [account: string]: boolean }; visited: { [account: string]: boolean };
} }
interface IPeoplePickerEntityData {
AccountName?: string;
Email?: string;
}
interface IPeoplePickerEntity {
EntityData?: IPeoplePickerEntityData;
IsResolved?: boolean;
Key?: string;
}
export class OrganizationChartDataService { export class OrganizationChartDataService {
private static cache: { [account: string]: IProfileCacheEntry } = {}; private static cache: { [account: string]: IProfileCacheEntry } = {};
@@ -49,7 +60,7 @@ export class OrganizationChartDataService {
visited: {} visited: {}
}; };
const rootRequest: Promise<IUserProfile> = account const rootRequest: Promise<IUserProfile> = account
? this.getProfile(account, cacheMinutes) ? this.getConfiguredProfile(account, cacheMinutes)
: this.getMyProfile(cacheMinutes); : this.getMyProfile(cacheMinutes);
return rootRequest return rootRequest
.then((profile: IUserProfile): Promise<IOrganizationNode> => .then((profile: IUserProfile): Promise<IOrganizationNode> =>
@@ -83,6 +94,79 @@ export class OrganizationChartDataService {
return this.requestProfile(url, key, cacheMinutes); return this.requestProfile(url, key, cacheMinutes);
} }
private getConfiguredProfile(accountName: string, cacheMinutes: number): Promise<IUserProfile> {
return this.resolveAccountNames(accountName)
.catch((): string[] => [accountName])
.then((accounts: string[]): Promise<IUserProfile> =>
this.getFirstAvailableProfile(accounts, cacheMinutes, 0));
}
private getFirstAvailableProfile(
accounts: string[],
cacheMinutes: number,
index: number,
previousError?: Error
): Promise<IUserProfile> {
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<IUserProfile> =>
this.getFirstAvailableProfile(accounts, cacheMinutes, index + 1, error));
}
private resolveAccountNames(accountName: string): Promise<string[]> {
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<any> => { // tslint:disable-line:no-any
if (response.ok) { return response.json(); }
return response.text().then((text: string): Promise<any> => // 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<IUserProfile> { private requestProfile(url: string, cacheKey: string, cacheMinutes: number): Promise<IUserProfile> {
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers })
.then((response: SPHttpClientResponse): Promise<any> => { // tslint:disable-line:no-any .then((response: SPHttpClientResponse): Promise<any> => { // tslint:disable-line:no-any
+2
View File
@@ -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/GetPropertiesFor') >= 0, 'User Profile REST access is missing.');
assert(service.indexOf('/_api/SP.UserProfiles.PeopleManager/GetMyProperties') >= 0, assert(service.indexOf('/_api/SP.UserProfiles.PeopleManager/GetMyProperties') >= 0,
'Reliable current-user profile access is missing.'); '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, assert(service.indexOf('profile.directReports') >= 0 && service.indexOf('this.buildNode(childAccount, depth + 1') >= 0,
'Recursive downward organization loading is missing.'); 'Recursive downward organization loading is missing.');
assert(webPart.indexOf("PropertyPaneTextField('startUser'") >= 0, 'Starting-person configuration is missing.'); assert(webPart.indexOf("PropertyPaneTextField('startUser'") >= 0, 'Starting-person configuration is missing.');