feat: Implement SharePoint People Picker for selecting starting person and update version to 1.0.3

This commit is contained in:
Torsten Brendgen
2026-08-24 14:28:01 +02:00
parent 4e5dafabfc
commit a686f8c055
13 changed files with 299 additions and 48 deletions
+4 -6
View File
@@ -5,6 +5,7 @@ SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es erstellt aus
## Funktionen
- aktueller Benutzer oder konfigurierter Manager als Startpunkt
- Startperson über einen SharePoint-People-Picker direkt im WebPart auswählen
- rekursives Laden aller darunterliegenden Ebenen über `DirectReports`
- konfigurierbare maximale Tiefe und Personenzahl
- standardmäßig vollständig aufgeklappter Baum
@@ -20,19 +21,16 @@ Das WebPart verwendet direkt:
```text
/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)
/_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.
## Startperson
Im Property Pane kann als **Startperson / Manager** angegeben werden:
Im Bearbeitungsmodus der modernen Seite wird direkt oberhalb des Organigramms ein People Picker angezeigt. Dort kann die Startperson per Name oder E-Mail-Adresse gesucht werden. Das WebPart akzeptiert nur einen von SharePoint aufgelösten Benutzer und prüft bei der Auswahl zusätzlich, ob dessen Profil über den User Profile Service gelesen werden kann. Gespeichert werden der kanonische Login und der Accountname als Fallback.
- Claims-Login, beispielsweise `i:0#.w|CONTOSO\max.mustermann`
- klassischer Login, beispielsweise `CONTOSO\max.mustermann`
- je nach Profilkonfiguration ein UPN wie `max.mustermann@contoso.local`
Bleibt das Feld leer, verwendet das WebPart den angemeldeten Benutzer.
Wird die Auswahl entfernt, verwendet das WebPart wieder den angemeldeten Benutzer. Im Anzeigemodus ist der People Picker nicht sichtbar.
## Build-Umgebung
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "organization-chart-webpart",
"title": "organization-chart-webpart",
"id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e",
"version": "1.0.2.0",
"version": "1.0.3.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "organization-chart-webpart",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"main": "lib/index.js",
"engines": { "node": ">=6.9.0 <9.0.0" },
@@ -19,6 +19,7 @@
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "5.21.0",
"react": "15.6.2",
"react-dom": "15.6.2"
},
@@ -11,6 +11,7 @@ import {
} from './OrganizationChartCore';
import {
IOrganizationChartData,
IOrganizationChartPersonSuggestion,
IOrganizationNode,
IUserProfile,
IUserProfileProperty
@@ -35,6 +36,7 @@ interface IPeoplePickerEntityData {
}
interface IPeoplePickerEntity {
DisplayText?: string;
EntityData?: IPeoplePickerEntityData;
IsResolved?: boolean;
Key?: string;
@@ -49,7 +51,8 @@ export class OrganizationChartDataService {
accountName: string,
maxDepth: number,
maxPeople: number,
cacheMinutes: number
cacheMinutes: number,
alternateAccountName?: string
): Promise<IOrganizationChartData> {
const account: string = normalizeAccount(accountName);
const state: ITreeLoadState = {
@@ -60,7 +63,7 @@ export class OrganizationChartDataService {
visited: {}
};
const rootRequest: Promise<IUserProfile> = account
? this.getConfiguredProfile(account, cacheMinutes)
? this.getConfiguredProfile(account, cacheMinutes, alternateAccountName)
: this.getMyProfile(cacheMinutes);
return rootRequest
.then((profile: IUserProfile): Promise<IOrganizationNode> =>
@@ -94,11 +97,39 @@ export class OrganizationChartDataService {
return this.requestProfile(url, key, cacheMinutes);
}
private getConfiguredProfile(accountName: string, cacheMinutes: number): Promise<IUserProfile> {
public searchPeople(query: string): Promise<IOrganizationChartPersonSuggestion[]> {
const searchText: string = normalizeAccount(query);
if (searchText.length < 2) { return Promise.resolve([]); }
return this.requestPeoplePicker(searchText).then(
(entities: IPeoplePickerEntity[]): IOrganizationChartPersonSuggestion[] =>
entities.filter((entity: IPeoplePickerEntity): boolean =>
entity.IsResolved !== false && !!normalizeAccount(entity.Key))
.map((entity: IPeoplePickerEntity): IOrganizationChartPersonSuggestion => ({
accountName: normalizeAccount(entity.EntityData && entity.EntityData.AccountName),
displayName: normalizeAccount(entity.DisplayText) || normalizeAccount(entity.Key),
email: normalizeAccount(entity.EntityData && entity.EntityData.Email),
loginName: normalizeAccount(entity.Key)
}))
);
}
public validatePerson(loginName: string, accountName: string, cacheMinutes: number): Promise<boolean> {
const candidates: string[] = uniqueAccounts([loginName, accountName], '', 2);
return this.getFirstAvailableProfile(candidates, cacheMinutes, 0)
.then((): boolean => true, (): boolean => false);
}
private getConfiguredProfile(
accountName: string,
cacheMinutes: number,
alternateAccountName?: string
): Promise<IUserProfile> {
return this.resolveAccountNames(accountName)
.catch((): string[] => [accountName])
.then((accounts: string[]): Promise<IUserProfile> =>
this.getFirstAvailableProfile(accounts, cacheMinutes, 0));
.then((accounts: string[]): Promise<IUserProfile> => {
const candidates: string[] = uniqueAccounts(accounts.concat([alternateAccountName]), '', accounts.length + 1);
return this.getFirstAvailableProfile(candidates, cacheMinutes, 0);
});
}
private getFirstAvailableProfile(
@@ -116,6 +147,29 @@ export class OrganizationChartDataService {
}
private resolveAccountNames(accountName: string): Promise<string[]> {
return this.searchPeople(accountName).then(
(suggestions: IOrganizationChartPersonSuggestion[]): string[] => {
const input: string = normalizeAccount(accountName).toLowerCase();
const matching: IOrganizationChartPersonSuggestion[] = suggestions.filter(
(suggestion: IOrganizationChartPersonSuggestion): boolean =>
suggestion.accountName.toLowerCase() === input ||
suggestion.email.toLowerCase() === input ||
suggestion.loginName.toLowerCase() === input
);
const selected: IOrganizationChartPersonSuggestion[] = matching.length
? matching
: (suggestions.length === 1 ? suggestions : []);
const candidates: string[] = [];
selected.forEach((suggestion: IOrganizationChartPersonSuggestion): void => {
candidates.push(suggestion.loginName);
candidates.push(suggestion.accountName);
});
candidates.push(normalizeAccount(accountName));
return uniqueAccounts(candidates, '', candidates.length || 1);
});
}
private requestPeoplePicker(accountName: string): Promise<IPeoplePickerEntity[]> {
const url: string = this.webUrl +
'/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser';
const body: string = JSON.stringify({
@@ -140,30 +194,14 @@ export class OrganizationChartDataService {
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
}).then((payload: any): IPeoplePickerEntity[] => { // 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);
try { return JSON.parse(serialized) as IPeoplePickerEntity[]; } catch (error) { return []; }
});
}
@@ -30,3 +30,10 @@ export interface IOrganizationChartData {
peopleCount: number;
truncated: boolean;
}
export interface IOrganizationChartPersonSuggestion {
accountName: string;
displayName: string;
email: string;
loginName: string;
}
@@ -18,6 +18,7 @@
"properties": {
"title": "Organization Chart",
"startUser": "",
"startUserAccountName": "",
"maxDepth": 6,
"maxPeople": 100,
"cacheMinutes": 15,
@@ -1,6 +1,6 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { DisplayMode, Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
@@ -16,14 +16,19 @@ import {
normalizeMaxPeople
} from './OrganizationChartCore';
import { OrganizationChartDataService } from './OrganizationChartDataService';
import { IOrganizationChartData } from './OrganizationChartTypes';
import { IOrganizationChartData, IOrganizationChartPersonSuggestion } from './OrganizationChartTypes';
import OrganizationChart from './components/OrganizationChart';
import {
IOrganizationChartPeoplePickerProps,
OrganizationChartPeoplePicker
} from './components/OrganizationChartPeoplePicker';
import { IOrganizationChartProps } from './components/IOrganizationChartProps';
import styles from './components/OrganizationChart.module.scss';
export interface IOrganizationChartWebPartProps {
title: string;
startUser: string;
startUserAccountName: string;
maxDepth: number;
maxPeople: number;
cacheMinutes: number;
@@ -35,6 +40,8 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
private service: OrganizationChartDataService;
private requestVersion: number = 0;
private disposed: boolean = false;
private pickerHost: HTMLDivElement;
private contentHost: HTMLDivElement;
public render(): void {
const account: string = normalizeAccount(this.properties.startUser);
@@ -44,7 +51,8 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
account,
normalizeMaxDepth(this.properties.maxDepth),
normalizeMaxPeople(this.properties.maxPeople),
normalizeCacheMinutes(this.properties.cacheMinutes)
normalizeCacheMinutes(this.properties.cacheMinutes),
normalizeAccount(this.properties.startUserAccountName)
).then((data: IOrganizationChartData): void => {
if (this.disposed || version !== this.requestVersion) { return; }
if (!data.root) { this.renderStatus(strings.EmptyMessage, false); return; }
@@ -59,7 +67,8 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
truncatedLabel: strings.TruncatedLabel,
peopleCountLabel: strings.PeopleCountLabel
});
ReactDom.render(element, this.domElement);
const contentHost: HTMLDivElement = this.prepareSurface(data.root.profile.displayName);
ReactDom.render(element, contentHost);
}).catch((error: Error): void => {
if (this.disposed || version !== this.requestVersion) { return; }
if (window.console) { console.error('[OrganizationChart] ' + strings.LoadErrorMessage, error); }
@@ -75,7 +84,7 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
protected onDispose(): void {
this.disposed = true;
this.requestVersion++;
ReactDom.unmountComponentAtNode(this.domElement);
this.clearSurface();
}
protected get dataVersion(): Version { return Version.parse('1.0'); }
@@ -88,11 +97,6 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
groupName: strings.SourceGroupName,
groupFields: [
PropertyPaneTextField('title', { label: strings.TitleFieldLabel }),
PropertyPaneTextField('startUser', {
label: strings.StartUserFieldLabel,
description: strings.StartUserFieldDescription,
placeholder: strings.StartUserPlaceholder
}),
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 })
@@ -109,12 +113,61 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrg
}
private renderStatus(message: string, error: boolean): void {
ReactDom.unmountComponentAtNode(this.domElement);
const contentHost: HTMLDivElement = this.prepareSurface(this.properties.startUser);
const status: HTMLDivElement = document.createElement('div');
status.className = error ? styles.error : styles.status;
status.setAttribute('role', error ? 'alert' : 'status');
status.textContent = message;
contentHost.appendChild(status);
}
private prepareSurface(selectedDisplayName: string): HTMLDivElement {
this.clearSurface();
this.domElement.innerHTML = '';
this.domElement.appendChild(status);
if (this.displayMode === DisplayMode.Edit) {
this.pickerHost = document.createElement('div');
this.domElement.appendChild(this.pickerHost);
const picker: React.ReactElement<IOrganizationChartPeoplePickerProps> =
React.createElement(OrganizationChartPeoplePicker, {
description: strings.StartUserFieldDescription,
label: strings.StartUserFieldLabel,
loadingText: strings.PeoplePickerLoadingText,
noResultsText: strings.PeoplePickerNoResultsText,
onChange: (loginName: string, accountName: string): void =>
this.onStartUserChanged(loginName, accountName),
placeholder: strings.StartUserPlaceholder,
removeButtonAriaLabel: strings.PeoplePickerRemoveLabel,
searchPeople: (query: string): Promise<IOrganizationChartPersonSuggestion[]> =>
this.service.searchPeople(query),
selectedAccountName: normalizeAccount(this.properties.startUserAccountName),
selectedDisplayName: selectedDisplayName,
selectedLoginName: normalizeAccount(this.properties.startUser),
suggestionsHeaderText: strings.PeoplePickerSuggestionsLabel,
validationErrorText: strings.PeoplePickerValidationError,
validatePerson: (loginName: string, accountName: string): Promise<boolean> =>
this.service.validatePerson(loginName, accountName, normalizeCacheMinutes(this.properties.cacheMinutes))
});
ReactDom.render(picker, this.pickerHost);
}
this.contentHost = document.createElement('div');
this.domElement.appendChild(this.contentHost);
return this.contentHost;
}
private clearSurface(): void {
if (this.pickerHost) { ReactDom.unmountComponentAtNode(this.pickerHost); }
if (this.contentHost) { ReactDom.unmountComponentAtNode(this.contentHost); }
this.pickerHost = undefined;
this.contentHost = undefined;
}
private onStartUserChanged(loginName: string, accountName: string): void {
const value: string = normalizeAccount(loginName);
const fallback: string = normalizeAccount(accountName);
if (value === normalizeAccount(this.properties.startUser) &&
fallback === normalizeAccount(this.properties.startUserAccountName)) { return; }
this.properties.startUser = value;
this.properties.startUserAccountName = fallback;
this.render();
}
}
@@ -4,6 +4,10 @@
color: "[theme:bodyText, default:#323130]";
font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.pickerPanel { background: "[theme:neutralLighterAlt, default:#faf9f8]"; border: 1px solid "[theme:neutralLight, default:#edebe9]"; margin-bottom: 16px; padding: 12px; }
.pickerLabel { color: "[theme:bodyText, default:#323130]"; display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
.pickerDescription { color: "[theme:neutralSecondary, default:#605e5c]"; font-size: 12px; line-height: 17px; margin-top: 6px; }
.pickerError { color: "[theme:errorText, default:#a80000]"; font-size: 12px; line-height: 17px; margin-top: 6px; }
.header { align-items: baseline; display: flex; justify-content: space-between; margin-bottom: 12px; }
.heading { font-size: 24px; font-weight: 300; line-height: 32px; margin: 0; }
.count { color: "[theme:neutralSecondary, default:#605e5c]"; font-size: 12px; }
@@ -0,0 +1,127 @@
import * as React from 'react';
import {
NormalPeoplePicker,
ValidationState
} from 'office-ui-fabric-react/lib/Pickers';
import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona';
import { IOrganizationChartPersonSuggestion } from '../OrganizationChartTypes';
import styles from './OrganizationChart.module.scss';
export interface IOrganizationChartPickerItem extends IPersonaProps {
accountName: string;
key: string;
loginName: string;
}
export interface IOrganizationChartPeoplePickerProps {
description: string;
label: string;
loadingText: string;
noResultsText: string;
onChange: (loginName: string, accountName: string) => void;
placeholder: string;
removeButtonAriaLabel: string;
searchPeople: (query: string) => Promise<IOrganizationChartPersonSuggestion[]>;
selectedDisplayName: string;
selectedAccountName: string;
selectedLoginName: string;
suggestionsHeaderText: string;
validationErrorText: string;
validatePerson: (loginName: string, accountName: string) => Promise<boolean>;
}
export interface IOrganizationChartPeoplePickerState {
validationError: string;
}
export class OrganizationChartPeoplePicker
extends React.Component<IOrganizationChartPeoplePickerProps, IOrganizationChartPeoplePickerState> {
public constructor(props: IOrganizationChartPeoplePickerProps) {
super(props);
this.resolveSuggestions = this.resolveSuggestions.bind(this);
this.onSelectionChanged = this.onSelectionChanged.bind(this);
this.onItemSelected = this.onItemSelected.bind(this);
this.getTextFromItem = this.getTextFromItem.bind(this);
this.state = { validationError: '' };
}
public render(): React.ReactElement<HTMLDivElement> {
const selectedItems: IOrganizationChartPickerItem[] = this.props.selectedLoginName ? [{
key: this.props.selectedLoginName,
accountName: this.props.selectedAccountName,
loginName: this.props.selectedLoginName,
primaryText: this.props.selectedDisplayName || this.props.selectedLoginName,
secondaryText: this.props.selectedLoginName
}] : [];
return (
<div className={styles.pickerPanel}>
<label className={styles.pickerLabel}>{this.props.label}</label>
<NormalPeoplePicker
selectedItems={selectedItems}
onResolveSuggestions={this.resolveSuggestions}
onChange={this.onSelectionChanged}
onItemSelected={this.onItemSelected}
getTextFromItem={this.getTextFromItem}
onValidateInput={(): ValidationState => ValidationState.invalid}
itemLimit={1}
inputProps={{
'aria-label': this.props.label,
placeholder: this.props.placeholder
}}
pickerSuggestionsProps={{
loadingText: this.props.loadingText,
noResultsFoundText: this.props.noResultsText,
suggestionsHeaderText: this.props.suggestionsHeaderText
}}
removeButtonAriaLabel={this.props.removeButtonAriaLabel}
/>
{this.state.validationError ?
<div className={styles.pickerError} role='alert'>{this.state.validationError}</div> : null}
<div className={styles.pickerDescription}>{this.props.description}</div>
</div>
);
}
private resolveSuggestions(filter: string): Promise<IOrganizationChartPickerItem[]> {
return this.props.searchPeople(filter).then(
(suggestions: IOrganizationChartPersonSuggestion[]): IOrganizationChartPickerItem[] =>
suggestions.map((suggestion: IOrganizationChartPersonSuggestion): IOrganizationChartPickerItem => ({
key: suggestion.loginName,
accountName: suggestion.accountName,
loginName: suggestion.loginName,
primaryText: suggestion.displayName,
secondaryText: suggestion.email || suggestion.accountName || suggestion.loginName
})),
(): IOrganizationChartPickerItem[] => []
);
}
private onSelectionChanged(items?: IOrganizationChartPickerItem[]): void {
this.setState({ validationError: '' });
this.props.onChange(
items && items.length ? items[0].loginName : '',
items && items.length ? items[0].accountName : ''
);
}
private onItemSelected(item?: IOrganizationChartPickerItem): Promise<IOrganizationChartPickerItem> {
if (!item) { return Promise.resolve(undefined as IOrganizationChartPickerItem); }
this.setState({ validationError: '' });
return this.props.validatePerson(item.loginName, item.accountName).then(
(valid: boolean): IOrganizationChartPickerItem => {
if (valid) { return item; }
this.setState({ validationError: this.props.validationErrorText });
return undefined as IOrganizationChartPickerItem;
},
(): IOrganizationChartPickerItem => {
this.setState({ validationError: this.props.validationErrorText });
return undefined as IOrganizationChartPickerItem;
}
);
}
private getTextFromItem(item: IOrganizationChartPickerItem): string {
return item.primaryText || item.loginName;
}
}
+7 -2
View File
@@ -5,8 +5,13 @@ define([], function() {
DisplayGroupName: 'Darstellung',
TitleFieldLabel: 'WebPart-Titel',
StartUserFieldLabel: 'Startperson / Manager',
StartUserFieldDescription: 'Leer lassen, um beim aktuellen Benutzer zu starten. Zulässig sind Claims-Login, DOMAIN\\Benutzer oder UPN.',
StartUserPlaceholder: 'DOMAIN\\Benutzer oder benutzer@contoso.local',
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',
PeoplePickerLoadingText: 'Personen werden gesucht...',
PeoplePickerNoResultsText: 'Keine passenden Personen gefunden.',
PeoplePickerRemoveLabel: 'Startperson entfernen',
PeoplePickerSuggestionsLabel: 'Vorgeschlagene Personen',
PeoplePickerValidationError: 'Für diese Person ist kein lesbares Profil im SharePoint User Profile Service vorhanden.',
MaxDepthFieldLabel: 'Maximale Ebenen nach unten',
MaxPeopleFieldLabel: 'Maximale Personenzahl',
CacheMinutesFieldLabel: 'Profilcache (Minuten, 0 = aus)',
+7 -2
View File
@@ -5,8 +5,13 @@ define([], function() {
DisplayGroupName: 'Display',
TitleFieldLabel: 'Web part title',
StartUserFieldLabel: 'Starting person / manager',
StartUserFieldDescription: 'Leave empty to start with the current user. Claims login, DOMAIN\\user or UPN are supported.',
StartUserPlaceholder: 'DOMAIN\\user or user@contoso.local',
StartUserFieldDescription: 'Search for and select a verified SharePoint user. Leave empty to start with the current user.',
StartUserPlaceholder: 'Enter a name or email address',
PeoplePickerLoadingText: 'Searching for people...',
PeoplePickerNoResultsText: 'No matching people found.',
PeoplePickerRemoveLabel: 'Remove starting person',
PeoplePickerSuggestionsLabel: 'Suggested people',
PeoplePickerValidationError: 'This person has no readable profile in the SharePoint User Profile Service.',
MaxDepthFieldLabel: 'Maximum levels below',
MaxPeopleFieldLabel: 'Maximum number of people',
CacheMinutesFieldLabel: 'Profile cache (minutes, 0 = off)',
+5
View File
@@ -6,6 +6,11 @@ declare interface IOrganizationChartWebPartStrings {
StartUserFieldLabel: string;
StartUserFieldDescription: string;
StartUserPlaceholder: string;
PeoplePickerLoadingText: string;
PeoplePickerNoResultsText: string;
PeoplePickerRemoveLabel: string;
PeoplePickerSuggestionsLabel: string;
PeoplePickerValidationError: string;
MaxDepthFieldLabel: string;
MaxPeopleFieldLabel: string;
CacheMinutesFieldLabel: string;
+8 -1
View File
@@ -14,6 +14,7 @@ var manifest = json('src/webparts/organizationChart/OrganizationChartWebPart.man
var service = read('src/webparts/organizationChart/OrganizationChartDataService.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');
var styles = read('src/webparts/organizationChart/components/OrganizationChart.module.scss');
assert(packageJson.name === 'organization-chart-webpart', 'Package name is inconsistent.');
@@ -31,7 +32,13 @@ 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("PropertyPaneTextField('startUser'") >= 0, 'Starting-person configuration 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(picker.indexOf('NormalPeoplePicker') >= 0 && picker.indexOf('ValidationState.invalid') >= 0,
'The on-canvas people picker must reject unverified free-text values.');
assert(picker.indexOf('validatePerson') >= 0 && service.indexOf('public validatePerson') >= 0,
'Selected people must be validated against the User Profile Service.');
assert(webPart.indexOf("PropertyPaneSlider('maxDepth'") >= 0 && webPart.indexOf("PropertyPaneSlider('maxPeople'") >= 0,
'Tree safety limits are missing.');
assert(component.indexOf("role='tree'") >= 0 && component.indexOf("role='treeitem'") >= 0,