feat: update organization chart web part to support SharePoint list as data source
- Bump version to 1.2.0 for organization-chart-webpart - Add new interfaces and types for SharePoint list configuration - Implement OrganizationChartListDataService to handle SharePoint list data retrieval - Update web part properties to include data source selection and list configuration - Modify rendering logic to accommodate both user profiles and SharePoint list data sources - Enhance property pane with new fields for list configuration - Update localization files for new property labels and messages - Add validation checks for list configuration in the web part
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
# Organization Chart
|
||||
|
||||
SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es erstellt aus dem SharePoint User Profile Service ein Organigramm, das ab einer frei konfigurierbaren Startperson rekursiv nach unten aufgebaut wird.
|
||||
SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es erstellt aus dem User Profile Service oder einer frei zugeordneten SharePoint-Liste ein Organigramm, das ab einer konfigurierbaren Startperson rekursiv nach unten aufgebaut wird.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- aktueller Benutzer oder konfigurierter Manager als Startpunkt
|
||||
- wählbare Datenquelle: User Profile Service oder SharePoint-Liste
|
||||
- konfigurierbarer WebPart-Titel
|
||||
- Startperson über einen SharePoint-People-Picker direkt im WebPart auswählen
|
||||
- rekursives Laden aller darunterliegenden Ebenen über `DirectReports`
|
||||
- konfigurierbare maximale Tiefe und Personenzahl
|
||||
@@ -28,7 +30,30 @@ Das WebPart verwendet direkt:
|
||||
/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser
|
||||
```
|
||||
|
||||
Es benötigt keine Microsoft-Graph-Berechtigungen und keine zusätzliche SharePoint-Liste. Die User Profile Service Application muss eingerichtet sein. Benutzerprofile und insbesondere die Profilproperty `Manager` müssen aus dem Active Directory importiert werden, damit SharePoint `DirectReports` korrekt bereitstellt.
|
||||
Es benötigt keine Microsoft-Graph-Berechtigungen. Bei Verwendung des User Profile Service muss die Service Application eingerichtet sein. Benutzerprofile und insbesondere die Profilproperty `Manager` müssen aus dem Active Directory importiert werden, damit SharePoint `DirectReports` korrekt bereitstellt.
|
||||
|
||||
Alternativ kann im Property Pane eine SharePoint-Liste gewählt werden. Erforderlich sind Spalten für einen eindeutigen Schlüssel, den zugehörigen Manager-Schlüssel und den Anzeigenamen. Position, Abteilung, E-Mail, Standort, Telefon, Bild-URL und Profil-URL sind optional. Schlüssel- und Manager-Spalten können Textspalten oder einzelne Personenfelder sein. Die Startperson wird aus den vorhandenen Listeneinträgen gewählt; ohne Auswahl wird ein Eintrag ohne bekannten Manager automatisch als Wurzel verwendet.
|
||||
|
||||
Eine mögliche Listenstruktur mit Textspalten ist:
|
||||
|
||||
| Spalte | Beispiel | Zuordnung |
|
||||
|---|---|---|
|
||||
| `AccountName` | `CONTOSO\max.mustermann` | eindeutiger Schlüssel |
|
||||
| `ManagerAccount` | `CONTOSO\erika.musterfrau` | Manager-Schlüssel |
|
||||
| `Title` | `Max Mustermann` | Anzeigename |
|
||||
| `JobTitle` | `Teamleiter` | Position, optional |
|
||||
| `Department` | `IT` | Abteilung, optional |
|
||||
|
||||
Alternativ können für Schlüssel und Manager zwei einzelne Personenfelder verwendet werden. Entscheidend ist, dass der aus der Manager-Spalte gelesene Wert mit dem Schlüssel des übergeordneten Listeneintrags übereinstimmt.
|
||||
|
||||
### Verwendung mit dem Inhaltstyp-Katalog
|
||||
|
||||
Die Listenfelder koennen ueber den Inhaltstyp-Katalog verteilt werden. Fuer den eindeutigen Personenschluessel
|
||||
und die Manager-Beziehung sollten Textspalten oder einzelne Personenfelder verwendet werden. Ein Lookup auf
|
||||
eine konkrete Mitarbeiterliste ist dafuer nicht geeignet, weil ein Lookup die Listen-ID der jeweiligen
|
||||
Websitesammlung enthaelt. Lookup-Spalten werden deshalb in der Spaltenzuordnung des WebParts nicht angeboten.
|
||||
Empfehlenswert sind ein unveraenderlicher Mitarbeiter-/Accountschluessel als Text oder zwei Personenfelder
|
||||
mit identischer Login-Darstellung.
|
||||
|
||||
## Startperson
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "organization-chart-webpart",
|
||||
"title": "organization-chart-webpart",
|
||||
"id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e",
|
||||
"version": "1.1.1.0",
|
||||
"version": "1.2.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "organization-chart-webpart",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": { "node": ">=6.9.0 <9.0.0" },
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.module.scss' {
|
||||
const styles: { [className: string]: string };
|
||||
export default styles;
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { normalizeMaxDepth, normalizeMaxPeople, normalizeProfileUrl } from './OrganizationChartCore';
|
||||
import {
|
||||
IOrganizationChartData,
|
||||
IOrganizationChartListConfiguration,
|
||||
IOrganizationChartListField,
|
||||
IOrganizationChartListOption,
|
||||
IOrganizationChartListRootOption,
|
||||
IOrganizationNode,
|
||||
IUserProfile
|
||||
} from './OrganizationChartTypes';
|
||||
|
||||
interface IListTreeState {
|
||||
count: number;
|
||||
maximum: number;
|
||||
maximumDepth: number;
|
||||
truncated: boolean;
|
||||
visited: { [key: string]: boolean };
|
||||
}
|
||||
|
||||
export class OrganizationChartListDataService {
|
||||
public constructor(private context: WebPartContext) { }
|
||||
|
||||
public getLists(): Promise<IOrganizationChartListOption[]> {
|
||||
const url: string = this.webUrl + '/_api/web/lists?$select=Id,Title&$filter=Hidden eq false&$orderby=Title';
|
||||
return this.getJson(url).then((payload: any): IOrganizationChartListOption[] => // tslint:disable-line:no-any
|
||||
this.results(payload).map((list: any): IOrganizationChartListOption => ({ // tslint:disable-line:no-any
|
||||
id: this.cleanGuid(list.Id),
|
||||
title: String(list.Title || list.Id)
|
||||
})));
|
||||
}
|
||||
|
||||
public getFields(listId: string): Promise<IOrganizationChartListField[]> {
|
||||
if (!this.isGuid(listId)) { return Promise.resolve([]); }
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.cleanGuid(listId) +
|
||||
"')/fields?$select=InternalName,Title,TypeAsString,Hidden&$filter=Hidden eq false&$orderby=Title";
|
||||
return this.getJson(url).then((payload: any): IOrganizationChartListField[] => // tslint:disable-line:no-any
|
||||
this.results(payload)
|
||||
.filter((field: any): boolean => this.isSupportedField(String(field.TypeAsString || ''))) // tslint:disable-line:no-any
|
||||
.map((field: any): IOrganizationChartListField => ({ // tslint:disable-line:no-any
|
||||
internalName: String(field.InternalName || ''),
|
||||
title: String(field.Title || field.InternalName || ''),
|
||||
type: String(field.TypeAsString || '')
|
||||
})));
|
||||
}
|
||||
|
||||
public getRootOptions(config: IOrganizationChartListConfiguration): Promise<IOrganizationChartListRootOption[]> {
|
||||
if (!this.hasMinimumConfiguration(config, false)) { return Promise.resolve([]); }
|
||||
return this.getRows(config).then((rows: any[]): IOrganizationChartListRootOption[] => { // tslint:disable-line:no-any
|
||||
const options: IOrganizationChartListRootOption[] = [];
|
||||
const keys: { [key: string]: boolean } = {};
|
||||
for (let i: number = 0; i < rows.length; i++) {
|
||||
const key: string = this.toKey(rows[i][config.keyField]);
|
||||
const normalized: string = key.toLocaleLowerCase();
|
||||
if (!key || keys['$' + normalized]) { continue; }
|
||||
keys['$' + normalized] = true;
|
||||
options.push({ key: key, text: this.toDisplayText(rows[i][config.displayNameField]) || key });
|
||||
}
|
||||
return options.sort((left: IOrganizationChartListRootOption, right: IOrganizationChartListRootOption): number =>
|
||||
left.text.localeCompare(right.text));
|
||||
});
|
||||
}
|
||||
|
||||
public getOrganization(
|
||||
config: IOrganizationChartListConfiguration,
|
||||
maxDepth: number,
|
||||
maxPeople: number
|
||||
): Promise<IOrganizationChartData> {
|
||||
if (!this.hasMinimumConfiguration(config, true)) {
|
||||
return Promise.reject(new Error('The SharePoint list mapping is incomplete.'));
|
||||
}
|
||||
return this.getRows(config).then((rows: any[]): IOrganizationChartData => { // tslint:disable-line:no-any
|
||||
const profilesByKey: { [key: string]: IUserProfile } = {};
|
||||
const orderedProfiles: IUserProfile[] = [];
|
||||
for (let i: number = 0; i < rows.length; i++) {
|
||||
const profile: IUserProfile = this.mapRow(rows[i], config);
|
||||
const normalizedKey: string = profile.accountName.toLocaleLowerCase();
|
||||
if (!normalizedKey || profilesByKey['$' + normalizedKey]) { continue; }
|
||||
profilesByKey['$' + normalizedKey] = profile;
|
||||
orderedProfiles.push(profile);
|
||||
}
|
||||
|
||||
for (let i: number = 0; i < orderedProfiles.length; i++) {
|
||||
orderedProfiles[i].directReports = [];
|
||||
}
|
||||
for (let i: number = 0; i < orderedProfiles.length; i++) {
|
||||
const managerKey: string = orderedProfiles[i].managerAccount.toLocaleLowerCase();
|
||||
const manager: IUserProfile = profilesByKey['$' + managerKey];
|
||||
if (manager && manager.accountName.toLocaleLowerCase() !== orderedProfiles[i].accountName.toLocaleLowerCase()) {
|
||||
manager.directReports.push(orderedProfiles[i].accountName);
|
||||
}
|
||||
}
|
||||
|
||||
const requestedRoot: string = String(config.rootKey || '').trim().toLocaleLowerCase();
|
||||
let rootProfile: IUserProfile = requestedRoot ? profilesByKey['$' + requestedRoot] : undefined as IUserProfile;
|
||||
if (!rootProfile) {
|
||||
const roots: IUserProfile[] = orderedProfiles.filter((profile: IUserProfile): boolean =>
|
||||
!profile.managerAccount || !profilesByKey['$' + profile.managerAccount.toLocaleLowerCase()]);
|
||||
rootProfile = (roots.length ? roots : orderedProfiles)
|
||||
.sort((left: IUserProfile, right: IUserProfile): number => left.displayName.localeCompare(right.displayName))[0];
|
||||
}
|
||||
if (!rootProfile) { return { root: undefined as IOrganizationNode, peopleCount: 0, truncated: false }; }
|
||||
|
||||
const state: IListTreeState = {
|
||||
count: 0,
|
||||
maximum: normalizeMaxPeople(maxPeople),
|
||||
maximumDepth: normalizeMaxDepth(maxDepth),
|
||||
truncated: false,
|
||||
visited: {}
|
||||
};
|
||||
const root: IOrganizationNode = this.buildNode(rootProfile, profilesByKey, 0, state);
|
||||
return { root: root, peopleCount: state.count, truncated: state.truncated };
|
||||
});
|
||||
}
|
||||
|
||||
private buildNode(
|
||||
profile: IUserProfile,
|
||||
profilesByKey: { [key: string]: IUserProfile },
|
||||
depth: number,
|
||||
state: IListTreeState
|
||||
): IOrganizationNode {
|
||||
const key: string = '$' + profile.accountName.toLocaleLowerCase();
|
||||
if (state.visited[key] || state.count >= state.maximum) {
|
||||
if (state.count >= state.maximum) { state.truncated = true; }
|
||||
return undefined as IOrganizationNode;
|
||||
}
|
||||
state.visited[key] = true;
|
||||
state.count++;
|
||||
const node: IOrganizationNode = { profile: profile, children: [] };
|
||||
if (depth >= state.maximumDepth) {
|
||||
if (profile.directReports.length) { state.truncated = true; }
|
||||
return node;
|
||||
}
|
||||
for (let i: number = 0; i < profile.directReports.length; i++) {
|
||||
const child: IUserProfile = profilesByKey['$' + profile.directReports[i].toLocaleLowerCase()];
|
||||
if (!child) { continue; }
|
||||
const childNode: IOrganizationNode = this.buildNode(child, profilesByKey, depth + 1, state);
|
||||
if (childNode) { node.children.push(childNode); }
|
||||
}
|
||||
node.children.sort((left: IOrganizationNode, right: IOrganizationNode): number =>
|
||||
left.profile.displayName.localeCompare(right.profile.displayName));
|
||||
return node;
|
||||
}
|
||||
|
||||
private mapRow(row: any, config: IOrganizationChartListConfiguration): IUserProfile { // tslint:disable-line:no-any
|
||||
const keyValue: any = row[config.keyField]; // tslint:disable-line:no-any
|
||||
const displayValue: any = row[config.displayNameField]; // tslint:disable-line:no-any
|
||||
const key: string = this.toKey(keyValue);
|
||||
const email: string = this.fieldText(row, config.emailField) || this.userProperty(displayValue, 'EMail') ||
|
||||
this.userProperty(keyValue, 'EMail');
|
||||
let pictureUrl: string = normalizeProfileUrl(this.fieldUrl(row, config.pictureField), this.webUrl);
|
||||
if (!pictureUrl && key && this.isUserField(config, config.keyField)) {
|
||||
pictureUrl = this.webUrl + '/_layouts/15/userphoto.aspx?size=L&accountname=' + encodeURIComponent(key);
|
||||
}
|
||||
return {
|
||||
accountName: key,
|
||||
displayName: this.toDisplayText(displayValue) || key,
|
||||
email: email,
|
||||
title: this.fieldText(row, config.jobTitleField),
|
||||
department: this.fieldText(row, config.departmentField),
|
||||
office: this.fieldText(row, config.officeField),
|
||||
workPhone: this.fieldText(row, config.phoneField),
|
||||
cellPhone: '',
|
||||
location: '',
|
||||
managerAccount: this.toKey(row[config.managerField]),
|
||||
pictureUrl: pictureUrl,
|
||||
profileUrl: normalizeProfileUrl(this.fieldUrl(row, config.profileUrlField), this.webUrl),
|
||||
directReports: [],
|
||||
extendedManagers: []
|
||||
};
|
||||
}
|
||||
|
||||
private getRows(config: IOrganizationChartListConfiguration): Promise<any[]> { // tslint:disable-line:no-any
|
||||
if (!this.isGuid(config.listId)) { return Promise.resolve([]); }
|
||||
const names: string[] = this.uniqueNames([
|
||||
config.keyField, config.managerField, config.displayNameField, config.jobTitleField,
|
||||
config.departmentField, config.emailField, config.officeField, config.phoneField,
|
||||
config.pictureField, config.profileUrlField
|
||||
]);
|
||||
const selects: string[] = ['Id'];
|
||||
const expands: string[] = [];
|
||||
for (let i: number = 0; i < names.length; i++) {
|
||||
if (this.isUserField(config, names[i])) {
|
||||
selects.push(names[i] + '/Id', names[i] + '/Title', names[i] + '/Name', names[i] + '/EMail');
|
||||
expands.push(names[i]);
|
||||
} else {
|
||||
selects.push(names[i]);
|
||||
}
|
||||
}
|
||||
const expand: string = expands.length ? '&$expand=' + this.uniqueNames(expands).join(',') : '';
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.cleanGuid(config.listId) + "')/items" +
|
||||
'?$select=' + selects.join(',') + expand + '&$top=5000';
|
||||
return this.getJson(url).then((payload: any): any[] => this.results(payload)); // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
private fieldText(row: any, fieldName: string): string { // tslint:disable-line:no-any
|
||||
return fieldName ? this.toDisplayText(row[fieldName]) : '';
|
||||
}
|
||||
private fieldUrl(row: any, fieldName: string): string { // tslint:disable-line:no-any
|
||||
if (!fieldName) { return ''; }
|
||||
const value: any = row[fieldName]; // tslint:disable-line:no-any
|
||||
return value && value.Url ? String(value.Url) : this.toDisplayText(value);
|
||||
}
|
||||
private toKey(value: any): string { // tslint:disable-line:no-any
|
||||
if (value === undefined || value === null) { return ''; }
|
||||
if (typeof value === 'string' || typeof value === 'number') { return String(value).trim(); }
|
||||
return String(value.Name || value.LoginName || value.EMail || value.LookupValue || value.Title || value.Id || '').trim();
|
||||
}
|
||||
private toDisplayText(value: any): string { // tslint:disable-line:no-any
|
||||
if (value === undefined || value === null) { return ''; }
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); }
|
||||
return String(value.Title || value.LookupValue || value.Description || value.Url || value.Name || '').trim();
|
||||
}
|
||||
private userProperty(value: any, propertyName: string): string { // tslint:disable-line:no-any
|
||||
return value && value[propertyName] ? String(value[propertyName]) : '';
|
||||
}
|
||||
private isUserField(config: IOrganizationChartListConfiguration, internalName: string): boolean {
|
||||
for (let i: number = 0; i < config.fields.length; i++) {
|
||||
if (config.fields[i].internalName === internalName) { return config.fields[i].type === 'User'; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private hasMinimumConfiguration(config: IOrganizationChartListConfiguration, includeManager: boolean): boolean {
|
||||
return !!(config && this.isGuid(config.listId) && config.keyField && config.displayNameField &&
|
||||
(!includeManager || config.managerField));
|
||||
}
|
||||
private isSupportedField(type: string): boolean {
|
||||
return ['Text', 'Note', 'Choice', 'Number', 'Integer', 'Computed', 'User', 'URL'].indexOf(type) >= 0;
|
||||
}
|
||||
private uniqueNames(values: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (let i: number = 0; i < values.length; i++) {
|
||||
const value: string = String(values[i] || '').trim();
|
||||
if (value && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value) && result.indexOf(value) < 0) { result.push(value); }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private getJson(url: string): Promise<any> { // tslint:disable-line:no-any
|
||||
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('SharePoint list request failed (' + response.status + '): ' + text)));
|
||||
});
|
||||
}
|
||||
private results(payload: any): any[] { // tslint:disable-line:no-any
|
||||
const source: any = payload && payload.d !== undefined ? payload.d : payload; // tslint:disable-line:no-any
|
||||
return source && Array.isArray(source.results) ? source.results : (Array.isArray(source) ? source : []);
|
||||
}
|
||||
private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); }
|
||||
private get headers(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; } // tslint:disable-line:no-any
|
||||
private isGuid(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(this.cleanGuid(value)); }
|
||||
private cleanGuid(value: string): string { return String(value || '').replace(/[{}]/g, '').toLowerCase(); }
|
||||
}
|
||||
@@ -37,3 +37,37 @@ export interface IOrganizationChartPersonSuggestion {
|
||||
email: string;
|
||||
loginName: string;
|
||||
}
|
||||
|
||||
export type OrganizationChartDataSource = 'userProfile' | 'sharePointList';
|
||||
|
||||
export interface IOrganizationChartListOption {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface IOrganizationChartListField {
|
||||
internalName: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IOrganizationChartListRootOption {
|
||||
key: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface IOrganizationChartListConfiguration {
|
||||
listId: string;
|
||||
rootKey: string;
|
||||
keyField: string;
|
||||
managerField: string;
|
||||
displayNameField: string;
|
||||
jobTitleField: string;
|
||||
departmentField: string;
|
||||
emailField: string;
|
||||
officeField: string;
|
||||
phoneField: string;
|
||||
pictureField: string;
|
||||
profileUrlField: string;
|
||||
fields: IOrganizationChartListField[];
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"officeFabricIconFontName": "Org",
|
||||
"properties": {
|
||||
"title": "Organization Chart",
|
||||
"dataSource": "userProfile",
|
||||
"startUser": "",
|
||||
"startUserAccountName": "",
|
||||
"maxDepth": 6,
|
||||
@@ -26,7 +27,19 @@
|
||||
"showContactDetails": false,
|
||||
"cardBackgroundColor": "",
|
||||
"cardAccentColor": "",
|
||||
"roundedCards": false
|
||||
"roundedCards": false,
|
||||
"listId": "",
|
||||
"listRootKey": "",
|
||||
"listKeyField": "",
|
||||
"listManagerField": "",
|
||||
"listDisplayNameField": "",
|
||||
"listJobTitleField": "",
|
||||
"listDepartmentField": "",
|
||||
"listEmailField": "",
|
||||
"listOfficeField": "",
|
||||
"listPhoneField": "",
|
||||
"listPictureField": "",
|
||||
"listProfileUrlField": ""
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DisplayMode, Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
IPropertyPaneDropdownOption,
|
||||
PropertyPaneDropdown,
|
||||
PropertyPaneSlider,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle
|
||||
@@ -16,7 +18,16 @@ import {
|
||||
normalizeMaxPeople
|
||||
} from './OrganizationChartCore';
|
||||
import { OrganizationChartDataService } from './OrganizationChartDataService';
|
||||
import { IOrganizationChartData, IOrganizationChartPersonSuggestion } from './OrganizationChartTypes';
|
||||
import { OrganizationChartListDataService } from './OrganizationChartListDataService';
|
||||
import {
|
||||
IOrganizationChartData,
|
||||
IOrganizationChartListConfiguration,
|
||||
IOrganizationChartListField,
|
||||
IOrganizationChartListOption,
|
||||
IOrganizationChartListRootOption,
|
||||
IOrganizationChartPersonSuggestion,
|
||||
OrganizationChartDataSource
|
||||
} from './OrganizationChartTypes';
|
||||
import OrganizationChart from './components/OrganizationChart';
|
||||
import {
|
||||
IOrganizationChartPeoplePickerProps,
|
||||
@@ -27,6 +38,7 @@ import styles from './components/OrganizationChart.module.scss';
|
||||
|
||||
export interface IOrganizationChartWebPartProps {
|
||||
title: string;
|
||||
dataSource: OrganizationChartDataSource;
|
||||
startUser: string;
|
||||
startUserAccountName: string;
|
||||
maxDepth: number;
|
||||
@@ -37,10 +49,29 @@ export interface IOrganizationChartWebPartProps {
|
||||
cardBackgroundColor: string;
|
||||
cardAccentColor: string;
|
||||
roundedCards: boolean;
|
||||
listId: string;
|
||||
listRootKey: string;
|
||||
listKeyField: string;
|
||||
listManagerField: string;
|
||||
listDisplayNameField: string;
|
||||
listJobTitleField: string;
|
||||
listDepartmentField: string;
|
||||
listEmailField: string;
|
||||
listOfficeField: string;
|
||||
listPhoneField: string;
|
||||
listPictureField: string;
|
||||
listProfileUrlField: string;
|
||||
}
|
||||
|
||||
export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrganizationChartWebPartProps> {
|
||||
private service: OrganizationChartDataService;
|
||||
private listService: OrganizationChartListDataService;
|
||||
private lists: IOrganizationChartListOption[] = [];
|
||||
private listFields: IOrganizationChartListField[] = [];
|
||||
private listRoots: IOrganizationChartListRootOption[] = [];
|
||||
private loadingLists: boolean = false;
|
||||
private loadingFields: boolean = false;
|
||||
private loadingRoots: boolean = false;
|
||||
private requestVersion: number = 0;
|
||||
private disposed: boolean = false;
|
||||
private pickerHost: HTMLDivElement;
|
||||
@@ -49,14 +80,23 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
|
||||
public render(): void {
|
||||
const account: string = normalizeAccount(this.properties.startUser);
|
||||
const version: number = ++this.requestVersion;
|
||||
if (this.isListSource() && !this.isListConfigured()) {
|
||||
this.renderStatus(strings.ListConfigureMessage, false);
|
||||
return;
|
||||
}
|
||||
this.renderStatus(strings.LoadingMessage, false);
|
||||
this.service.getOrganization(
|
||||
account,
|
||||
normalizeMaxDepth(this.properties.maxDepth),
|
||||
normalizeMaxPeople(this.properties.maxPeople),
|
||||
normalizeCacheMinutes(this.properties.cacheMinutes),
|
||||
normalizeAccount(this.properties.startUserAccountName)
|
||||
).then((data: IOrganizationChartData): void => {
|
||||
const request: Promise<IOrganizationChartData> = this.isListSource()
|
||||
? this.listService.getOrganization(
|
||||
this.getListConfiguration(),
|
||||
normalizeMaxDepth(this.properties.maxDepth),
|
||||
normalizeMaxPeople(this.properties.maxPeople))
|
||||
: this.service.getOrganization(
|
||||
account,
|
||||
normalizeMaxDepth(this.properties.maxDepth),
|
||||
normalizeMaxPeople(this.properties.maxPeople),
|
||||
normalizeCacheMinutes(this.properties.cacheMinutes),
|
||||
normalizeAccount(this.properties.startUserAccountName));
|
||||
request.then((data: IOrganizationChartData): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (!data.root) { this.renderStatus(strings.EmptyMessage, false); return; }
|
||||
const element: React.ReactElement<IOrganizationChartProps> = React.createElement(OrganizationChart, {
|
||||
@@ -88,7 +128,12 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
this.service = new OrganizationChartDataService(this.context);
|
||||
return super.onInit();
|
||||
this.listService = new OrganizationChartListDataService(this.context);
|
||||
return super.onInit().then((): Promise<void> => this.isListSource() ? this.loadLists() : Promise.resolve())
|
||||
.then((): Promise<void> => this.isListSource() && this.properties.listId
|
||||
? this.loadListFields(this.properties.listId) : Promise.resolve())
|
||||
.then((): Promise<void> => this.isListSource() && this.canLoadRoots()
|
||||
? this.loadListRoots() : Promise.resolve());
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
@@ -99,18 +144,97 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
|
||||
|
||||
protected get dataVersion(): Version { return Version.parse('1.0'); }
|
||||
|
||||
protected onPropertyPaneConfigurationStart(): void {
|
||||
if (!this.isListSource()) { return; }
|
||||
if (!this.lists.length && !this.loadingLists) { this.loadLists(); }
|
||||
if (this.properties.listId && !this.listFields.length && !this.loadingFields) {
|
||||
this.loadListFields(this.properties.listId);
|
||||
}
|
||||
if (this.canLoadRoots() && !this.listRoots.length && !this.loadingRoots) { this.loadListRoots(); }
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { // tslint:disable-line:no-any
|
||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
if (propertyPath === 'dataSource' && newValue !== oldValue) {
|
||||
this.context.propertyPane.refresh();
|
||||
if (this.isListSource()) {
|
||||
this.loadLists()
|
||||
.then((): Promise<void> => this.properties.listId
|
||||
? this.loadListFields(this.properties.listId) : Promise.resolve())
|
||||
.then((): void => this.render());
|
||||
} else { this.render(); }
|
||||
} else if (propertyPath === 'listId' && newValue !== oldValue) {
|
||||
this.resetListMapping();
|
||||
this.context.propertyPane.refresh();
|
||||
this.loadListFields(String(newValue || '')).then((): void => this.render());
|
||||
} else if ((propertyPath === 'listKeyField' || propertyPath === 'listDisplayNameField') && newValue !== oldValue) {
|
||||
this.properties.listRootKey = '';
|
||||
this.listRoots = [];
|
||||
this.context.propertyPane.refresh();
|
||||
this.loadListRoots().then((): void => this.render());
|
||||
}
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
const fieldOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.NoFieldOption }];
|
||||
const requiredFieldOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectFieldOption }];
|
||||
const listOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectListOption }];
|
||||
const rootOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.AutomaticRootOption }];
|
||||
for (let i: number = 0; i < this.listFields.length; i++) {
|
||||
const option: IPropertyPaneDropdownOption = {
|
||||
key: this.listFields[i].internalName,
|
||||
text: this.listFields[i].title + ' (' + this.listFields[i].internalName + ')'
|
||||
};
|
||||
fieldOptions.push(option);
|
||||
requiredFieldOptions.push(option);
|
||||
}
|
||||
for (let i: number = 0; i < this.lists.length; i++) {
|
||||
listOptions.push({ key: this.lists[i].id, text: this.lists[i].title });
|
||||
}
|
||||
for (let i: number = 0; i < this.listRoots.length; i++) {
|
||||
rootOptions.push({ key: this.listRoots[i].key, text: this.listRoots[i].text });
|
||||
}
|
||||
const sourceFields: any[] = [ // tslint:disable-line:no-any
|
||||
PropertyPaneTextField('title', { label: strings.TitleFieldLabel }),
|
||||
PropertyPaneDropdown('dataSource', {
|
||||
label: strings.DataSourceFieldLabel,
|
||||
options: [
|
||||
{ key: 'userProfile', text: strings.UserProfileSourceOption },
|
||||
{ key: 'sharePointList', text: strings.SharePointListSourceOption }
|
||||
]
|
||||
})
|
||||
];
|
||||
if (this.isListSource()) {
|
||||
sourceFields.push(
|
||||
PropertyPaneDropdown('listId', { label: strings.ListFieldLabel, options: listOptions, disabled: this.loadingLists }),
|
||||
PropertyPaneDropdown('listKeyField', { label: strings.ListKeyFieldLabel, options: requiredFieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listManagerField', { label: strings.ListManagerFieldLabel, options: requiredFieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listDisplayNameField', { label: strings.ListDisplayNameFieldLabel, options: requiredFieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listRootKey', { label: strings.ListRootFieldLabel, options: rootOptions, disabled: this.loadingRoots || !this.canLoadRoots() }),
|
||||
PropertyPaneDropdown('listJobTitleField', { label: strings.ListJobTitleFieldLabel, options: fieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listDepartmentField', { label: strings.ListDepartmentFieldLabel, options: fieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listEmailField', { label: strings.ListEmailFieldLabel, options: fieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listOfficeField', { label: strings.ListOfficeFieldLabel, options: fieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listPhoneField', { label: strings.ListPhoneFieldLabel, options: fieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listPictureField', { label: strings.ListPictureFieldLabel, options: fieldOptions, disabled: this.loadingFields }),
|
||||
PropertyPaneDropdown('listProfileUrlField', { label: strings.ListProfileUrlFieldLabel, options: fieldOptions, disabled: this.loadingFields })
|
||||
);
|
||||
}
|
||||
sourceFields.push(
|
||||
PropertyPaneSlider('maxDepth', { label: strings.MaxDepthFieldLabel, min: 1, max: 10, step: 1 }),
|
||||
PropertyPaneSlider('maxPeople', { label: strings.MaxPeopleFieldLabel, min: 10, max: 500, step: 10 })
|
||||
);
|
||||
if (!this.isListSource()) {
|
||||
sourceFields.push(PropertyPaneSlider('cacheMinutes', {
|
||||
label: strings.CacheMinutesFieldLabel, min: 0, max: 120, step: 5
|
||||
}));
|
||||
}
|
||||
return {
|
||||
pages: [{
|
||||
header: { description: strings.PropertyPaneDescription },
|
||||
groups: [{
|
||||
groupName: strings.SourceGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('title', { label: strings.TitleFieldLabel }),
|
||||
PropertyPaneSlider('maxDepth', { label: strings.MaxDepthFieldLabel, min: 1, max: 10, step: 1 }),
|
||||
PropertyPaneSlider('maxPeople', { label: strings.MaxPeopleFieldLabel, min: 10, max: 500, step: 10 }),
|
||||
PropertyPaneSlider('cacheMinutes', { label: strings.CacheMinutesFieldLabel, min: 0, max: 120, step: 5 })
|
||||
]
|
||||
groupFields: sourceFields
|
||||
}, {
|
||||
groupName: strings.DisplayGroupName,
|
||||
groupFields: [
|
||||
@@ -143,7 +267,7 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
|
||||
private prepareSurface(selectedDisplayName: string): HTMLDivElement {
|
||||
this.clearSurface();
|
||||
this.domElement.innerHTML = '';
|
||||
if (this.displayMode === DisplayMode.Edit) {
|
||||
if (this.displayMode === DisplayMode.Edit && !this.isListSource()) {
|
||||
this.pickerHost = document.createElement('div');
|
||||
this.domElement.appendChild(this.pickerHost);
|
||||
const picker: React.ReactElement<IOrganizationChartPeoplePickerProps> =
|
||||
@@ -186,4 +310,138 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
|
||||
this.properties.startUserAccountName = fallback;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private isListSource(): boolean {
|
||||
return this.properties.dataSource === 'sharePointList';
|
||||
}
|
||||
|
||||
private isListConfigured(): boolean {
|
||||
return !!(this.properties.listId && this.properties.listKeyField &&
|
||||
this.properties.listManagerField && this.properties.listDisplayNameField);
|
||||
}
|
||||
|
||||
private canLoadRoots(): boolean {
|
||||
return !!(this.properties.listId && this.properties.listKeyField && this.properties.listDisplayNameField);
|
||||
}
|
||||
|
||||
private getListConfiguration(): IOrganizationChartListConfiguration {
|
||||
return {
|
||||
listId: this.properties.listId || '',
|
||||
rootKey: this.properties.listRootKey || '',
|
||||
keyField: this.properties.listKeyField || '',
|
||||
managerField: this.properties.listManagerField || '',
|
||||
displayNameField: this.properties.listDisplayNameField || '',
|
||||
jobTitleField: this.properties.listJobTitleField || '',
|
||||
departmentField: this.properties.listDepartmentField || '',
|
||||
emailField: this.properties.listEmailField || '',
|
||||
officeField: this.properties.listOfficeField || '',
|
||||
phoneField: this.properties.listPhoneField || '',
|
||||
pictureField: this.properties.listPictureField || '',
|
||||
profileUrlField: this.properties.listProfileUrlField || '',
|
||||
fields: this.listFields
|
||||
};
|
||||
}
|
||||
|
||||
private loadLists(): Promise<void> {
|
||||
this.loadingLists = true;
|
||||
this.context.propertyPane.refresh();
|
||||
return this.listService.getLists().then((lists: IOrganizationChartListOption[]): void => {
|
||||
this.lists = lists;
|
||||
const selectedId: string = String(this.properties.listId || '').replace(/[{}]/g, '').toLowerCase();
|
||||
if (selectedId && !lists.some((list: IOrganizationChartListOption): boolean => list.id === selectedId)) {
|
||||
this.properties.listId = '';
|
||||
this.resetListMapping();
|
||||
}
|
||||
}).catch((error: Error): void => {
|
||||
this.lists = [];
|
||||
if (window.console) { console.error('[OrganizationChart] Lists could not be loaded.', error); }
|
||||
}).then((): void => {
|
||||
this.loadingLists = false;
|
||||
this.context.propertyPane.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
private loadListFields(listId: string): Promise<void> {
|
||||
if (!listId) { this.listFields = []; return Promise.resolve(); }
|
||||
this.loadingFields = true;
|
||||
this.context.propertyPane.refresh();
|
||||
return this.listService.getFields(listId).then((fields: IOrganizationChartListField[]): void => {
|
||||
this.listFields = fields;
|
||||
this.applyListFieldDefaults();
|
||||
}).catch((error: Error): void => {
|
||||
this.listFields = [];
|
||||
if (window.console) { console.error('[OrganizationChart] List fields could not be loaded.', error); }
|
||||
}).then((): Promise<void> => {
|
||||
this.loadingFields = false;
|
||||
this.context.propertyPane.refresh();
|
||||
return this.canLoadRoots() ? this.loadListRoots() : Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private loadListRoots(): Promise<void> {
|
||||
if (!this.canLoadRoots()) { this.listRoots = []; return Promise.resolve(); }
|
||||
this.loadingRoots = true;
|
||||
this.context.propertyPane.refresh();
|
||||
return this.listService.getRootOptions(this.getListConfiguration())
|
||||
.then((roots: IOrganizationChartListRootOption[]): void => {
|
||||
this.listRoots = roots;
|
||||
if (this.properties.listRootKey &&
|
||||
!roots.some((root: IOrganizationChartListRootOption): boolean => root.key === this.properties.listRootKey)) {
|
||||
this.properties.listRootKey = '';
|
||||
}
|
||||
}).catch((error: Error): void => {
|
||||
this.listRoots = [];
|
||||
if (window.console) { console.error('[OrganizationChart] List roots could not be loaded.', error); }
|
||||
}).then((): void => {
|
||||
this.loadingRoots = false;
|
||||
this.context.propertyPane.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
private applyListFieldDefaults(): void {
|
||||
this.properties.listKeyField = this.validOrDefault(this.properties.listKeyField,
|
||||
['AccountName', 'Email', 'Title']);
|
||||
this.properties.listManagerField = this.validOrDefault(this.properties.listManagerField,
|
||||
['ManagerAccount', 'Manager'], true);
|
||||
this.properties.listDisplayNameField = this.validOrDefault(this.properties.listDisplayNameField,
|
||||
['DisplayName', 'Title']);
|
||||
this.properties.listJobTitleField = this.validOrDefault(this.properties.listJobTitleField,
|
||||
['JobTitle', 'Position'], true);
|
||||
this.properties.listDepartmentField = this.validOrDefault(this.properties.listDepartmentField,
|
||||
['Department'], true);
|
||||
this.properties.listEmailField = this.validOrDefault(this.properties.listEmailField, ['Email'], true);
|
||||
this.properties.listOfficeField = this.validOrDefault(this.properties.listOfficeField,
|
||||
['Office', 'Location'], true);
|
||||
this.properties.listPhoneField = this.validOrDefault(this.properties.listPhoneField,
|
||||
['Phone', 'WorkPhone'], true);
|
||||
this.properties.listPictureField = this.validOrDefault(this.properties.listPictureField,
|
||||
['PictureUrl', 'Picture'], true);
|
||||
this.properties.listProfileUrlField = this.validOrDefault(this.properties.listProfileUrlField,
|
||||
['ProfileUrl'], true);
|
||||
}
|
||||
|
||||
private validOrDefault(current: string, preferredNames: string[], optional?: boolean): string {
|
||||
const names: string[] = this.listFields.map((field: IOrganizationChartListField): string => field.internalName);
|
||||
if (names.indexOf(current) >= 0) { return current; }
|
||||
for (let i: number = 0; i < preferredNames.length; i++) {
|
||||
if (names.indexOf(preferredNames[i]) >= 0) { return preferredNames[i]; }
|
||||
}
|
||||
return optional ? '' : (names[0] || '');
|
||||
}
|
||||
|
||||
private resetListMapping(): void {
|
||||
this.listFields = [];
|
||||
this.listRoots = [];
|
||||
this.properties.listRootKey = '';
|
||||
this.properties.listKeyField = '';
|
||||
this.properties.listManagerField = '';
|
||||
this.properties.listDisplayNameField = '';
|
||||
this.properties.listJobTitleField = '';
|
||||
this.properties.listDepartmentField = '';
|
||||
this.properties.listEmailField = '';
|
||||
this.properties.listOfficeField = '';
|
||||
this.properties.listPhoneField = '';
|
||||
this.properties.listPictureField = '';
|
||||
this.properties.listProfileUrlField = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
define([], function() {
|
||||
return {
|
||||
PropertyPaneDescription: 'Erstellt das Organigramm aus dem SharePoint User Profile Service.',
|
||||
PropertyPaneDescription: 'Erstellt das Organigramm aus Benutzerprofilen oder einer SharePoint-Liste.',
|
||||
SourceGroupName: 'Startpunkt und Umfang',
|
||||
DisplayGroupName: 'Darstellung',
|
||||
TitleFieldLabel: 'WebPart-Titel',
|
||||
DataSourceFieldLabel: 'Datenquelle',
|
||||
UserProfileSourceOption: 'User Profile Service',
|
||||
SharePointListSourceOption: 'SharePoint-Liste',
|
||||
ListFieldLabel: 'Organigramm-Liste',
|
||||
SelectListOption: 'Liste auswählen...',
|
||||
SelectFieldOption: 'Spalte auswählen...',
|
||||
NoFieldOption: 'Keine Spalte',
|
||||
AutomaticRootOption: 'Automatisch ermitteln',
|
||||
ListKeyFieldLabel: 'Spalte für eindeutigen Schlüssel',
|
||||
ListManagerFieldLabel: 'Spalte für Manager-Schlüssel',
|
||||
ListDisplayNameFieldLabel: 'Spalte für Anzeigename',
|
||||
ListRootFieldLabel: 'Startperson',
|
||||
ListJobTitleFieldLabel: 'Spalte für Position',
|
||||
ListDepartmentFieldLabel: 'Spalte für Abteilung',
|
||||
ListEmailFieldLabel: 'Spalte für E-Mail',
|
||||
ListOfficeFieldLabel: 'Spalte für Büro / Standort',
|
||||
ListPhoneFieldLabel: 'Spalte für Telefon',
|
||||
ListPictureFieldLabel: 'Spalte für Bild-URL',
|
||||
ListProfileUrlFieldLabel: 'Spalte für Profil-URL',
|
||||
StartUserFieldLabel: 'Startperson / Manager',
|
||||
StartUserFieldDescription: 'Einen geprüften SharePoint-Benutzer suchen und auswählen. Leer lassen, um beim aktuellen Benutzer zu starten.',
|
||||
StartUserPlaceholder: 'Name oder E-Mail-Adresse eingeben',
|
||||
@@ -21,9 +40,10 @@ define([], function() {
|
||||
CardAccentColorFieldLabel: 'Akzentfarbe der Karten',
|
||||
ColorFieldDescription: 'Optionale Hexadezimalfarbe, zum Beispiel #ffffff.',
|
||||
ConfigureMessage: 'Es konnte keine Startperson bestimmt werden.',
|
||||
ListConfigureMessage: 'Wählen Sie eine SharePoint-Liste und ordnen Sie Schlüssel-, Manager- und Anzeigenamenspalte zu.',
|
||||
LoadingMessage: 'Organigramm wird geladen…',
|
||||
EmptyMessage: 'Für die Startperson wurden keine Profildaten gefunden.',
|
||||
LoadErrorMessage: 'Das Organigramm konnte nicht aus dem User Profile Service geladen werden.',
|
||||
LoadErrorMessage: 'Das Organigramm konnte nicht aus der konfigurierten Datenquelle geladen werden.',
|
||||
DefaultTitle: 'Organigramm',
|
||||
ReportsLabel: 'Mitarbeiter',
|
||||
ExpandDetailsLabel: 'Personendetails anzeigen',
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
define([], function() {
|
||||
return {
|
||||
PropertyPaneDescription: 'Builds the organization chart from the SharePoint User Profile Service.',
|
||||
PropertyPaneDescription: 'Builds the organization chart from user profiles or a SharePoint list.',
|
||||
SourceGroupName: 'Starting point and scope',
|
||||
DisplayGroupName: 'Display',
|
||||
TitleFieldLabel: 'Web part title',
|
||||
DataSourceFieldLabel: 'Data source',
|
||||
UserProfileSourceOption: 'User Profile Service',
|
||||
SharePointListSourceOption: 'SharePoint list',
|
||||
ListFieldLabel: 'Organization list',
|
||||
SelectListOption: 'Select a list...',
|
||||
SelectFieldOption: 'Select a column...',
|
||||
NoFieldOption: 'No column',
|
||||
AutomaticRootOption: 'Determine automatically',
|
||||
ListKeyFieldLabel: 'Unique key column',
|
||||
ListManagerFieldLabel: 'Manager key column',
|
||||
ListDisplayNameFieldLabel: 'Display name column',
|
||||
ListRootFieldLabel: 'Starting person',
|
||||
ListJobTitleFieldLabel: 'Job title column',
|
||||
ListDepartmentFieldLabel: 'Department column',
|
||||
ListEmailFieldLabel: 'Email column',
|
||||
ListOfficeFieldLabel: 'Office / location column',
|
||||
ListPhoneFieldLabel: 'Phone column',
|
||||
ListPictureFieldLabel: 'Picture URL column',
|
||||
ListProfileUrlFieldLabel: 'Profile URL column',
|
||||
StartUserFieldLabel: 'Starting person / manager',
|
||||
StartUserFieldDescription: 'Search for and select a verified SharePoint user. Leave empty to start with the current user.',
|
||||
StartUserPlaceholder: 'Enter a name or email address',
|
||||
@@ -21,9 +40,10 @@ define([], function() {
|
||||
CardAccentColorFieldLabel: 'Card accent color',
|
||||
ColorFieldDescription: 'Optional hexadecimal color, for example #ffffff.',
|
||||
ConfigureMessage: 'A starting person could not be determined.',
|
||||
ListConfigureMessage: 'Select a SharePoint list and map the key, manager and display name columns.',
|
||||
LoadingMessage: 'Loading organization chart…',
|
||||
EmptyMessage: 'No profile data was found for the starting person.',
|
||||
LoadErrorMessage: 'The organization chart could not be loaded from the User Profile Service.',
|
||||
LoadErrorMessage: 'The organization chart could not be loaded from the configured data source.',
|
||||
DefaultTitle: 'Organization Chart',
|
||||
ReportsLabel: 'reports',
|
||||
ExpandDetailsLabel: 'Show person details',
|
||||
|
||||
@@ -3,6 +3,25 @@ declare interface IOrganizationChartWebPartStrings {
|
||||
SourceGroupName: string;
|
||||
DisplayGroupName: string;
|
||||
TitleFieldLabel: string;
|
||||
DataSourceFieldLabel: string;
|
||||
UserProfileSourceOption: string;
|
||||
SharePointListSourceOption: string;
|
||||
ListFieldLabel: string;
|
||||
SelectListOption: string;
|
||||
SelectFieldOption: string;
|
||||
NoFieldOption: string;
|
||||
AutomaticRootOption: string;
|
||||
ListKeyFieldLabel: string;
|
||||
ListManagerFieldLabel: string;
|
||||
ListDisplayNameFieldLabel: string;
|
||||
ListRootFieldLabel: string;
|
||||
ListJobTitleFieldLabel: string;
|
||||
ListDepartmentFieldLabel: string;
|
||||
ListEmailFieldLabel: string;
|
||||
ListOfficeFieldLabel: string;
|
||||
ListPhoneFieldLabel: string;
|
||||
ListPictureFieldLabel: string;
|
||||
ListProfileUrlFieldLabel: string;
|
||||
StartUserFieldLabel: string;
|
||||
StartUserFieldDescription: string;
|
||||
StartUserPlaceholder: string;
|
||||
@@ -20,6 +39,7 @@ declare interface IOrganizationChartWebPartStrings {
|
||||
CardAccentColorFieldLabel: string;
|
||||
ColorFieldDescription: string;
|
||||
ConfigureMessage: string;
|
||||
ListConfigureMessage: string;
|
||||
LoadingMessage: string;
|
||||
EmptyMessage: string;
|
||||
LoadErrorMessage: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ var packageSolution = json('config/package-solution.json');
|
||||
var config = json('config/config.json');
|
||||
var manifest = json('src/webparts/organizationChart/OrganizationChartWebPart.manifest.json');
|
||||
var service = read('src/webparts/organizationChart/OrganizationChartDataService.ts');
|
||||
var listService = read('src/webparts/organizationChart/OrganizationChartListDataService.ts');
|
||||
var webPart = read('src/webparts/organizationChart/OrganizationChartWebPart.ts');
|
||||
var component = read('src/webparts/organizationChart/components/OrganizationChart.tsx');
|
||||
var picker = read('src/webparts/organizationChart/components/OrganizationChartPeoplePicker.tsx');
|
||||
@@ -25,6 +26,10 @@ assert(packageSolution.solution.includeClientSideAssets === true && packageSolut
|
||||
'Central deployment is not configured.');
|
||||
assert(!!config.bundles['organization-chart-webpart'], 'Bundle name is inconsistent.');
|
||||
assert(manifest.componentType === 'WebPart' && manifest.alias === 'OrganizationChartWebPart', 'Manifest is invalid.');
|
||||
assert(packageJson.version === '1.2.0' && packageSolution.solution.version === '1.2.0.0',
|
||||
'Organization Chart version is inconsistent.');
|
||||
assert(webPart.indexOf("PropertyPaneTextField('title'") >= 0 && component.indexOf('styles.heading') >= 0,
|
||||
'Configurable web part title 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,
|
||||
'Reliable current-user profile access is missing.');
|
||||
@@ -32,9 +37,18 @@ assert(service.indexOf('ClientPeoplePickerWebServiceInterface.clientPeoplePicker
|
||||
'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("PropertyPaneDropdown('dataSource'") >= 0 &&
|
||||
webPart.indexOf("PropertyPaneDropdown('listId'") >= 0 &&
|
||||
webPart.indexOf("PropertyPaneDropdown('listManagerField'") >= 0,
|
||||
'SharePoint list data-source configuration is missing.');
|
||||
assert(listService.indexOf('/_api/web/lists') >= 0 && listService.indexOf('getOrganization') >= 0 &&
|
||||
listService.indexOf('manager.directReports.push') >= 0,
|
||||
'SharePoint list organization tree loading is missing.');
|
||||
assert(webPart.indexOf('OrganizationChartPeoplePicker') >= 0 &&
|
||||
webPart.indexOf("PropertyPaneTextField('startUser'") < 0,
|
||||
'The verified starting-person picker must be rendered on the canvas instead of the property pane.');
|
||||
assert(webPart.indexOf('DisplayMode.Edit && !this.isListSource()') >= 0,
|
||||
'The User Profile people picker must be hidden for SharePoint-list data sources.');
|
||||
assert(picker.indexOf('NormalPeoplePicker') >= 0 && picker.indexOf('ValidationState.invalid') >= 0,
|
||||
'The on-canvas people picker must reject unverified free-text values.');
|
||||
assert(webPart.indexOf("PropertyPaneSlider('maxDepth'") >= 0 && webPart.indexOf("PropertyPaneSlider('maxPeople'") >= 0,
|
||||
|
||||
Reference in New Issue
Block a user