feat: добавил поле организация во все заявки, а также расширил компонет выбора даты для ui-kit
This commit is contained in:
@@ -17,19 +17,46 @@ async function client(context: WebPartContext): Promise<AadHttpClient> {
|
||||
return context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||
}
|
||||
|
||||
// Ретрай-политика на сетевые сбои — тот же принцип и те же по порядку величины
|
||||
// значения, что и у RETRY в shared/pnp/pnpjs.ts (там для PnPjs/SharePoint).
|
||||
const NETWORK_RETRY_ATTEMPTS = 2;
|
||||
const NETWORK_RETRY_INTERVAL_MS = 1000;
|
||||
|
||||
/**
|
||||
* Мягкий ретрай на СЕТЕВЫЕ сбои (fetch не долетел вовсе — TypeError «Failed to
|
||||
* fetch»/«NetworkError», не HTTP-статус). Тот же принцип, что и
|
||||
* BrowserFetchWithRetry у PnPjs-клиента (shared/pnp/pnpjs.ts) — там ретрай уже
|
||||
* есть, а этот внешний API его был лишён, из-за чего случайная заминка сети или
|
||||
* холодный старт бэкенда сразу превращались в необратимое «Failed to fetch» в
|
||||
* UI матрицы. HTTP-ошибки (404/401/500 — throw Error ниже по коду) НЕ ретраим:
|
||||
* повтор того же запроса их не лечит, только тянет время до сообщения об ошибке.
|
||||
*/
|
||||
async function withNetworkRetry<T>(fn: () => Promise<T>): Promise<T> {
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
if (attempt >= NETWORK_RETRY_ATTEMPTS || !(e instanceof TypeError)) throw e;
|
||||
await new Promise((resolve) => setTimeout(resolve, NETWORK_RETRY_INTERVAL_MS * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/request-types — список типов заявок (id + имя). Пригодится, чтобы
|
||||
// сопоставить форму с RequestTypeId, если он неизвестен заранее.
|
||||
export async function getRequestTypes(context: WebPartContext): Promise<IRequestType[]> {
|
||||
const c = await client(context);
|
||||
const response: HttpClientResponse = await c.get(
|
||||
`${getApprovalApiBaseUrl()}/api/request-types`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request-types: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IRequestType[]> = await response.json();
|
||||
return body.data ?? [];
|
||||
return withNetworkRetry(async () => {
|
||||
const c = await client(context);
|
||||
const response: HttpClientResponse = await c.get(
|
||||
`${getApprovalApiBaseUrl()}/api/request-types`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request-types: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IRequestType[]> = await response.json();
|
||||
return body.data ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,16 +70,18 @@ export async function getRequestTypes(context: WebPartContext): Promise<IRequest
|
||||
* Типы без настроенного списка SharePoint в ответ не попадают.
|
||||
*/
|
||||
export async function getListPartitions(context: WebPartContext): Promise<IRequestTypeListSeries[]> {
|
||||
const c = await client(context);
|
||||
const response: HttpClientResponse = await c.get(
|
||||
`${getApprovalApiBaseUrl()}/api/request-types/lists`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request-types/lists: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IRequestTypeListSeries[]> = await response.json();
|
||||
return body.data ?? [];
|
||||
return withNetworkRetry(async () => {
|
||||
const c = await client(context);
|
||||
const response: HttpClientResponse = await c.get(
|
||||
`${getApprovalApiBaseUrl()}/api/request-types/lists`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request-types/lists: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IRequestTypeListSeries[]> = await response.json();
|
||||
return body.data ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,16 +96,18 @@ export async function getCurrentListPartition(
|
||||
context: WebPartContext,
|
||||
requestTypeId: number
|
||||
): Promise<ICurrentListPartition> {
|
||||
const c = await client(context);
|
||||
const response: HttpClientResponse = await c.get(
|
||||
`${getApprovalApiBaseUrl()}/api/request-types/${requestTypeId}/lists/current`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request-types/${requestTypeId}/lists/current: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<ICurrentListPartition> = await response.json();
|
||||
return body.data;
|
||||
return withNetworkRetry(async () => {
|
||||
const c = await client(context);
|
||||
const response: HttpClientResponse = await c.get(
|
||||
`${getApprovalApiBaseUrl()}/api/request-types/${requestTypeId}/lists/current`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request-types/${requestTypeId}/lists/current: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<ICurrentListPartition> = await response.json();
|
||||
return body.data;
|
||||
});
|
||||
}
|
||||
|
||||
// Кеш на уровне МОДУЛЯ (не useRef/useState) — список типов заявок админ меняет
|
||||
@@ -138,30 +169,32 @@ export async function resolveRoute(
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- см. departmentId
|
||||
projectId: string | null = null
|
||||
): Promise<IResolvedRoute> {
|
||||
const c = await client(context);
|
||||
const url = `${getApprovalApiBaseUrl()}/api/template-routes/resolve`;
|
||||
return withNetworkRetry(async () => {
|
||||
const c = await client(context);
|
||||
const url = `${getApprovalApiBaseUrl()}/api/template-routes/resolve`;
|
||||
|
||||
// Эндпоинт сменился с GET (query) на POST (JSON-тело).
|
||||
// projectId (uuid проекта) — для форм, где согласующий зависит от проекта
|
||||
// (напр. «руководитель проекта-источника» в СЗ на перемещение ТМЦ): бэкенд по
|
||||
// нему подставляет исполнителя роли. Формы без привязки к проекту шлют null.
|
||||
const response: HttpClientResponse = await c.post(url, AadHttpClient.configurations.v1, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestTypeId,
|
||||
userEmail,
|
||||
projectId: projectId || null,
|
||||
status,
|
||||
fieldValues,
|
||||
departmentId: departmentId || null
|
||||
})
|
||||
// Эндпоинт сменился с GET (query) на POST (JSON-тело).
|
||||
// projectId (uuid проекта) — для форм, где согласующий зависит от проекта
|
||||
// (напр. «руководитель проекта-источника» в СЗ на перемещение ТМЦ): бэкенд по
|
||||
// нему подставляет исполнителя роли. Формы без привязки к проекту шлют null.
|
||||
const response: HttpClientResponse = await c.post(url, AadHttpClient.configurations.v1, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestTypeId,
|
||||
userEmail,
|
||||
projectId: projectId || null,
|
||||
status,
|
||||
fieldValues,
|
||||
departmentId: departmentId || null
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`resolve: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IResolvedRoute> = await response.json();
|
||||
return body.data;
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`resolve: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IResolvedRoute> = await response.json();
|
||||
return body.data;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||
waiting = false,
|
||||
waitingText = '',
|
||||
}) => {
|
||||
const { rows, loading, error, editable, blocks } = editor;
|
||||
const { rows, loading, error, editable, blocks, retry } = editor;
|
||||
|
||||
// Подпись «доп. согласующий»/«доп. исполнитель» — из roleLabel, а не
|
||||
// хардкод: иначе блок «Исполнение» (roleLabel="Исполнитель") показывал бы
|
||||
@@ -113,7 +113,14 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||
<div className={styles.matrixTitle}>{title}</div>
|
||||
{waiting && <div className={styles.matrixNote}>{waitingText}</div>}
|
||||
{!waiting && loading && <div className={styles.matrixNote}>Загрузка маршрута…</div>}
|
||||
{!waiting && !loading && error && <div className={styles.matrixNote}>Маршрут недоступен: {error}</div>}
|
||||
{!waiting && !loading && error && (
|
||||
<div className={styles.matrixNote}>
|
||||
Маршрут недоступен: {error}{' '}
|
||||
<button type="button" className={styles.retryBtn} onClick={retry}>
|
||||
Повторить
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!waiting && !loading && !error && rows.length === 0 && (
|
||||
<div className={styles.matrixNote}>{emptyText}</div>
|
||||
)}
|
||||
|
||||
@@ -91,6 +91,23 @@
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
// Инлайновая ссылка-кнопка повтора запроса рядом с текстом ошибки — тот же
|
||||
// приём, что у .email в InitiatorLine: не отдельная кнопка с рамкой, а
|
||||
// подчёркиваемая ссылка внутри строки заметки.
|
||||
.retryBtn {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font: inherit;
|
||||
color: $navy-700;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $gold-600;
|
||||
}
|
||||
}
|
||||
|
||||
// Обёртка таблицы: на промежуточных ширинах — горизонтальный скролл вместо
|
||||
// сплющивания колонок. На телефоне таблица становится карточками (медиазапрос ниже).
|
||||
.matrixScroll {
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface IUseMatrixEditor {
|
||||
loading: boolean;
|
||||
error: string | undefined;
|
||||
editable: boolean;
|
||||
/** Перезапросить маршрут ещё раз (после сетевой ошибки) без смены reloadKey. */
|
||||
retry: () => void;
|
||||
addApprover: (user: ISiteUser) => void;
|
||||
/** Переместить ШАГ целиком (вместе со всеми параллельными строками) выше/ниже. */
|
||||
moveBlockUp: (index: number) => void;
|
||||
@@ -155,6 +157,10 @@ export function useMatrixEditor(
|
||||
const [rows, setRows] = useState<IMatrixRow[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
// Ручной повтор после ошибки (см. кнопку «Повторить» в ApprovalMatrix) — свой
|
||||
// счётчик, а не reloadKey: тот приходит от вызывающего и не должен знать о
|
||||
// повторных попытках, это внутренняя механика самого хука.
|
||||
const [retrySeq, setRetrySeq] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) return;
|
||||
@@ -175,9 +181,11 @@ export function useMatrixEditor(
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
// load — стабильная ссылка от вызывающего (useCallback); перезагрузка по reloadKey/ready.
|
||||
// load — стабильная ссылка от вызывающего (useCallback); перезагрузка по reloadKey/ready/retrySeq.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [reloadKey, ready]);
|
||||
}, [reloadKey, ready, retrySeq]);
|
||||
|
||||
const retry = (): void => setRetrySeq((n) => n + 1);
|
||||
|
||||
const appendUser = (user: ISiteUser, position: string): void => {
|
||||
setRows((prev) => {
|
||||
@@ -235,6 +243,7 @@ export function useMatrixEditor(
|
||||
loading,
|
||||
error,
|
||||
editable,
|
||||
retry,
|
||||
addApprover,
|
||||
moveBlockUp,
|
||||
moveBlockDown,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
function pad2(n: number): string {
|
||||
return n < 10 ? `0${n}` : `${n}`;
|
||||
}
|
||||
|
||||
/** Сегодня + n дней в формате YYYY-MM-DD (для min/max у input type=date). */
|
||||
export function isoDatePlusDays(days: number): string {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + days);
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
/** YYYY-MM-DD → дд.мм.гггг, для текста ошибок валидации. Невалидную строку возвращает как есть. */
|
||||
export function formatIsoDate(iso: string): string {
|
||||
const [y, m, d] = iso.split('-');
|
||||
return y && m && d ? `${d}.${m}.${y}` : iso;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { APPROVAL_API_CLIENT_ID, DICTIONARY_API_BASE_URL } from '../config';
|
||||
import { IEmployeeOrganization } from './models/IEmployeeOrganization';
|
||||
|
||||
// GET /Employee/ByEmail?Email=... — организации сотрудника (см. IEmployeeOrganization:
|
||||
// один email может вернуть несколько записей). 404 (email не найден в системе
|
||||
// кадров) — не ошибка формы, просто пусто: не у каждого приглашённого/внешнего
|
||||
// пользователя портала есть карточка сотрудника.
|
||||
async function fetchEmployeeOrganizations(context: WebPartContext, email: string): Promise<IEmployeeOrganization[]> {
|
||||
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||
const response: HttpClientResponse = await client.get(
|
||||
`${DICTIONARY_API_BASE_URL}/Employee/ByEmail?Email=${encodeURIComponent(email)}`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (response.status === 404) return [];
|
||||
if (!response.ok) {
|
||||
throw new Error(`Employee/ByEmail: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body = (await response.json()) as Array<Record<string, unknown>>;
|
||||
return body.map((r) => ({
|
||||
id: (r.Id as string) || '',
|
||||
fio: (r.Fio as string) || '',
|
||||
email: (r.Email as string) || '',
|
||||
company: (r.Company as string) || '',
|
||||
companyId: (r.CompanyId as string) || '',
|
||||
section: (r.Section as string) || '',
|
||||
departmentId: (r.DepartmentId as string) || '',
|
||||
position: (r.Position as string) || '',
|
||||
mobile: (r.Mobile as string) || ''
|
||||
}));
|
||||
}
|
||||
|
||||
// Кеш по e-mail (не один общий промис, как у companies/departments — там список
|
||||
// один на всех, здесь результат зависит от аргумента). Ошибку не кешируем — см.
|
||||
// companies.service.ts, тот же принцип.
|
||||
const cache = new Map<string, Promise<IEmployeeOrganization[]>>();
|
||||
|
||||
/** Организации сотрудника по e-mail. Пустой email → [] без обращения к API. */
|
||||
export function getEmployeeOrganizations(context: WebPartContext, email: string): Promise<IEmployeeOrganization[]> {
|
||||
if (!email) return Promise.resolve([]);
|
||||
const cached = cache.get(email);
|
||||
if (cached) return cached;
|
||||
const promise = fetchEmployeeOrganizations(context, email).catch((e) => {
|
||||
cache.delete(email);
|
||||
throw e;
|
||||
});
|
||||
cache.set(email, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* «Компания — должность» по каждой записи, по одной на строку (`\n`) — общий
|
||||
* формат и для отображения (построчно), и для хранения в списке SharePoint
|
||||
* (колонка Organization, Note — поддерживает переносы строк).
|
||||
*/
|
||||
export function formatEmployeeOrganizations(records: IEmployeeOrganization[]): string {
|
||||
return records.map((r) => (r.position ? `${r.company} — ${r.position}` : r.company)).join('\n');
|
||||
}
|
||||
@@ -10,3 +10,5 @@ export { getCompanies } from './companies.service';
|
||||
export type { ICompany } from './models/ICompany';
|
||||
export { CompanyPickerMulti } from './ui/CompanyPickerMulti';
|
||||
export type { ICompanyPickerMultiProps } from './ui/CompanyPickerMulti';
|
||||
export { getEmployeeOrganizations, formatEmployeeOrganizations } from './employeeOrganizations.service';
|
||||
export type { IEmployeeOrganization } from './models/IEmployeeOrganization';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Одна запись из GET /Employee/ByEmail — сотрудник МОЖЕТ состоять сразу в
|
||||
* нескольких организациях (совместительство/аффилированные ТОО), тогда на один
|
||||
* и тот же e-mail эндпоинт возвращает НЕСКОЛЬКО записей с разными Company/
|
||||
* CompanyId/Section/DepartmentId/Position.
|
||||
*/
|
||||
export interface IEmployeeOrganization {
|
||||
id: string;
|
||||
fio: string;
|
||||
email: string;
|
||||
company: string;
|
||||
companyId: string;
|
||||
section: string;
|
||||
departmentId: string;
|
||||
position: string;
|
||||
mobile: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $sp-1;
|
||||
}
|
||||
|
||||
// Read-only «карточка» на организацию — тот же радиус/паддинг, что у TextInput
|
||||
// ($r-sm, 11px/13px из @tnsu/ui-kit), но приглушённый фон/рамка (как в
|
||||
// readonly-field) — сигнал «подставлено автоматически, не редактируется».
|
||||
.line {
|
||||
box-sizing: border-box;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-sm;
|
||||
background: $gray-50;
|
||||
font-family: $font-sans;
|
||||
font-size: 13.5px;
|
||||
color: $gray-700;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import styles from './OrganizationField.module.scss';
|
||||
import { FormField } from '@tnsu/ui-kit';
|
||||
|
||||
interface IOrganizationFieldProps {
|
||||
/** `\n`-joined «Компания — должность» на организацию (см. formatEmployeeOrganizations). */
|
||||
value: string;
|
||||
/**
|
||||
* Подпись поля. По умолчанию «Организация» — переопределяют формы, где
|
||||
* секция описывает ДРУГОГО человека (напр. нового сотрудника), чтобы не
|
||||
* читалось как «организация того, о ком заявка».
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Организации сотрудника — read-only, подтягивается автоматически по e-mail
|
||||
* (см. shared/dictionary/employeeOrganizations.service.ts), руками не
|
||||
* редактируется. Сотрудник может состоять сразу в НЕСКОЛЬКИХ организациях —
|
||||
* тогда строк несколько. Пока не резолвилось (сотрудник не выбран/не найден) —
|
||||
* ничего не рендерим, а не пустой блок.
|
||||
*/
|
||||
export const OrganizationField: React.FC<IOrganizationFieldProps> = ({ value, label = 'Организация' }) => {
|
||||
const lines = value ? value.split('\n') : [];
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
return (
|
||||
<FormField label={label} full>
|
||||
<div className={styles.list}>
|
||||
{lines.map((line) => (
|
||||
<div key={line} className={styles.line}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { PeoplePicker, PrincipalType } from '@pnp/spfx-controls-react/lib/PeoplePicker';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { buildPickerContext, pickedLogin, IPickedUser } from './pickerContext';
|
||||
import { peoplePickerStyles } from './peoplePickerStyles';
|
||||
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||
|
||||
interface IPeopleFieldProps {
|
||||
@@ -34,6 +35,7 @@ export const PeopleField: React.FC<IPeopleFieldProps> = ({ context, value, onCha
|
||||
disabled={effectiveReadOnly}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
styles={peoplePickerStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { PeoplePicker, PrincipalType } from '@pnp/spfx-controls-react/lib/PeoplePicker';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { buildPickerContext, pickedLogin, IPickedUser } from './pickerContext';
|
||||
import { peoplePickerStyles } from './peoplePickerStyles';
|
||||
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||
|
||||
interface IPeopleFieldMultiProps {
|
||||
@@ -43,6 +44,7 @@ export const PeopleFieldMulti: React.FC<IPeopleFieldMultiProps> = ({
|
||||
disabled={effectiveReadOnly}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
styles={peoplePickerStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { IBasePickerStyles } from '@fluentui/react/lib/Pickers';
|
||||
|
||||
/**
|
||||
* PeoplePicker — сторонний Fluent-контрол (@pnp/spfx-controls-react), а не наш
|
||||
* @tnsu/ui-kit: у него нет своего CSS-модуля и без этих стилей он выглядит
|
||||
* дефолтным Fluent-полем — другой border-radius, другие паддинги, другой цвет
|
||||
* рамки, не в тон соседним полям формы (TextInput и т.п.). Стилизуется через
|
||||
* fluent-проп `styles` (IBasePickerStyles), а не через className/CSS-модуль,
|
||||
* поэтому значения ниже — те же токены, что в styles/_design-tokens.scss
|
||||
* ($gray-300, $gold-500, $r-sm, TextInput.module.scss), но продублированы как
|
||||
* значения: Fluent styles-проп не умеет импортировать SCSS-переменные.
|
||||
*/
|
||||
export const peoplePickerStyles: Partial<IBasePickerStyles> = {
|
||||
text: {
|
||||
boxSizing: 'border-box',
|
||||
padding: '9px 12px',
|
||||
border: '1px solid #D1D5DB', // $gray-300
|
||||
borderRadius: 6, // $r-sm
|
||||
background: '#fff',
|
||||
fontFamily: "'Inter', -apple-system, 'Segoe UI', sans-serif",
|
||||
fontSize: 13.5,
|
||||
transition: '.18s',
|
||||
selectors: {
|
||||
// Фокус — на самом input внутри .text, подсветку показываем на всей рамке.
|
||||
':focus-within': {
|
||||
borderColor: '#D4A01E', // $gold-500
|
||||
boxShadow: '0 0 0 3px rgba(212,160,30,.12)'
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
padding: 0,
|
||||
fontFamily: "'Inter', -apple-system, 'Segoe UI', sans-serif",
|
||||
fontSize: 13.5,
|
||||
color: '#111827', // $gray-900
|
||||
selectors: {
|
||||
'::placeholder': { color: '#9CA3AF' } // $gray-400
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 { OrganizationField } from './OrganizationField/OrganizationField';
|
||||
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
|
||||
export { SubmitActions } from './SubmitActions/SubmitActions';
|
||||
export { InitiatorLine } from './InitiatorLine/InitiatorLine';
|
||||
|
||||
Reference in New Issue
Block a user