From a686f8c055f17cf2b528ca6ceec6dcb644cc9c08 Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Mon, 24 Aug 2026 14:28:01 +0200 Subject: [PATCH] feat: Implement SharePoint People Picker for selecting starting person and update version to 1.0.3 --- README.md | 10 +- config/package-solution.json | 2 +- package.json | 3 +- .../OrganizationChartDataService.ts | 84 ++++++++---- .../OrganizationChartTypes.ts | 7 + .../OrganizationChartWebPart.manifest.json | 1 + .../OrganizationChartWebPart.ts | 77 +++++++++-- .../components/OrganizationChart.module.scss | 4 + .../OrganizationChartPeoplePicker.tsx | 127 ++++++++++++++++++ src/webparts/organizationChart/loc/de-de.js | 9 +- src/webparts/organizationChart/loc/en-us.js | 9 +- .../organizationChart/loc/myStrings.d.ts | 5 + tests/validate-project.js | 9 +- 13 files changed, 299 insertions(+), 48 deletions(-) create mode 100644 src/webparts/organizationChart/components/OrganizationChartPeoplePicker.tsx diff --git a/README.md b/README.md index a531417..01e5abc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config/package-solution.json b/config/package-solution.json index fb51557..2162b5e 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.2.0", + "version": "1.0.3.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index e3aa960..bec37b0 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/webparts/organizationChart/OrganizationChartDataService.ts b/src/webparts/organizationChart/OrganizationChartDataService.ts index d8762ab..2a35549 100644 --- a/src/webparts/organizationChart/OrganizationChartDataService.ts +++ b/src/webparts/organizationChart/OrganizationChartDataService.ts @@ -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 { const account: string = normalizeAccount(accountName); const state: ITreeLoadState = { @@ -60,7 +63,7 @@ export class OrganizationChartDataService { visited: {} }; const rootRequest: Promise = account - ? this.getConfiguredProfile(account, cacheMinutes) + ? this.getConfiguredProfile(account, cacheMinutes, alternateAccountName) : this.getMyProfile(cacheMinutes); return rootRequest .then((profile: IUserProfile): Promise => @@ -94,11 +97,39 @@ export class OrganizationChartDataService { return this.requestProfile(url, key, cacheMinutes); } - private getConfiguredProfile(accountName: string, cacheMinutes: number): Promise { + public searchPeople(query: string): Promise { + 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 { + 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 { return this.resolveAccountNames(accountName) .catch((): string[] => [accountName]) - .then((accounts: string[]): Promise => - this.getFirstAvailableProfile(accounts, cacheMinutes, 0)); + .then((accounts: string[]): Promise => { + 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 { + 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 { 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 => // 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 []; } }); } diff --git a/src/webparts/organizationChart/OrganizationChartTypes.ts b/src/webparts/organizationChart/OrganizationChartTypes.ts index 25ac6f5..1f70068 100644 --- a/src/webparts/organizationChart/OrganizationChartTypes.ts +++ b/src/webparts/organizationChart/OrganizationChartTypes.ts @@ -30,3 +30,10 @@ export interface IOrganizationChartData { peopleCount: number; truncated: boolean; } + +export interface IOrganizationChartPersonSuggestion { + accountName: string; + displayName: string; + email: string; + loginName: string; +} diff --git a/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json b/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json index 78c88ed..c1eb385 100644 --- a/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json +++ b/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json @@ -18,6 +18,7 @@ "properties": { "title": "Organization Chart", "startUser": "", + "startUserAccountName": "", "maxDepth": 6, "maxPeople": 100, "cacheMinutes": 15, diff --git a/src/webparts/organizationChart/OrganizationChartWebPart.ts b/src/webparts/organizationChart/OrganizationChartWebPart.ts index f617f04..b1da5cc 100644 --- a/src/webparts/organizationChart/OrganizationChartWebPart.ts +++ b/src/webparts/organizationChart/OrganizationChartWebPart.ts @@ -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 { 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 { 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 = + 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 => + 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 => + 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(); } } diff --git a/src/webparts/organizationChart/components/OrganizationChart.module.scss b/src/webparts/organizationChart/components/OrganizationChart.module.scss index 34ae683..77f81f3 100644 --- a/src/webparts/organizationChart/components/OrganizationChart.module.scss +++ b/src/webparts/organizationChart/components/OrganizationChart.module.scss @@ -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; } diff --git a/src/webparts/organizationChart/components/OrganizationChartPeoplePicker.tsx b/src/webparts/organizationChart/components/OrganizationChartPeoplePicker.tsx new file mode 100644 index 0000000..7c1ee40 --- /dev/null +++ b/src/webparts/organizationChart/components/OrganizationChartPeoplePicker.tsx @@ -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; + selectedDisplayName: string; + selectedAccountName: string; + selectedLoginName: string; + suggestionsHeaderText: string; + validationErrorText: string; + validatePerson: (loginName: string, accountName: string) => Promise; +} + +export interface IOrganizationChartPeoplePickerState { + validationError: string; +} + +export class OrganizationChartPeoplePicker + extends React.Component { + + 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 { + 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 ( +
+ + 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 ? +
{this.state.validationError}
: null} +
{this.props.description}
+
+ ); + } + + private resolveSuggestions(filter: string): Promise { + 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 { + 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; + } +} diff --git a/src/webparts/organizationChart/loc/de-de.js b/src/webparts/organizationChart/loc/de-de.js index 5bacb62..6a81dc0 100644 --- a/src/webparts/organizationChart/loc/de-de.js +++ b/src/webparts/organizationChart/loc/de-de.js @@ -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)', diff --git a/src/webparts/organizationChart/loc/en-us.js b/src/webparts/organizationChart/loc/en-us.js index 9471890..e5307bb 100644 --- a/src/webparts/organizationChart/loc/en-us.js +++ b/src/webparts/organizationChart/loc/en-us.js @@ -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)', diff --git a/src/webparts/organizationChart/loc/myStrings.d.ts b/src/webparts/organizationChart/loc/myStrings.d.ts index 664855a..8cc15d9 100644 --- a/src/webparts/organizationChart/loc/myStrings.d.ts +++ b/src/webparts/organizationChart/loc/myStrings.d.ts @@ -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; diff --git a/tests/validate-project.js b/tests/validate-project.js index 62bd099..4583e08 100644 --- a/tests/validate-project.js +++ b/tests/validate-project.js @@ -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,