Feature/vacation edo signing (#23)
Tests / frontend-test (push) Successful in 1m24s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/23
This commit was merged in pull request #23.
This commit is contained in:
2026-10-05 08:53:18 +00:00
parent f8ab2a1d71
commit 637e51d7f9
85 changed files with 2152 additions and 171 deletions
@@ -19,3 +19,39 @@
font-size: 13.5px;
color: $gray-700;
}
// Блок таблицы на всю ширину сетки формы (без FormField — его подпись дублировала бы заголовок колонки).
.tableWrap {
grid-column: 1 / -1;
}
// Таблица организаций: те же цвета, что у .line — «подставлено автоматически».
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
border: 1px solid $gray-200;
border-radius: $r-sm;
background: $gray-50;
font-family: $font-sans;
font-size: 13.5px;
color: $gray-700;
th,
td {
padding: 9px 13px;
text-align: left;
vertical-align: top;
}
th {
font-weight: 600;
font-size: 12px;
color: $gray-500;
border-bottom: 1px solid $gray-200;
}
tr + tr td {
border-top: 1px solid $gray-200;
}
}
@@ -0,0 +1,44 @@
import * as React from 'react';
import styles from './OrganizationField.module.scss';
import { parseOrganizationLine } from '../../dictionary/organizationLines';
interface IOrganizationTableProps {
/** `\n`-joined «Компания — должность — подразделение» на организацию (см. formatEmployeeOrganizationsWithDepartment). */
value: string;
/** Индекс строки, остающейся в печати (см. OrganizationField.printIndex); остальные — `rp-no-print`. */
printIndex?: number;
}
/**
* Организации сотрудника таблицей «Организация · Должность · Подразделение» —
* read-only, подтягивается из Employee/ByEmail. На каждую организацию своё
* подразделение (сотрудник может состоять в нескольких). Без внешней подписи: её роль
* играет заголовок колонки «Организация». Нет данных — ничего не рендерим.
*/
export const OrganizationTable: React.FC<IOrganizationTableProps> = ({ value, printIndex }) => {
const rows = value ? value.split('\n').map(parseOrganizationLine) : [];
if (rows.length === 0) return null;
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Организация</th>
<th>Должность</th>
<th>Подразделение</th>
</tr>
</thead>
<tbody>
{rows.map((r, i) => (
<tr key={i} className={printIndex !== undefined && i !== printIndex ? 'rp-no-print' : undefined}>
<td>{r.company}</td>
<td>{r.position || '—'}</td>
<td>{r.department || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
@@ -0,0 +1,27 @@
import { trimPickerQuery } from './pickerContext';
const body = (q: unknown): string => JSON.stringify({ queryParams: { QueryString: q, PrincipalType: 1 } });
const query = (b: unknown): unknown => (JSON.parse(b as string) as { queryParams: { QueryString: unknown } }).queryParams.QueryString;
describe('trimPickerQuery', () => {
it('убирает пробел в конце и в начале', () => {
expect(query(trimPickerQuery(body('Иван ')))).toBe('Иван');
expect(query(trimPickerQuery(body(' Иван Петров ')))).toBe('Иван Петров');
});
it('схлопывает двойные пробелы внутри', () => {
expect(query(trimPickerQuery(body('Иван Петров')))).toBe('Иван Петров');
});
it('не трогает остальные поля запроса', () => {
const parsed = JSON.parse(trimPickerQuery(body('a ')) as string) as { queryParams: { PrincipalType: number } };
expect(parsed.queryParams.PrincipalType).toBe(1);
});
it('чужие тела возвращает как есть', () => {
expect(trimPickerQuery('не json QueryString')).toBe('не json QueryString');
expect(trimPickerQuery(undefined)).toBeUndefined();
const other = JSON.stringify({ a: 1 });
expect(trimPickerQuery(other)).toBe(other);
});
});
@@ -18,6 +18,43 @@ export function pickedLogin(user: IPickedUser): string {
return user.secondaryText || user.loginName || user.id || '';
}
/**
* Убирает пробелы по краям (и схлопывает внутренние) у QueryString в теле запроса
* поиска сотрудника. PeoplePicker из @pnp/spfx-controls-react шлёт введённый
* текст как есть, а SharePoint с «Иван » (пробел в конце, обычное дело при вводе
* «имя фамилия») ничего не находит. Тело не JSON или без QueryString — возвращается как было.
*/
export function trimPickerQuery(body: unknown): unknown {
if (typeof body !== 'string' || body.indexOf('QueryString') < 0) return body;
try {
const parsed = JSON.parse(body) as { queryParams?: { QueryString?: unknown } };
const query = parsed.queryParams?.QueryString;
if (typeof query !== 'string') return body;
parsed.queryParams!.QueryString = query.trim().replace(/\s+/g, ' ');
return JSON.stringify(parsed);
} catch {
return body;
}
}
/**
* spHttpClient с подчисткой запроса поиска (см. trimPickerQuery): прокси, чтобы
* остальные методы клиента работали как прежде, подменяется только post.
*/
function withTrimmedSearch(client: WebPartContext['spHttpClient']): WebPartContext['spHttpClient'] {
return new Proxy(client, {
get(target, prop, receiver) {
if (prop !== 'post') return Reflect.get(target, prop, receiver);
return (url: string, config: unknown, options?: { body?: unknown }) =>
(target.post as (u: string, c: unknown, o?: unknown) => unknown)(
url,
config,
options ? { ...options, body: trimPickerQuery(options.body) } : options
);
}
});
}
/**
* Контекст для PeoplePicker из WebPartContext.
* as unknown: @pnp/spfx-controls-react тянет свою копию sp-http-msgraph,
@@ -27,6 +64,6 @@ export function buildPickerContext(context: WebPartContext): IPeoplePickerContex
return {
absoluteUrl: context.pageContext.web.absoluteUrl,
msGraphClientFactory: context.msGraphClientFactory,
spHttpClient: context.spHttpClient
spHttpClient: withTrimmedSearch(context.spHttpClient)
} as unknown as IPeoplePickerContext;
}
@@ -85,7 +85,7 @@ export const SubmitActions: React.FC<ISubmitActionsProps> = ({
onDelete,
}) => {
const { saving, kind } = submit;
const unfrozenForm = formId != null && UNFROZEN_FORMS.has(formId);
const unfrozenForm = formId !== undefined && UNFROZEN_FORMS.has(formId);
// false, пока не пришёл ответ о членстве в группе, — безопасный дефолт: не
// мигать разблокированными кнопками до подтверждения, что это тестировщик.
const [isTester, setIsTester] = React.useState<boolean>(false);
@@ -47,6 +47,7 @@ export { PeopleField } from './PeopleField/PeopleField';
export { PeopleFieldMulti } from './PeopleField/PeopleFieldMulti';
export { PeopleTextField } from './PeopleField/PeopleTextField';
export { OrganizationField } from './OrganizationField/OrganizationField';
export { OrganizationTable } from './OrganizationField/OrganizationTable';
export { PrintSplitButton } from './PrintSplitButton/PrintSplitButton';
export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead';
export { LinkField } from './LinkField/LinkField';