Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/18
This commit was merged in pull request #18.
This commit is contained in:
@@ -128,6 +128,7 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
|
||||
.matrix {
|
||||
width: 100%;
|
||||
min-width: 480px; // колонки не сжимаются; при нехватке места .matrixScroll прокручивает
|
||||
@@ -355,3 +356,16 @@
|
||||
padding-top: $sp-2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Печать ----
|
||||
// Прокрутка на бумаге недоступна: неуместившееся вбок просто не попало бы на
|
||||
// лист. Обе оси отпускаем — overflow-y здесь стоит только ради анимации
|
||||
// перестановки строк (см. .matrixScroll), а в печати её нет.
|
||||
//
|
||||
// В конце файла намеренно: @media print не повышает специфичность, и любое
|
||||
// базовое правило ниже перебило бы это (см. CostGroupsEditor.module.scss).
|
||||
@media print {
|
||||
.matrixScroll {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { friendlyErrorMessage } from '../diagnostics';
|
||||
import { IMatrixRow } from './models/IMatrixRow';
|
||||
import { ISiteUser } from './models/ISiteUser';
|
||||
|
||||
@@ -175,13 +176,22 @@ export function useMatrixEditor(
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (!active) return;
|
||||
setError(err.message);
|
||||
// Через friendlyErrorMessage, а не err.message: PnPjs кладёт в message
|
||||
// тело ответа SharePoint целиком, и кириллица в нём остаётся
|
||||
// \u-escape-последовательностями — пользователь видел «Поле или
|
||||
// свойство…» набором «Поле».
|
||||
setError(friendlyErrorMessage(err));
|
||||
setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
// load — стабильная ссылка от вызывающего (useCallback); перезагрузка по reloadKey/ready/retrySeq.
|
||||
// load в зависимостях СОЗНАТЕЛЬНО нет: когда матрицу перечитывать —
|
||||
// решает вызывающий через reloadKey (у него это, например, «выбранный
|
||||
// сотрудник + id заявки»), а сам load пересоздаётся и от тех данных,
|
||||
// которые перезагрузки не требуют. С load в массиве маршрут дёргался бы
|
||||
// на каждое такое изменение. Устаревшего замыкания при этом нет: любой
|
||||
// повторный запуск (reloadKey/ready/retrySeq) берёт свежий load.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [reloadKey, ready, retrySeq]);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { friendlyErrorMessage } from '../diagnostics';
|
||||
import { getRequestTypeIdByPageUrl } from './approvalRoute.service';
|
||||
|
||||
export interface IRequestTypeIdState {
|
||||
@@ -33,7 +34,9 @@ export function useRequestTypeId(context: WebPartContext, pageUrl: string): IReq
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (alive) {
|
||||
setError(e.message);
|
||||
// См. useMatrixEditor: сырой message от PnPjs/SharePoint показывает
|
||||
// кириллицу \u-escape-последовательностями.
|
||||
setError(friendlyErrorMessage(e));
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,7 +35,8 @@ const LIST_TITLE_BASE: Record<RequestTypeId, string> = {
|
||||
'recruiting-hire': 'Прием на работу',
|
||||
'recruiting-referral': 'Реферальная рекомендация',
|
||||
'recruiting-selection': 'Заявка на подбор',
|
||||
'wh-tmc-transfer': 'СЗ на перемещение ТМЦ'
|
||||
'wh-tmc-transfer': 'СЗ на перемещение ТМЦ',
|
||||
'wh-service-request': 'Заявка на услуги'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -164,3 +165,11 @@
|
||||
display: flex;
|
||||
gap: $sp-2;
|
||||
}
|
||||
|
||||
// На печати прокрутки не существует: то, что не поместилось по ширине, просто
|
||||
// пропало бы с листа, а контейнер вышел бы с полосой прокрутки.
|
||||
@media print {
|
||||
.tableWrap {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
// Общая машина сохранения заявки для ВСЕХ форм портала.
|
||||
// Отвечает ТОЛЬКО за транзиентное состояние операции (идёт сохранение / ошибка /
|
||||
@@ -15,6 +15,7 @@ export interface IRequestSubmit {
|
||||
/**
|
||||
* Запустить сохранение. validate (опц.) возвращает текст ошибки или undefined.
|
||||
* Повторный вызов во время сохранения игнорируется (защита от дабл-клика).
|
||||
* Ссылка на функцию неизменна — её можно класть в зависимости хуков.
|
||||
*/
|
||||
run: (
|
||||
kind: SubmitKind,
|
||||
@@ -29,13 +30,17 @@ export function useRequestSubmit(): IRequestSubmit {
|
||||
const [kind, setKind] = useState<SubmitKind | undefined>(undefined);
|
||||
const [savedKind, setSavedKind] = useState<SubmitKind | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
// Тот же признак «идёт сохранение», но читаемый синхронно. Нужен, чтобы run
|
||||
// не зависела от состояния и сохраняла неизменную ссылку: формы кладут её
|
||||
// (и весь submit) в зависимости эффектов.
|
||||
const savingRef = useRef<boolean>(false);
|
||||
|
||||
const run = async (
|
||||
const run = useCallback(async (
|
||||
k: SubmitKind,
|
||||
save: () => Promise<void>,
|
||||
validate?: () => string | undefined
|
||||
): Promise<void> => {
|
||||
if (saving) {
|
||||
if (savingRef.current) {
|
||||
return;
|
||||
}
|
||||
setKind(k);
|
||||
@@ -45,6 +50,7 @@ export function useRequestSubmit(): IRequestSubmit {
|
||||
return;
|
||||
}
|
||||
setError(undefined);
|
||||
savingRef.current = true;
|
||||
setSaving(true);
|
||||
try {
|
||||
await save();
|
||||
@@ -52,9 +58,20 @@ export function useRequestSubmit(): IRequestSubmit {
|
||||
} catch (e) {
|
||||
setError(`Ошибка сохранения: ${(e as Error).message}`);
|
||||
} finally {
|
||||
// require-atomic-updates ловит присваивание после await, но здесь это не
|
||||
// гонка: savingRef — не производное состояние, а флаг «занято» с одним
|
||||
// владельцем, и второй вызов run до этой строки просто не проходит.
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
savingRef.current = false;
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { saving, kind, savedKind, error, run, setError };
|
||||
// Объект пересоздаётся только при РЕАЛЬНОЙ смене состояния операции, а не на
|
||||
// каждом рендере формы: run и setError стабильны, поэтому эффекты, которым
|
||||
// нужен только setError, не перезапускаются вовсе.
|
||||
return useMemo(
|
||||
() => ({ saving, kind, savedKind, error, run, setError }),
|
||||
[saving, kind, savedKind, error, run]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useStableCallback } from './useStableCallback';
|
||||
|
||||
// Задержка перед тем, как введённый текст уйдёт в запрос. Без неё поиск
|
||||
// срабатывает на КАЖДОЕ нажатие клавиши: «согласование» — это 12 полных циклов
|
||||
@@ -57,15 +58,17 @@ export function useRoutedQuery(
|
||||
const [input, setInput] = React.useState<string>(routeQuery);
|
||||
const debounced = useDebouncedQuery(input);
|
||||
const pushed = React.useRef<string>(routeQuery);
|
||||
// onQueryChange родитель пересоздаёт на каждом рендере: в зависимостях он
|
||||
// гонял бы эффект вхолостую, а без него эффект навсегда запомнил бы ПЕРВУЮ
|
||||
// версию колбэка (а она замкнута на раздел, открытый при монтировании, — и
|
||||
// запрос уезжал бы не в тот маршрут). Стабильная обёртка снимает и то и другое.
|
||||
const pushQuery = useStableCallback(onQueryChange);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (debounced === pushed.current) return;
|
||||
pushed.current = debounced;
|
||||
onQueryChange(debounced);
|
||||
// onQueryChange пересоздаётся на каждый рендер родителя — в зависимостях
|
||||
// он гонял бы эффект вхолостую; важен только сам факт смены debounced.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debounced]);
|
||||
pushQuery(debounced);
|
||||
}, [debounced, pushQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (routeQuery === pushed.current) return;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useCallback, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Обёртка вокруг колбэка-пропа с НЕИЗМЕННОЙ ссылкой.
|
||||
*
|
||||
* Зачем: колбэк, приходящий сверху пропом (`onCodeLoaded`, `onSaved` и т.п.),
|
||||
* родитель почти всегда пересоздаёт на каждом рендере. Положить такой колбэк
|
||||
* в зависимости useEffect нельзя — эффект начнёт перезапускаться на ровном
|
||||
* месте (для форм портала это означает повторную загрузку заявки с сервера).
|
||||
* Выкинуть его из зависимостей тоже нельзя — эффект зафиксирует ПЕРВУЮ версию
|
||||
* колбэка и после смены пропа будет звать устаревшее замыкание.
|
||||
*
|
||||
* Хук снимает обе проблемы: наружу отдаётся одна и та же функция на всю жизнь
|
||||
* компонента, а внутри она всегда вызывает АКТУАЛЬНУЮ версию колбэка. Такую
|
||||
* обёртку можно честно указывать в зависимостях (см. react-hooks/exhaustive-deps).
|
||||
*
|
||||
* Аналог `useEffectEvent` из будущих версий React; ограничен колбэками без
|
||||
* возвращаемого значения — именно такие у нас в пропах.
|
||||
*
|
||||
* ВАЖНО: вызывать её во время РЕНДЕРА нельзя — только из эффектов и
|
||||
* обработчиков событий, иначе можно прочитать колбэк от прошлого рендера.
|
||||
*/
|
||||
export function useStableCallback<TArgs extends unknown[]>(
|
||||
callback: ((...args: TArgs) => void) | undefined
|
||||
): (...args: TArgs) => void {
|
||||
const ref = useRef(callback);
|
||||
// Обновляем ссылку до того, как отработают обычные (passive) эффекты, —
|
||||
// так вызов из useEffect гарантированно попадает в свежий колбэк.
|
||||
useLayoutEffect(() => {
|
||||
ref.current = callback;
|
||||
});
|
||||
return useCallback((...args: TArgs) => ref.current?.(...args), []);
|
||||
}
|
||||
Reference in New Issue
Block a user