пофикшен вебпарт
This commit is contained in:
@@ -22,8 +22,6 @@
|
|||||||
"title": { "default": "request-portal" },
|
"title": { "default": "request-portal" },
|
||||||
"description": { "default": "request-portal description" },
|
"description": { "default": "request-portal description" },
|
||||||
"officeFabricIconFontName": "Page",
|
"officeFabricIconFontName": "Page",
|
||||||
"properties": {
|
"properties": {}
|
||||||
"description": "request-portal"
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDom from 'react-dom';
|
import * as ReactDom from 'react-dom';
|
||||||
import { Version } from '@microsoft/sp-core-library';
|
import { Version } from '@microsoft/sp-core-library';
|
||||||
import {
|
import { type IPropertyPaneConfiguration } from '@microsoft/sp-property-pane';
|
||||||
type IPropertyPaneConfiguration,
|
|
||||||
PropertyPaneTextField
|
|
||||||
} from '@microsoft/sp-property-pane';
|
|
||||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||||
|
|
||||||
@@ -13,13 +10,15 @@ import RequestPortal from './components/RequestPortal';
|
|||||||
import { IRequestPortalProps } from './components/IRequestPortalProps';
|
import { IRequestPortalProps } from './components/IRequestPortalProps';
|
||||||
import { initSP } from './shared/pnp/pnpjs';
|
import { initSP } from './shared/pnp/pnpjs';
|
||||||
import { installErrorLogging } from './shared/diagnostics';
|
import { installErrorLogging } from './shared/diagnostics';
|
||||||
import { setImpersonatedEmail } from './shared/impersonation';
|
import { setImpersonatedIdentity } from './shared/impersonation';
|
||||||
import { ErrorOverlay } from './shared/ui';
|
import { ErrorOverlay } from './shared/ui';
|
||||||
|
import { PropertyPaneDebugUser } from './propertyPane/PropertyPaneDebugUser';
|
||||||
|
|
||||||
export interface IRequestPortalWebPartProps {
|
export interface IRequestPortalWebPartProps {
|
||||||
description: string;
|
|
||||||
/** Отладочная подмена личности — см. shared/impersonation.ts. На бою пусто. */
|
/** Отладочная подмена личности — см. shared/impersonation.ts. На бою пусто. */
|
||||||
|
debugUserQuery: string;
|
||||||
debugUserEmail: string;
|
debugUserEmail: string;
|
||||||
|
debugUserDisplayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
|
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
|
||||||
@@ -30,13 +29,12 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
public render(): void {
|
public render(): void {
|
||||||
// Именно здесь, а не в onInit: свойство меняется в панели без перезапуска
|
// Именно здесь, а не в onInit: свойство меняется в панели без перезапуска
|
||||||
// веб-части, и render вызывается на каждое такое изменение.
|
// веб-части, и render вызывается на каждое такое изменение.
|
||||||
setImpersonatedEmail(this.properties.debugUserEmail);
|
setImpersonatedIdentity(this.properties.debugUserEmail, this.properties.debugUserDisplayName);
|
||||||
|
|
||||||
const element: React.ReactElement = React.createElement(
|
const element: React.ReactElement = React.createElement(
|
||||||
ErrorOverlay,
|
ErrorOverlay,
|
||||||
undefined,
|
undefined,
|
||||||
React.createElement<IRequestPortalProps>(RequestPortal, {
|
React.createElement<IRequestPortalProps>(RequestPortal, {
|
||||||
description: this.properties.description,
|
|
||||||
isDarkTheme: this._isDarkTheme,
|
isDarkTheme: this._isDarkTheme,
|
||||||
environmentMessage: this._environmentMessage,
|
environmentMessage: this._environmentMessage,
|
||||||
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
||||||
@@ -122,13 +120,17 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
{
|
{
|
||||||
groupName: strings.BasicGroupName,
|
groupName: strings.BasicGroupName,
|
||||||
groupFields: [
|
groupFields: [
|
||||||
PropertyPaneTextField('description', {
|
PropertyPaneDebugUser('debugUserQuery', {
|
||||||
label: strings.DescriptionFieldLabel
|
label: strings.DebugUserFieldLabel,
|
||||||
}),
|
description: strings.DebugUserFieldDescription,
|
||||||
PropertyPaneTextField('debugUserEmail', {
|
context: this.context,
|
||||||
label: strings.DebugUserEmailFieldLabel,
|
initialQuery: this.properties.debugUserQuery,
|
||||||
description: strings.DebugUserEmailFieldDescription,
|
onResolved: (query, email, displayName) => {
|
||||||
placeholder: 'employee@tnsu.kz'
|
this.properties.debugUserQuery = query;
|
||||||
|
this.properties.debugUserEmail = email;
|
||||||
|
this.properties.debugUserDisplayName = displayName;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
export interface IRequestPortalProps {
|
export interface IRequestPortalProps {
|
||||||
description: string;
|
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
environmentMessage: string;
|
environmentMessage: string;
|
||||||
hasTeamsContext: boolean;
|
hasTeamsContext: boolean;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
OwnTab,
|
OwnTab,
|
||||||
} from '../shared/routing';
|
} from '../shared/routing';
|
||||||
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
||||||
import { getImpersonatedEmail } from '../shared/impersonation';
|
import { getImpersonatedEmail, getImpersonatedDisplayName } from '../shared/impersonation';
|
||||||
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
||||||
import { Drawer } from './Drawer/Drawer';
|
import { Drawer } from './Drawer/Drawer';
|
||||||
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
||||||
@@ -62,6 +62,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
// (setImpersonatedEmail в render) до создания этого элемента, и меняется оно
|
// (setImpersonatedEmail в render) до создания этого элемента, и меняется оно
|
||||||
// только вместе с новым render всей веб-части.
|
// только вместе с новым render всей веб-части.
|
||||||
const impersonatedEmail = getImpersonatedEmail();
|
const impersonatedEmail = getImpersonatedEmail();
|
||||||
|
const impersonatedDisplayName = getImpersonatedDisplayName();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
||||||
@@ -276,8 +277,11 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
закрыть: она исчезает только когда свойство веб-части очищено. */}
|
закрыть: она исчезает только когда свойство веб-части очищено. */}
|
||||||
{impersonatedEmail && (
|
{impersonatedEmail && (
|
||||||
<div className={styles.impersonationBar}>
|
<div className={styles.impersonationBar}>
|
||||||
Включена отладочная подмена: портал работает от имени <strong>{impersonatedEmail}</strong>.
|
Включена отладочная подмена: портал работает от имени{' '}
|
||||||
Очистите поле в свойствах веб-части, чтобы вернуться к своей учётной записи.
|
<strong>
|
||||||
|
{impersonatedDisplayName ? `${impersonatedDisplayName} (${impersonatedEmail})` : impersonatedEmail}
|
||||||
|
</strong>
|
||||||
|
. Очистите поле в свойствах веб-части, чтобы вернуться к своей учётной записи.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ define([], function() {
|
|||||||
return {
|
return {
|
||||||
"PropertyPaneDescription": "Description",
|
"PropertyPaneDescription": "Description",
|
||||||
"BasicGroupName": "Group Name",
|
"BasicGroupName": "Group Name",
|
||||||
"DescriptionFieldLabel": "Description Field",
|
"DebugUserFieldLabel": "Отладка: работать от имени сотрудника",
|
||||||
"DebugUserEmailFieldLabel": "Отладка: работать от имени сотрудника",
|
"DebugUserFieldDescription": "Только для отладки маршрутов. Введите фамилию и имя сотрудника (можно в обратном порядке, отчество не обязательно) — портал найдёт его e-mail сам, найденное отобразится над полем. Можно ввести e-mail напрямую. По этому e-mail планируется маршрут, отбираются «свои» заявки и очередь согласования. Прав это не даёт — запись в списки всё равно идёт под вашей учёткой. На боевой странице оставьте пустым.",
|
||||||
"DebugUserEmailFieldDescription": "Только для отладки маршрутов. Портал будет считать текущим пользователем указанный e-mail: по нему планируется маршрут, отбираются «свои» заявки и очередь согласования. Прав это не даёт — запись в списки всё равно идёт под вашей учёткой. На боевой странице оставьте пустым.",
|
|
||||||
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
|
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
|
||||||
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
|
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
|
||||||
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
|
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
|
||||||
|
|||||||
+2
-3
@@ -1,9 +1,8 @@
|
|||||||
declare interface IRequestPortalWebPartStrings {
|
declare interface IRequestPortalWebPartStrings {
|
||||||
PropertyPaneDescription: string;
|
PropertyPaneDescription: string;
|
||||||
BasicGroupName: string;
|
BasicGroupName: string;
|
||||||
DescriptionFieldLabel: string;
|
DebugUserFieldLabel: string;
|
||||||
DebugUserEmailFieldLabel: string;
|
DebugUserFieldDescription: string;
|
||||||
DebugUserEmailFieldDescription: string;
|
|
||||||
AppLocalEnvironmentSharePoint: string;
|
AppLocalEnvironmentSharePoint: string;
|
||||||
AppLocalEnvironmentTeams: string;
|
AppLocalEnvironmentTeams: string;
|
||||||
AppLocalEnvironmentOffice: string;
|
AppLocalEnvironmentOffice: string;
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { resolveDebugUser } from '../shared/impersonation';
|
||||||
|
|
||||||
|
export interface IDebugUserFieldProps {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
context: WebPartContext;
|
||||||
|
initialQuery: string;
|
||||||
|
/** Зовётся при каждом изменении: query — как введено, email/displayName — итог поиска ('' если не найдено). */
|
||||||
|
onResolved: (query: string, email: string, displayName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status = 'idle' | 'loading' | 'found' | 'notfound' | 'email';
|
||||||
|
|
||||||
|
const DEBOUNCE_MS = 400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поле свойств веб-части: ввод «Фамилия Имя [Отчество]» (в любом порядке) или
|
||||||
|
* e-mail напрямую. Сам резолвит e-mail через SharePoint people search — см.
|
||||||
|
* shared/impersonation.ts (resolveDebugUser) — и показывает найденное тут же,
|
||||||
|
* чтобы не гадать, кого именно подставили.
|
||||||
|
*/
|
||||||
|
export const DebugUserField: React.FC<IDebugUserFieldProps> = ({ label, description, context, initialQuery, onResolved }) => {
|
||||||
|
const [query, setQuery] = React.useState(initialQuery || '');
|
||||||
|
const [status, setStatus] = React.useState<Status>('idle');
|
||||||
|
const [found, setFound] = React.useState<{ email: string; displayName: string } | undefined>(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const text = query.trim();
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
setStatus('idle');
|
||||||
|
setFound(undefined);
|
||||||
|
onResolved('', '', '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-mail — используем как есть, без похода в people search.
|
||||||
|
if (text.indexOf('@') >= 0) {
|
||||||
|
setStatus('email');
|
||||||
|
setFound(undefined);
|
||||||
|
onResolved(text, text, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setStatus('loading');
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
resolveDebugUser(context, text)
|
||||||
|
.then((hit) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (hit) {
|
||||||
|
setStatus('found');
|
||||||
|
setFound(hit);
|
||||||
|
onResolved(text, hit.email, hit.displayName);
|
||||||
|
} else {
|
||||||
|
setStatus('notfound');
|
||||||
|
setFound(undefined);
|
||||||
|
onResolved(text, '', '');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setStatus('notfound');
|
||||||
|
setFound(undefined);
|
||||||
|
onResolved(text, '', '');
|
||||||
|
});
|
||||||
|
}, DEBOUNCE_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 4 }}>{label}</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Иванов Иван Иванович, Иван Иванов или e-mail"
|
||||||
|
style={{ width: '100%', boxSizing: 'border-box', padding: '5px 8px', fontSize: 14 }}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 4, fontSize: 12, lineHeight: 1.4, color: '#a4262c', minHeight: 16 }}>
|
||||||
|
{status === 'loading' && 'Ищем сотрудника…'}
|
||||||
|
{status === 'found' && found && `Найдено: ${found.displayName} — ${found.email}`}
|
||||||
|
{status === 'notfound' && 'Сотрудник не найден — подмена работать не будет'}
|
||||||
|
{status === 'email' && `Используется как e-mail напрямую: ${query.trim()}`}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 6, fontSize: 12, lineHeight: 1.4, color: '#605e5c' }}>{description}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDom from 'react-dom';
|
||||||
|
import {
|
||||||
|
IPropertyPaneField,
|
||||||
|
PropertyPaneFieldType,
|
||||||
|
IPropertyPaneCustomFieldProps
|
||||||
|
} from '@microsoft/sp-property-pane';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { DebugUserField } from './DebugUserField';
|
||||||
|
|
||||||
|
export interface IPropertyPaneDebugUserProps {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
context: WebPartContext;
|
||||||
|
initialQuery: string;
|
||||||
|
onResolved: (query: string, email: string, displayName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PropertyPaneDebugUserBuilder
|
||||||
|
implements IPropertyPaneField<IPropertyPaneCustomFieldProps & IPropertyPaneDebugUserProps> {
|
||||||
|
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||||
|
public targetProperty: string;
|
||||||
|
public properties: IPropertyPaneCustomFieldProps & IPropertyPaneDebugUserProps;
|
||||||
|
|
||||||
|
constructor(targetProperty: string, properties: IPropertyPaneDebugUserProps) {
|
||||||
|
this.targetProperty = targetProperty;
|
||||||
|
this.properties = {
|
||||||
|
key: targetProperty,
|
||||||
|
onRender: this.onRender.bind(this),
|
||||||
|
onDispose: this.onDispose.bind(this),
|
||||||
|
...properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRender(elem: HTMLElement): void {
|
||||||
|
ReactDom.render(
|
||||||
|
<DebugUserField
|
||||||
|
label={this.properties.label}
|
||||||
|
description={this.properties.description}
|
||||||
|
context={this.properties.context}
|
||||||
|
initialQuery={this.properties.initialQuery}
|
||||||
|
onResolved={this.properties.onResolved}
|
||||||
|
/>,
|
||||||
|
elem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDispose(elem: HTMLElement): void {
|
||||||
|
ReactDom.unmountComponentAtNode(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Кастомное поле property pane: ФИО/e-mail сотрудника с резолвом в e-mail на лету. */
|
||||||
|
export function PropertyPaneDebugUser(
|
||||||
|
targetProperty: string,
|
||||||
|
properties: IPropertyPaneDebugUserProps
|
||||||
|
): IPropertyPaneField<IPropertyPaneCustomFieldProps & IPropertyPaneDebugUserProps> {
|
||||||
|
return new PropertyPaneDebugUserBuilder(targetProperty, properties);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { SPHttpClient } from '@microsoft/sp-http';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ОТЛАДОЧНАЯ подмена «текущего пользователя»: портал ведёт себя так, будто им
|
* ОТЛАДОЧНАЯ подмена «текущего пользователя»: портал ведёт себя так, будто им
|
||||||
@@ -22,10 +23,12 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
let impersonatedEmail = '';
|
let impersonatedEmail = '';
|
||||||
|
let impersonatedDisplayName = '';
|
||||||
|
|
||||||
/** Вызывается веб-частью на каждый render — свойство могли поменять в панели. */
|
/** Вызывается веб-частью на каждый render — свойство могли поменять в панели. */
|
||||||
export function setImpersonatedEmail(email: string | undefined): void {
|
export function setImpersonatedIdentity(email: string | undefined, displayName?: string): void {
|
||||||
impersonatedEmail = (email || '').trim();
|
impersonatedEmail = (email || '').trim();
|
||||||
|
impersonatedDisplayName = (displayName || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заданная подмена ('' — подмены нет). Для предупреждающей полосы в интерфейсе. */
|
/** Заданная подмена ('' — подмены нет). Для предупреждающей полосы в интерфейсе. */
|
||||||
@@ -33,7 +36,89 @@ export function getImpersonatedEmail(): string {
|
|||||||
return impersonatedEmail;
|
return impersonatedEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ФИО подменённого сотрудника, если его удалось найти поиском по имени ('' — для e-mail, введённого напрямую). */
|
||||||
|
export function getImpersonatedDisplayName(): string {
|
||||||
|
return impersonatedDisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
/** Email, под которым портал работает: подменённый, если задан, иначе — из токена. */
|
/** Email, под которым портал работает: подменённый, если задан, иначе — из токена. */
|
||||||
export function effectiveUserEmail(context: WebPartContext): string {
|
export function effectiveUserEmail(context: WebPartContext): string {
|
||||||
return impersonatedEmail || context.pageContext.user.email || '';
|
return impersonatedEmail || context.pageContext.user.email || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IResolvedDebugUser {
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPeopleSearchHit {
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сырой ответ ClientPeoplePickerSearchUser — value внутри двойного JSON. */
|
||||||
|
interface IPeoplePickerEntity {
|
||||||
|
DisplayText?: string;
|
||||||
|
Description?: string;
|
||||||
|
EntityData?: { Email?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchPeopleByText(context: WebPartContext, queryText: string): Promise<IPeopleSearchHit[]> {
|
||||||
|
const url = `${context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
queryParams: {
|
||||||
|
AllowEmailAddresses: true,
|
||||||
|
AllowMultipleEntities: false,
|
||||||
|
MaximumEntitySuggestions: 5,
|
||||||
|
PrincipalSource: 15, // All
|
||||||
|
PrincipalType: 1, // User
|
||||||
|
QueryString: queryText
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json;odata=nometadata',
|
||||||
|
'Content-Type': 'application/json;odata=nometadata'
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return [];
|
||||||
|
|
||||||
|
const json: { value?: string } = await response.json();
|
||||||
|
if (!json.value) return [];
|
||||||
|
|
||||||
|
let entries: IPeoplePickerEntity[];
|
||||||
|
try {
|
||||||
|
entries = JSON.parse(json.value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.map((e) => ({ displayName: e.DisplayText || '', email: e.EntityData?.Email || e.Description || '' }))
|
||||||
|
.filter((e) => !!e.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ищет сотрудника по имени для отладочной подмены (см. property pane —
|
||||||
|
* PropertyPaneDebugUser/DebugUserField). Порядок «Фамилия Имя» или «Имя
|
||||||
|
* Фамилия» не важен: если прямой запрос ничего не находит, пробуем с
|
||||||
|
* переставленными первыми двумя словами.
|
||||||
|
*/
|
||||||
|
export async function resolveDebugUser(context: WebPartContext, nameQuery: string): Promise<IResolvedDebugUser | undefined> {
|
||||||
|
const text = nameQuery.trim();
|
||||||
|
if (!text) return undefined;
|
||||||
|
|
||||||
|
let hits = await searchPeopleByText(context, text);
|
||||||
|
if (hits.length === 0) {
|
||||||
|
const tokens = text.split(/\s+/).filter(Boolean);
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
const swapped = [tokens[1], tokens[0], ...tokens.slice(2)].join(' ');
|
||||||
|
hits = await searchPeopleByText(context, swapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hits[0];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user