feat: Initialize organization chart web part with core functionality and configuration
- Added configuration files: config.json, copy-assets.json, package-solution.json, serve.json, write-manifests.json. - Created gulpfile.js for build process. - Implemented core logic in OrganizationChartCore.ts for account normalization and profile handling. - Developed data service in OrganizationChartDataService.ts to fetch organization data from SharePoint User Profile Service. - Defined types for user profiles and organization nodes in OrganizationChartTypes.ts. - Created web part manifest for OrganizationChartWebPart. - Built the main web part component with rendering logic and property pane configuration in OrganizationChartWebPart.ts. - Developed React components for displaying the organization chart. - Implemented styling for the organization chart in OrganizationChart.module.scss. - Added localization support with English and German strings. - Created unit tests for core functionalities in OrganizationChartCore.test.ts. - Set up TypeScript and TSLint configurations for the project.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import {
|
||||
normalizeAccount,
|
||||
normalizeCacheMinutes,
|
||||
normalizeMaxDepth,
|
||||
normalizeMaxPeople,
|
||||
normalizeProfileUrl,
|
||||
profileProperty,
|
||||
uniqueAccounts
|
||||
} from './OrganizationChartCore';
|
||||
import {
|
||||
IOrganizationChartData,
|
||||
IOrganizationNode,
|
||||
IUserProfile,
|
||||
IUserProfileProperty
|
||||
} from './OrganizationChartTypes';
|
||||
|
||||
interface IProfileCacheEntry {
|
||||
expires: number;
|
||||
profile: IUserProfile;
|
||||
}
|
||||
|
||||
interface ITreeLoadState {
|
||||
count: number;
|
||||
maximum: number;
|
||||
maximumDepth: number;
|
||||
truncated: boolean;
|
||||
visited: { [account: string]: boolean };
|
||||
}
|
||||
|
||||
export class OrganizationChartDataService {
|
||||
private static cache: { [account: string]: IProfileCacheEntry } = {};
|
||||
|
||||
public constructor(private context: WebPartContext) { }
|
||||
|
||||
public getOrganization(
|
||||
accountName: string,
|
||||
maxDepth: number,
|
||||
maxPeople: number,
|
||||
cacheMinutes: number
|
||||
): Promise<IOrganizationChartData> {
|
||||
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),
|
||||
maximumDepth: normalizeMaxDepth(maxDepth),
|
||||
truncated: false,
|
||||
visited: {}
|
||||
};
|
||||
return this.buildNode(account, 0, state, cacheMinutes).then((root: IOrganizationNode): IOrganizationChartData => ({
|
||||
root: root,
|
||||
peopleCount: state.count,
|
||||
truncated: state.truncated
|
||||
}));
|
||||
}
|
||||
|
||||
public getProfile(accountName: string, cacheMinutes: number): Promise<IUserProfile> {
|
||||
const account: string = normalizeAccount(accountName);
|
||||
const key: string = account.toLowerCase();
|
||||
const cached: IProfileCacheEntry = OrganizationChartDataService.cache[key];
|
||||
if (cached && cached.expires > new Date().getTime()) { return Promise.resolve(cached.profile); }
|
||||
const escaped: string = account.replace(/'/g, "''");
|
||||
const url: string = this.webUrl +
|
||||
"/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v='" + encodeURIComponent(escaped) + "'";
|
||||
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers })
|
||||
.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('User profile request failed (' + response.status + '): ' + text)));
|
||||
}).then((payload: any): IUserProfile => { // tslint:disable-line:no-any
|
||||
const raw: any = payload && payload.d !== undefined ? payload.d : payload; // tslint:disable-line:no-any
|
||||
const profile: IUserProfile = this.mapProfile(raw || {});
|
||||
const minutes: number = normalizeCacheMinutes(cacheMinutes);
|
||||
if (minutes > 0) {
|
||||
OrganizationChartDataService.cache[key] = {
|
||||
profile: profile,
|
||||
expires: new Date().getTime() + minutes * 60000
|
||||
};
|
||||
}
|
||||
return profile;
|
||||
});
|
||||
}
|
||||
|
||||
private buildNode(
|
||||
accountName: string,
|
||||
depth: number,
|
||||
state: ITreeLoadState,
|
||||
cacheMinutes: number
|
||||
): Promise<IOrganizationNode> {
|
||||
const account: string = normalizeAccount(accountName);
|
||||
const key: string = account.toLowerCase();
|
||||
if (!account || 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<IOrganizationNode> => {
|
||||
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<Promise<IOrganizationNode>> = accounts.map(
|
||||
(childAccount: string): Promise<IOrganizationNode> =>
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private mapProfile(raw: any): IUserProfile { // tslint:disable-line:no-any
|
||||
const properties: IUserProfileProperty[] = this.results(raw.UserProfileProperties)
|
||||
.map((entry: any): IUserProfileProperty => ({ // tslint:disable-line:no-any
|
||||
Key: String(entry.Key || ''),
|
||||
Value: String(entry.Value || '')
|
||||
}));
|
||||
const account: string = String(raw.AccountName || profileProperty(properties, 'AccountName') || '');
|
||||
let pictureUrl: string = normalizeProfileUrl(String(raw.PictureUrl || ''), this.webUrl);
|
||||
if (!pictureUrl && account) {
|
||||
pictureUrl = this.webUrl + '/_layouts/15/userphoto.aspx?size=L&accountname=' + encodeURIComponent(account);
|
||||
}
|
||||
return {
|
||||
accountName: account,
|
||||
displayName: String(raw.DisplayName || profileProperty(properties, 'PreferredName') || account),
|
||||
email: String(raw.Email || profileProperty(properties, 'WorkEmail') || ''),
|
||||
title: String(raw.Title || profileProperty(properties, 'SPS-JobTitle') || ''),
|
||||
department: profileProperty(properties, 'Department'),
|
||||
office: profileProperty(properties, 'Office'),
|
||||
workPhone: profileProperty(properties, 'WorkPhone'),
|
||||
cellPhone: profileProperty(properties, 'CellPhone'),
|
||||
location: profileProperty(properties, 'SPS-Location'),
|
||||
managerAccount: profileProperty(properties, 'Manager'),
|
||||
pictureUrl: pictureUrl,
|
||||
profileUrl: normalizeProfileUrl(String(raw.UserUrl || raw.PersonalUrl || ''), this.webUrl),
|
||||
directReports: this.stringResults(raw.DirectReports),
|
||||
extendedManagers: this.stringResults(raw.ExtendedManagers)
|
||||
};
|
||||
}
|
||||
|
||||
private stringResults(value: any): string[] { // tslint:disable-line:no-any
|
||||
return this.results(value).map((entry: any): string => String(entry || '')); // tslint:disable-line:no-any
|
||||
}
|
||||
private results(value: any): any[] { // tslint:disable-line:no-any
|
||||
if (!value) { return []; }
|
||||
if (Array.isArray(value.results)) { return value.results; }
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); }
|
||||
private get headers(): any { // tslint:disable-line:no-any
|
||||
return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user