import * as React from 'react'; import * as ReactDom from 'react-dom'; import { Version } from '@microsoft/sp-core-library'; import { type IPropertyPaneConfiguration, PropertyPaneDropdown } from '@microsoft/sp-property-pane'; import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; import { IReadonlyTheme } from '@microsoft/sp-component-base'; import * as strings from 'RequestPortalWebPartStrings'; import RequestPortal from './components/RequestPortal'; import { IRequestPortalProps } from './components/IRequestPortalProps'; import { initSP } from './shared/pnp/pnpjs'; import { installErrorLogging } from './shared/diagnostics'; import { setImpersonatedIdentity } from './shared/impersonation'; import { APPROVAL_API_BASE_URL_LOCAL, APPROVAL_API_BASE_URL_PROD, setApprovalApiBaseUrl } from './shared/config'; import { initListPartitions } from './shared/approval/listPartitions'; import { ErrorOverlay } from './shared/ui'; import { PropertyPaneDebugUser } from './propertyPane/PropertyPaneDebugUser'; export interface IRequestPortalWebPartProps { /** * Отладочная подмена личности — см. shared/impersonation.ts. На бою пусто. * Одно поле на оба случая: e-mail, введённый напрямую, хранится как есть; * ФИО, найденное поиском по имени (см. propertyPane/DebugUserField), после * резолва заменяется собой же на найденный e-mail — отдельное поле под * «сырой» ввод не нужно, DebugUserField ведёт его в своём локальном состоянии. */ debugUserQuery: string; debugUserDisplayName: string; /** Куда шлём запросы approval-route (маршруты согласования) — прод или локальный backend. */ approvalApiBaseUrl: string; } export default class RequestPortalWebPart extends BaseClientSideWebPart { private _isDarkTheme: boolean = false; private _environmentMessage: string = ''; public render(): void { // Именно здесь, а не в onInit: свойство меняется в панели без перезапуска // веб-части, и render вызывается на каждое такое изменение. setImpersonatedIdentity(this.properties.debugUserQuery, this.properties.debugUserDisplayName); this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD; setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl); const element: React.ReactElement = React.createElement( ErrorOverlay, undefined, React.createElement(RequestPortal, { // key на подменённом e-mail: смена подмены в property pane должна // пересобрать раздел заново — иначе счётчики в боковом меню и открытые // таблицы остались бы от прежнего пользователя (их эффекты завязаны на // монтирование/context, а не на identity подмены). key: this.properties.debugUserQuery || '__self__', isDarkTheme: this._isDarkTheme, environmentMessage: this._environmentMessage, hasTeamsContext: !!this.context.sdks.microsoftTeams, userDisplayName: this.context.pageContext.user.displayName, context: this.context }) ); ReactDom.render(element, this.domElement); } protected onInit(): Promise { installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения initSP(this.context); // единый клиент PnPjs для записи в списки // Адрес approval-route должен быть выставлен ДО загрузки партиций — они // берутся именно оттуда. В render() он ставится снова (свойство меняется в // панели без перезапуска), но render случается позже onInit. this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD; setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl); // Партиции грузим здесь, а не по требованию из форм: имена списков нужны // формам синхронно (см. shared/listTitles.ts), а формы — лениво подгружаемые // чанки, монтируются заведомо позже onInit. initListPartitions не бросает: // при недоступном approval-route портал работает по конвенции имён. return Promise.all([this._getEnvironmentMessage(), initListPartitions(this.context)]).then(([message]) => { this._environmentMessage = message; }); } private _getEnvironmentMessage(): Promise { if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook return this.context.sdks.microsoftTeams.teamsJs.app.getContext() .then(context => { let environmentMessage: string; switch (context.app.host.name) { case 'Office': // running in Office environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment; break; case 'Outlook': // running in Outlook environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment; break; case 'Teams': // running in Teams case 'TeamsModern': environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment; break; default: environmentMessage = strings.UnknownEnvironment; } return environmentMessage; }); } return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment); } protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { if (!currentTheme) { return; } this._isDarkTheme = !!currentTheme.isInverted; const { semanticColors } = currentTheme; if (semanticColors) { this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null); this.domElement.style.setProperty('--link', semanticColors.link || null); this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null); } } protected onDispose(): void { ReactDom.unmountComponentAtNode(this.domElement); } protected get dataVersion(): Version { return Version.parse('1.0'); } protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.BasicGroupName, groupFields: [ PropertyPaneDebugUser('debugUserQuery', { label: strings.DebugUserFieldLabel, description: strings.DebugUserFieldDescription, context: this.context, initialQuery: this.properties.debugUserQuery, onResolved: (query, email, displayName) => { // Сохраняем ТОЛЬКО e-mail (введённый напрямую или найденный по // ФИО) — свойство одно и то же используется как e-mail для // подмены (см. render() выше), так что писать в него сырое ФИО // нельзя. Пустой query — поле очистили, подмену выключаем. // А вот «email пуст, но query не пуст» — ФИО ещё не нашлось // (недописано/не существует): ничего не трогаем, иначе неудачный // промежуточный поиск молча выключил бы уже работающую подмену. if (email) { this.properties.debugUserQuery = email; this.properties.debugUserDisplayName = displayName; this.render(); } else if (!query) { this.properties.debugUserQuery = ''; this.properties.debugUserDisplayName = ''; this.render(); } } }) ] }, { groupName: 'Approval Route API', groupFields: [ PropertyPaneDropdown('approvalApiBaseUrl', { label: 'Api Base Url', selectedKey: this.properties.approvalApiBaseUrl, options: [ { key: APPROVAL_API_BASE_URL_PROD, text: `Прод (${APPROVAL_API_BASE_URL_PROD})` }, { key: APPROVAL_API_BASE_URL_LOCAL, text: `Локально (${APPROVAL_API_BASE_URL_LOCAL})` } ] }) ] } ] } ] }; } }