Разные фиксы (#8)
Tests / frontend-test (push) Successful in 1m20s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/8
This commit was merged in pull request #8.
This commit is contained in:
2026-08-18 08:45:42 +00:00
parent 5b2c062e81
commit 3a71d0df1f
22 changed files with 497 additions and 67 deletions
@@ -0,0 +1,104 @@
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 введён напрямую — используем его как есть, но всё равно ищем ФИО
// (для красной полосы в интерфейсе и для проверки, что такой сотрудник есть).
const isEmail = text.indexOf('@') >= 0;
let cancelled = false;
setStatus(isEmail ? 'email' : 'loading');
if (isEmail) setFound(undefined);
const timer = window.setTimeout(() => {
resolveDebugUser(context, text)
.then((hit) => {
if (cancelled) return;
if (isEmail) {
setFound(hit);
onResolved(text, text, hit ? hit.displayName : '');
} else if (hit) {
setStatus('found');
setFound(hit);
onResolved(text, hit.email, hit.displayName);
} else {
setStatus('notfound');
setFound(undefined);
onResolved(text, '', '');
}
})
.catch(() => {
if (cancelled) return;
if (isEmail) {
onResolved(text, text, '');
} else {
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' &&
(found
? `Используется как e-mail напрямую: ${query.trim()} (${found.displayName})`
: `Используется как 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);
}