fix: внес изменения в заявках рекрутинга от 16.09.26
Tests / frontend-test (push) Successful in 1m20s
Tests / frontend-test (push) Successful in 1m20s
This commit is contained in:
@@ -40,6 +40,7 @@ export async function getPlannedMatrix(
|
||||
askOverdueReason: s.askOverdueReason,
|
||||
autoApprove: s.autoApprove,
|
||||
warning: s.warning,
|
||||
positionCode: s.positionCode,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,4 +117,12 @@ export interface IResolvedRouteStep {
|
||||
* нечего, см. соответствующее поле на бэкенде (TemplateRouteStep.AutoApprove).
|
||||
*/
|
||||
autoApprove: boolean;
|
||||
/**
|
||||
* Код типовой роли шага (напр. "Chief" — непосредственный руководитель),
|
||||
* настраивается в админке маршрутов вместе с самим шагом. undefined — роль
|
||||
* без кода (обычный именной/групповой шаг). Используется формами точечно,
|
||||
* когда нужно сопоставить конкретный шаг маршрута с полем формы (см.
|
||||
* Ticket1C/managerOverride.ts — единственный пример на сейчас).
|
||||
*/
|
||||
positionCode?: string;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ export interface IMatrixRow {
|
||||
* IResolvedRouteStep.autoApprove).
|
||||
*/
|
||||
autoApprove?: boolean;
|
||||
/**
|
||||
* Код типовой роли шага (напр. "Chief"), см. IResolvedRouteStep.positionCode —
|
||||
* то же транзитное поле: только у ПЛАНИРУЕМОГО маршрута (getPlannedMatrix), у
|
||||
* уже записанных строк списка (loadXxxRequest) его нет.
|
||||
*/
|
||||
positionCode?: string;
|
||||
/**
|
||||
* Предупреждение approval-route по ЭТОМУ шагу (см. IResolvedRouteStep.warning) —
|
||||
* например, роль не разрешилась в конкретного человека, и почему именно.
|
||||
|
||||
@@ -77,14 +77,6 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||
else editor.moveBlockDown(rowIndex);
|
||||
};
|
||||
|
||||
// Остальные правки состава — не перестановка: сбрасываем «активный» шаг,
|
||||
// иначе подъём и подсветка достались бы случайной строке, которая сдвинулась
|
||||
// из-за перенумерации после удаления.
|
||||
const removeRow = (rowIndex: number): void => {
|
||||
setActiveKeys([]);
|
||||
editor.remove(rowIndex);
|
||||
};
|
||||
|
||||
const toggleParallel = (rowIndex: number): void => {
|
||||
setActiveKeys([]);
|
||||
editor.toggleParallel(rowIndex);
|
||||
@@ -239,14 +231,6 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||
>
|
||||
<Icon name="user-check" size="sm" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
|
||||
onClick={() => removeRow(i)}
|
||||
title="Удалить"
|
||||
>
|
||||
<Icon name="trash" size="sm" />
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-1;
|
||||
}
|
||||
|
||||
.pickerBox {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: $r-sm;
|
||||
background: $white;
|
||||
font-family: $font-sans;
|
||||
font-size: 13.5px;
|
||||
color: $gray-900;
|
||||
transition: .18s;
|
||||
|
||||
@include input-focus-gold;
|
||||
@include readonly-field(':read-only');
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.modeButton {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 0;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: $r-sm;
|
||||
background: $white;
|
||||
color: $gray-500;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $navy-50;
|
||||
color: $gray-900;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { PeoplePicker, PrincipalType } from '@pnp/spfx-controls-react/lib/PeoplePicker';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { Icon, useFormReadOnly } from '@tnsu/ui-kit';
|
||||
import { buildPickerContext } from './pickerContext';
|
||||
import { peoplePickerStyles } from './peoplePickerStyles';
|
||||
import styles from './PeopleTextField.module.scss';
|
||||
|
||||
interface IPickedPerson {
|
||||
text?: string;
|
||||
secondaryText?: string;
|
||||
loginName?: string;
|
||||
}
|
||||
|
||||
interface IPeopleTextFieldProps {
|
||||
context: WebPartContext;
|
||||
id?: string;
|
||||
/** Текущее значение (свободный текст — ФИО, «ФИО · email» и т.п.). */
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
/** Только чтение (режим согласующего). */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const SEPARATOR = ' · ';
|
||||
const EMAIL_RE = /^\S+@\S+\.\S+$/;
|
||||
|
||||
/** Формат значения после выбора из PeoplePicker: «ФИО · email» — тот же, что у ручного ввода в этой форме. */
|
||||
function formatPicked(person: IPickedPerson): string {
|
||||
const email = person.secondaryText || person.loginName || '';
|
||||
return person.text ? (email ? `${person.text}${SEPARATOR}${email}` : person.text) : email;
|
||||
}
|
||||
|
||||
/** Email из значения — для defaultSelectedUsers пикера. Если распознать нельзя, пикер стартует пустым. */
|
||||
function extractEmail(value: string): string | undefined {
|
||||
const idx = value.lastIndexOf(SEPARATOR);
|
||||
const email = idx !== -1 ? value.slice(idx + SEPARATOR.length).trim() : value.trim();
|
||||
return EMAIL_RE.test(email) ? email : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Поле «сотрудник»: по умолчанию — PeoplePicker (с предзаполнением из value),
|
||||
* кнопка переключает на обычный текстовый ввод для случаев, когда нужного
|
||||
* человека нет в AAD или пикер не резолвит совпадение. В обоих режимах
|
||||
* итоговое значение — свободная строка «ФИО · email», а не жёстко резолвленный
|
||||
* пользователь (в отличие от PeopleField) — переключение между режимами саму
|
||||
* строку не трогает.
|
||||
*/
|
||||
export const PeopleTextField: React.FC<IPeopleTextFieldProps> = ({
|
||||
context,
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly
|
||||
}) => {
|
||||
const contextReadOnly = useFormReadOnly();
|
||||
const effectiveReadOnly = readOnly ?? contextReadOnly;
|
||||
const [manual, setManual] = useState<boolean>(false);
|
||||
|
||||
const handlePick = (items: IPickedPerson[]): void => {
|
||||
const first = items && items.length > 0 ? items[0] : undefined;
|
||||
onChange(first ? formatPicked(first) : '');
|
||||
};
|
||||
|
||||
const defaultEmail = extractEmail(value);
|
||||
const defaultSelectedUsers = defaultEmail ? [defaultEmail] : [];
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
{manual ? (
|
||||
<input
|
||||
id={id}
|
||||
className={styles.input}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
readOnly={effectiveReadOnly}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.pickerBox}>
|
||||
<PeoplePicker
|
||||
context={buildPickerContext(context)}
|
||||
personSelectionLimit={1}
|
||||
principalTypes={[PrincipalType.User]}
|
||||
defaultSelectedUsers={defaultSelectedUsers}
|
||||
resolveDelay={300}
|
||||
ensureUser
|
||||
disabled={effectiveReadOnly}
|
||||
placeholder={placeholder}
|
||||
onChange={handlePick}
|
||||
styles={peoplePickerStyles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!effectiveReadOnly && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.modeButton}
|
||||
title={manual ? 'Выбрать из списка сотрудников' : 'Ввести вручную'}
|
||||
onClick={() => setManual((prev) => !prev)}
|
||||
>
|
||||
<Icon name={manual ? 'search' : 'edit'} size="sm" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -38,6 +38,7 @@ export { EMPTY_LINE_ITEM } from './ItemsTable/ILineItem';
|
||||
export { parseExcelRows } from './ItemsTable/parseExcelRows';
|
||||
export { PeopleField } from './PeopleField/PeopleField';
|
||||
export { PeopleFieldMulti } from './PeopleField/PeopleFieldMulti';
|
||||
export { PeopleTextField } from './PeopleField/PeopleTextField';
|
||||
export { OrganizationField } from './OrganizationField/OrganizationField';
|
||||
export { PrintSplitButton } from './PrintSplitButton/PrintSplitButton';
|
||||
export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead';
|
||||
|
||||
Reference in New Issue
Block a user