чистка кода
This commit is contained in:
+13
-2
@@ -71,8 +71,19 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
|||||||
[blocks, withParallel]
|
[blocks, withParallel]
|
||||||
);
|
);
|
||||||
// Индекс последнего заблокированного блока — все блоки ПОСЛЕ него движимы,
|
// Индекс последнего заблокированного блока — все блоки ПОСЛЕ него движимы,
|
||||||
// все блоки НА НЁМ И РАНЬШЕ — нет (см. комментарий к компоненту).
|
// все блоки НА НЁМ И РАНЬШЕ — нет (см. комментарий к компоненту). Считаем
|
||||||
const lastLockedBlockIndex = lockedBlock.reduce((acc, locked, i) => (locked ? i : acc), -1);
|
// именно НЕПРЕРЫВНЫЙ префикс заблокированных блоков с начала, а не индекс
|
||||||
|
// последнего true по всему списку: реальные данные не всегда гарантируют
|
||||||
|
// строгую непрерывность (например, более ранняя ручная правка тем же
|
||||||
|
// инструментом могла оставить решённый шаг ПОСЛЕ ещё не решённого) — а
|
||||||
|
// reduce по всему массиву в этом случае блокирует стрелки вообще у всех
|
||||||
|
// блоков до самого дальнего заблокированного, даже у тех, что реально
|
||||||
|
// редактируемы.
|
||||||
|
let lastLockedBlockIndex = -1;
|
||||||
|
for (let i = 0; i < lockedBlock.length; i++) {
|
||||||
|
if (!lockedBlock[i]) break;
|
||||||
|
lastLockedBlockIndex = i;
|
||||||
|
}
|
||||||
const firstMovableRowIndex = blocks[lastLockedBlockIndex + 1]?.start ?? -1;
|
const firstMovableRowIndex = blocks[lastLockedBlockIndex + 1]?.start ?? -1;
|
||||||
|
|
||||||
// Фаза уже целиком решена (например, все согласующие одобрили, и заявка ушла
|
// Фаза уже целиком решена (например, все согласующие одобрили, и заявка ушла
|
||||||
|
|||||||
@@ -42,15 +42,20 @@ interface IFormState {
|
|||||||
|
|
||||||
const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', listId: '' };
|
const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', listId: '' };
|
||||||
|
|
||||||
|
// siteUrl/listId — поля RequestType, зеркалящие nullable JSON-контракт бэкенда
|
||||||
|
// (см. models/requestTypes.ts) — здесь сразу приводим null к undefined,
|
||||||
|
// внутренний код (хуки/кэш SharePointListsClient) с null не работает нигде.
|
||||||
/** Ячейка «Сайт SharePoint» — подтягивает заголовок сайта по url, хранящемуся на бэкенде. */
|
/** Ячейка «Сайт SharePoint» — подтягивает заголовок сайта по url, хранящемуся на бэкенде. */
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null -- siteUrl зеркалит RequestType.siteUrl (nullable JSON-контракт бэкенда)
|
||||||
const SiteUrlCell: React.FC<{ client: SharePointListsClient; siteUrl: string | null }> = ({ client, siteUrl }) => {
|
const SiteUrlCell: React.FC<{ client: SharePointListsClient; siteUrl: string | null }> = ({ client, siteUrl }) => {
|
||||||
const { title } = useSharePointSiteTitle(client, siteUrl);
|
const { title } = useSharePointSiteTitle(client, siteUrl ?? undefined);
|
||||||
return <>{siteUrl ? formatSiteLabel(title, siteUrl) : texts.common.empty}</>;
|
return <>{siteUrl ? formatSiteLabel(title, siteUrl) : texts.common.empty}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Ячейка «Список SharePoint» — на бэкенде хранится id списка (устойчив к переименованию), тут подтягиваем Title для читаемости. */
|
/** Ячейка «Список SharePoint» — на бэкенде хранится id списка (устойчив к переименованию), тут подтягиваем Title для читаемости. */
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null -- siteUrl/listId зеркалят RequestType (nullable JSON-контракт бэкенда)
|
||||||
const ListIdCell: React.FC<{ client: SharePointListsClient; siteUrl: string | null; listId: string | null }> = ({ client, siteUrl, listId }) => {
|
const ListIdCell: React.FC<{ client: SharePointListsClient; siteUrl: string | null; listId: string | null }> = ({ client, siteUrl, listId }) => {
|
||||||
const { title } = useSharePointListTitle(client, siteUrl, listId);
|
const { title } = useSharePointListTitle(client, siteUrl ?? undefined, listId ?? undefined);
|
||||||
return <>{listId ? (title ?? listId) : texts.common.empty}</>;
|
return <>{listId ? (title ?? listId) : texts.common.empty}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +109,11 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
|||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
pageUrl: form.pageUrl.trim(),
|
pageUrl: form.pageUrl.trim(),
|
||||||
section: form.section.trim(),
|
section: form.section.trim(),
|
||||||
|
// null, а не undefined — контракт CreateRequestTypeInput зеркалит
|
||||||
|
// nullable-поля бэкенда (см. models/requestTypes.ts).
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null
|
||||||
siteUrl: siteUrl || null,
|
siteUrl: siteUrl || null,
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null
|
||||||
listId: listId || null
|
listId: listId || null
|
||||||
};
|
};
|
||||||
const request = editing ? api.requestTypes.update(editing.id, input) : api.requestTypes.create(input);
|
const request = editing ? api.requestTypes.update(editing.id, input) : api.requestTypes.create(input);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
|||||||
|
|
||||||
const requestType = requestTypes.items.find(rt => rt.id === Number(requestTypeId));
|
const requestType = requestTypes.items.find(rt => rt.id === Number(requestTypeId));
|
||||||
const hasListId = Boolean(requestType?.listId);
|
const hasListId = Boolean(requestType?.listId);
|
||||||
const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl, requestType?.listId);
|
const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl ?? undefined, requestType?.listId ?? undefined);
|
||||||
const fieldNameOptions = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title }));
|
const fieldNameOptions = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title }));
|
||||||
|
|
||||||
const resolved = useResolveRoute(api);
|
const resolved = useResolveRoute(api);
|
||||||
@@ -272,7 +272,7 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
|||||||
key: 'executor',
|
key: 'executor',
|
||||||
header: texts.resolve.columnExecutor,
|
header: texts.resolve.columnExecutor,
|
||||||
render: r => {
|
render: r => {
|
||||||
const employee = findEmployeeByEmail(employees.items, r.executorEmail);
|
const employee = findEmployeeByEmail(employees.items, r.executorEmail ?? undefined);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PrimaryCell>{employee ? employee.fio : (r.executorEmail ?? texts.common.empty)}</PrimaryCell>
|
<PrimaryCell>{employee ? employee.fio : (r.executorEmail ?? texts.common.empty)}</PrimaryCell>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
|||||||
const [form, setForm] = React.useState<IFormState>(() => toFormState(editing, nextOrderNo));
|
const [form, setForm] = React.useState<IFormState>(() => toFormState(editing, nextOrderNo));
|
||||||
const submit = useAsyncSubmit();
|
const submit = useAsyncSubmit();
|
||||||
const hasListId = Boolean(requestType?.listId);
|
const hasListId = Boolean(requestType?.listId);
|
||||||
const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl, requestType?.listId);
|
const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl ?? undefined, requestType?.listId ?? undefined);
|
||||||
|
|
||||||
const fieldNameOptions = React.useMemo(() => {
|
const fieldNameOptions = React.useMemo(() => {
|
||||||
const options = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title }));
|
const options = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title }));
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export interface SharePointFieldsState {
|
|||||||
/** Заголовки столбцов списка (site_url + list_name типа заявки) — источник значений для field_name шага. */
|
/** Заголовки столбцов списка (site_url + list_name типа заявки) — источник значений для field_name шага. */
|
||||||
export function useSharePointFields(
|
export function useSharePointFields(
|
||||||
client: SharePointListsClient | undefined,
|
client: SharePointListsClient | undefined,
|
||||||
siteUrl: string | undefined | null,
|
siteUrl: string | undefined,
|
||||||
listTitle: string | undefined | null
|
listTitle: string | undefined
|
||||||
): SharePointFieldsState {
|
): SharePointFieldsState {
|
||||||
const [items, setItems] = React.useState<SharePointListField[]>([]);
|
const [items, setItems] = React.useState<SharePointListField[]>([]);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ import * as React from 'react';
|
|||||||
import { SharePointListsClient } from '../services/sharePointLists';
|
import { SharePointListsClient } from '../services/sharePointLists';
|
||||||
|
|
||||||
export interface SharePointListTitleState {
|
export interface SharePointListTitleState {
|
||||||
title: string | null;
|
title: string | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заголовок списка SharePoint по (site_url, list_id) — для отображения «Название (id)» вместо голого GUID. */
|
/** Заголовок списка SharePoint по (site_url, list_id) — для отображения «Название (id)» вместо голого GUID. */
|
||||||
export function useSharePointListTitle(
|
export function useSharePointListTitle(
|
||||||
client: SharePointListsClient | undefined,
|
client: SharePointListsClient | undefined,
|
||||||
siteUrl: string | undefined | null,
|
siteUrl: string | undefined,
|
||||||
listId: string | undefined | null
|
listId: string | undefined
|
||||||
): SharePointListTitleState {
|
): SharePointListTitleState {
|
||||||
const [title, setTitle] = React.useState<string | null>(null);
|
const [title, setTitle] = React.useState<string | undefined>(undefined);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!client || !siteUrl?.trim() || !listId?.trim()) {
|
if (!client || !siteUrl?.trim() || !listId?.trim()) {
|
||||||
setTitle(null);
|
setTitle(undefined);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ export function useSharePointListTitle(
|
|||||||
|
|
||||||
client.getListTitle(siteUrl.trim(), listId.trim())
|
client.getListTitle(siteUrl.trim(), listId.trim())
|
||||||
.then(result => { if (!cancelled) setTitle(result); })
|
.then(result => { if (!cancelled) setTitle(result); })
|
||||||
.catch(() => { if (!cancelled) setTitle(null); })
|
.catch(() => { if (!cancelled) setTitle(undefined); })
|
||||||
.finally(() => { if (!cancelled) setIsLoading(false); });
|
.finally(() => { if (!cancelled) setIsLoading(false); });
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import * as React from 'react';
|
|||||||
import { SharePointListsClient } from '../services/sharePointLists';
|
import { SharePointListsClient } from '../services/sharePointLists';
|
||||||
|
|
||||||
export interface SharePointSiteTitleState {
|
export interface SharePointSiteTitleState {
|
||||||
title: string | null;
|
title: string | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заголовок сайта SharePoint по его url — для отображения «Название — url» вместо голого url. */
|
/** Заголовок сайта SharePoint по его url — для отображения «Название — url» вместо голого url. */
|
||||||
export function useSharePointSiteTitle(
|
export function useSharePointSiteTitle(
|
||||||
client: SharePointListsClient | undefined,
|
client: SharePointListsClient | undefined,
|
||||||
siteUrl: string | undefined | null
|
siteUrl: string | undefined
|
||||||
): SharePointSiteTitleState {
|
): SharePointSiteTitleState {
|
||||||
const [title, setTitle] = React.useState<string | null>(null);
|
const [title, setTitle] = React.useState<string | undefined>(undefined);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!client || !siteUrl?.trim()) {
|
if (!client || !siteUrl?.trim()) {
|
||||||
setTitle(null);
|
setTitle(undefined);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export function useSharePointSiteTitle(
|
|||||||
|
|
||||||
client.getSiteTitle(siteUrl.trim())
|
client.getSiteTitle(siteUrl.trim())
|
||||||
.then(result => { if (!cancelled) setTitle(result); })
|
.then(result => { if (!cancelled) setTitle(result); })
|
||||||
.catch(() => { if (!cancelled) setTitle(null); })
|
.catch(() => { if (!cancelled) setTitle(undefined); })
|
||||||
.finally(() => { if (!cancelled) setIsLoading(false); });
|
.finally(() => { if (!cancelled) setIsLoading(false); });
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Справочники api.tnsu.kz (компании, отделы, проекты) в виде, удобном для UI.
|
// Справочники api.tnsu.kz (компании, отделы, проекты) в виде, удобном для UI.
|
||||||
// Приведение из «сырых» PascalCase-ответов портала — в services/portalDictionary.ts.
|
// Приведение из «сырых» PascalCase-ответов портала — в services/portalDictionary.ts.
|
||||||
|
/* eslint-disable @rushstack/no-new-null -- поля 1:1 зеркалят nullable JSON-контракт портала api.tnsu.kz */
|
||||||
|
|
||||||
export interface Company {
|
export interface Company {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export interface ManagedRequestPage {
|
|||||||
export interface ManagedApprovalRow {
|
export interface ManagedApprovalRow {
|
||||||
id: number;
|
id: number;
|
||||||
ticketId: number;
|
ticketId: number;
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null -- lookup-поле SharePoint, у ghost-строки (см. handleAddApprover) значения ещё нет
|
||||||
approverId: number | null;
|
approverId: number | null;
|
||||||
approverName: string;
|
approverName: string;
|
||||||
approverEmail: string;
|
approverEmail: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// DTO эндпоинта api/request-types (Application/RequestTypes/*.cs).
|
// DTO эндпоинта api/request-types (Application/RequestTypes/*.cs).
|
||||||
|
/* eslint-disable @rushstack/no-new-null -- поля 1:1 зеркалят nullable JSON-контракт бэкенда */
|
||||||
|
|
||||||
export interface RequestType {
|
export interface RequestType {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// DTO эндпоинта api/template-routes/resolve
|
// DTO эндпоинта api/template-routes/resolve
|
||||||
// (Application/TemplateRoutes/ResolveTemplateRouteDto.cs).
|
// (Application/TemplateRoutes/ResolveTemplateRouteDto.cs).
|
||||||
|
/* eslint-disable @rushstack/no-new-null -- поля 1:1 зеркалят nullable JSON-контракт бэкенда */
|
||||||
|
|
||||||
import { RouteStepStatus } from './enums';
|
import { RouteStepStatus } from './enums';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// DTO эндпоинта api/template-routes/{routeId}/steps
|
// DTO эндпоинта api/template-routes/{routeId}/steps
|
||||||
// (Application/TemplateRouteSteps/*.cs).
|
// (Application/TemplateRouteSteps/*.cs).
|
||||||
|
/* eslint-disable @rushstack/no-new-null -- поля 1:1 зеркалят nullable JSON-контракт бэкенда (C# nullable-типы сериализуются в null, не в отсутствие поля) */
|
||||||
|
|
||||||
import { ComparisonOperator, HeadType, ProjectRole, RouteStepStatus } from './enums';
|
import { ComparisonOperator, HeadType, ProjectRole, RouteStepStatus } from './enums';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// DTO эндпоинта api/template-routes (Application/TemplateRoutes/*.cs).
|
// DTO эндпоинта api/template-routes (Application/TemplateRoutes/*.cs).
|
||||||
|
/* eslint-disable @rushstack/no-new-null -- поля 1:1 зеркалят nullable JSON-контракт бэкенда */
|
||||||
|
|
||||||
export interface TemplateRoute {
|
export interface TemplateRoute {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -54,10 +54,13 @@ const PARAM_KEYS = [
|
|||||||
'aamtype', 'aamreq'
|
'aamtype', 'aamreq'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// raw — результат URLSearchParams.get() (DOM API, возвращает string | null).
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null
|
||||||
function parseTab(raw: string | null): ScreenKey {
|
function parseTab(raw: string | null): ScreenKey {
|
||||||
return raw && (TABS as string[]).indexOf(raw) !== -1 ? (raw as ScreenKey) : 'dashboard';
|
return raw && (TABS as string[]).indexOf(raw) !== -1 ? (raw as ScreenKey) : 'dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @rushstack/no-new-null -- raw — результат URLSearchParams.get() (DOM API)
|
||||||
function parseFieldValues(raw: string | null): ResolveFieldValue[] {
|
function parseFieldValues(raw: string | null): ResolveFieldValue[] {
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class ApiClientError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryParams = Record<string, string | number | boolean | undefined | null>;
|
export type QueryParams = Record<string, string | number | boolean | undefined>;
|
||||||
|
|
||||||
export interface IHttpClient {
|
export interface IHttpClient {
|
||||||
get<T>(path: string, query?: QueryParams): Promise<T>;
|
get<T>(path: string, query?: QueryParams): Promise<T>;
|
||||||
@@ -34,7 +34,7 @@ function buildUrl(baseUrl: string, path: string, query?: QueryParams): string {
|
|||||||
if (query) {
|
if (query) {
|
||||||
Object.keys(query).forEach(key => {
|
Object.keys(query).forEach(key => {
|
||||||
const value = query[key];
|
const value = query[key];
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
if (value !== undefined && value !== '') {
|
||||||
url.searchParams.set(key, String(value));
|
url.searchParams.set(key, String(value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @rushstack/no-new-null -- поля "сырых" IPortal*-интерфейсов 1:1 зеркалят nullable JSON-контракт портала api.tnsu.kz */
|
||||||
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||||
import { Company, Department, Employee, Project } from '../models';
|
import { Company, Department, Employee, Project } from '../models';
|
||||||
import { ApiClientError } from './httpClient';
|
import { ApiClientError } from './httpClient';
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function isNumericFieldType(typeAsString: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** «Название — url», либо просто url, пока название ещё не подтянулось/не нашлось. */
|
/** «Название — url», либо просто url, пока название ещё не подтянулось/не нашлось. */
|
||||||
export function formatSiteLabel(title: string | null | undefined, url: string): string {
|
export function formatSiteLabel(title: string | undefined, url: string): string {
|
||||||
return title ? `${title} — ${url}` : url;
|
return title ? `${title} — ${url}` : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ export class SharePointListsClient {
|
|||||||
// Заголовок сайта по site_url и заголовок списка по (site_url, list_id) бэкенд
|
// Заголовок сайта по site_url и заголовок списка по (site_url, list_id) бэкенд
|
||||||
// не хранит (только сами url) — кэшируем на клиенте, чтобы не дёргать сайт
|
// не хранит (только сами url) — кэшируем на клиенте, чтобы не дёргать сайт
|
||||||
// заново на каждый рендер строки таблицы или переоткрытие формы с тем же значением.
|
// заново на каждый рендер строки таблицы или переоткрытие формы с тем же значением.
|
||||||
private readonly siteTitleCache = new Map<string, string | null>();
|
private readonly siteTitleCache = new Map<string, string | undefined>();
|
||||||
private readonly listTitleCache = new Map<string, string | null>();
|
private readonly listTitleCache = new Map<string, string | undefined>();
|
||||||
|
|
||||||
constructor(private readonly context: WebPartContext) {}
|
constructor(private readonly context: WebPartContext) {}
|
||||||
|
|
||||||
@@ -90,33 +90,31 @@ export class SharePointListsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Заголовок сайта по его url — для отображения рядом с url там, где хранится только он. */
|
/** Заголовок сайта по его url — для отображения рядом с url там, где хранится только он. */
|
||||||
public async getSiteTitle(siteUrl: string): Promise<string | null> {
|
public async getSiteTitle(siteUrl: string): Promise<string | undefined> {
|
||||||
const cached = this.siteTitleCache.get(siteUrl);
|
if (this.siteTitleCache.has(siteUrl)) return this.siteTitleCache.get(siteUrl);
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const web = await spfi(siteUrl).using(SPFx(this.context)).web.select('Title')() as { Title: string };
|
const web = await spfi(siteUrl).using(SPFx(this.context)).web.select('Title')() as { Title: string };
|
||||||
this.siteTitleCache.set(siteUrl, web.Title);
|
this.siteTitleCache.set(siteUrl, web.Title);
|
||||||
return web.Title;
|
return web.Title;
|
||||||
} catch {
|
} catch {
|
||||||
this.siteTitleCache.set(siteUrl, null);
|
this.siteTitleCache.set(siteUrl, undefined);
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заголовок списка по (site_url, list_id — server-relative url списка) — для отображения там, где хранится только он. */
|
/** Заголовок списка по (site_url, list_id — server-relative url списка) — для отображения там, где хранится только он. */
|
||||||
public async getListTitle(siteUrl: string, listId: string): Promise<string | null> {
|
public async getListTitle(siteUrl: string, listId: string): Promise<string | undefined> {
|
||||||
const key = `${siteUrl}|${listId}`;
|
const key = `${siteUrl}|${listId}`;
|
||||||
const cached = this.listTitleCache.get(key);
|
if (this.listTitleCache.has(key)) return this.listTitleCache.get(key);
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const list = await clientFor(this.context, siteUrl).web.getList(listId).select('Title')() as { Title: string };
|
const list = await clientFor(this.context, siteUrl).web.getList(listId).select('Title')() as { Title: string };
|
||||||
this.listTitleCache.set(key, list.Title);
|
this.listTitleCache.set(key, list.Title);
|
||||||
return list.Title;
|
return list.Title;
|
||||||
} catch {
|
} catch {
|
||||||
this.listTitleCache.set(key, null);
|
this.listTitleCache.set(key, undefined);
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function toEmployeeOption(employee: Employee): ISearchableOption {
|
|||||||
return { value: employee.email, label: employee.fio, sublabel: employee.email };
|
return { value: employee.email, label: employee.fio, sublabel: employee.email };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findEmployeeByEmail(employees: Employee[], email: string | null): Employee | undefined {
|
export function findEmployeeByEmail(employees: Employee[], email: string | undefined): Employee | undefined {
|
||||||
if (!email) return undefined;
|
if (!email) return undefined;
|
||||||
const target = email.toLowerCase();
|
const target = email.toLowerCase();
|
||||||
return employees.find(e => e.email.toLowerCase() === target);
|
return employees.find(e => e.email.toLowerCase() === target);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { comparisonOperatorSymbols, headTypeLabels, projectRoleLabels, texts } f
|
|||||||
import { findEmployeeByEmail } from './employee';
|
import { findEmployeeByEmail } from './employee';
|
||||||
|
|
||||||
/** Каким из четырёх взаимоисключающих способов у шага задан исполнитель. */
|
/** Каким из четырёх взаимоисключающих способов у шага задан исполнитель. */
|
||||||
|
/* eslint-disable @rushstack/no-new-null -- форма зеркалит TemplateRouteStep (nullable JSON-контракт бэкенда) */
|
||||||
export function detectExecutorKind(step: {
|
export function detectExecutorKind(step: {
|
||||||
executorEmail?: string | null;
|
executorEmail?: string | null;
|
||||||
position?: string | null;
|
position?: string | null;
|
||||||
projectRole?: ProjectRole | null;
|
projectRole?: ProjectRole | null;
|
||||||
headType?: HeadType | null;
|
headType?: HeadType | null;
|
||||||
}): StepExecutorKind {
|
}): StepExecutorKind {
|
||||||
|
/* eslint-enable @rushstack/no-new-null */
|
||||||
if (step.executorEmail) return 'email';
|
if (step.executorEmail) return 'email';
|
||||||
if (step.position) return 'position';
|
if (step.position) return 'position';
|
||||||
if (step.projectRole && step.projectRole !== ProjectRole.None) return 'projectRole';
|
if (step.projectRole && step.projectRole !== ProjectRole.None) return 'projectRole';
|
||||||
@@ -24,7 +26,7 @@ export interface StepExecutorDisplay {
|
|||||||
/** Колонка «Исполнитель» в таблице шагов шаблона маршрута. */
|
/** Колонка «Исполнитель» в таблице шагов шаблона маршрута. */
|
||||||
export function describeStepExecutor(step: TemplateRouteStep, employees: Employee[]): StepExecutorDisplay {
|
export function describeStepExecutor(step: TemplateRouteStep, employees: Employee[]): StepExecutorDisplay {
|
||||||
if (step.executorEmail) {
|
if (step.executorEmail) {
|
||||||
const employee = findEmployeeByEmail(employees, step.executorEmail);
|
const employee = findEmployeeByEmail(employees, step.executorEmail ?? undefined);
|
||||||
return employee ? { primary: employee.fio, secondary: employee.email } : { primary: step.executorEmail };
|
return employee ? { primary: employee.fio, secondary: employee.email } : { primary: step.executorEmail };
|
||||||
}
|
}
|
||||||
if (step.position) return { primary: step.position };
|
if (step.position) return { primary: step.position };
|
||||||
|
|||||||
Reference in New Issue
Block a user