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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user