вызов метода взятия партиции перед сабмитом

This commit is contained in:
gorbenko.pavel
2026-08-18 15:05:01 +05:00
parent 5b1a30fdf6
commit 3de50372a0
19 changed files with 301 additions and 106 deletions
@@ -16,6 +16,7 @@ import {
StatusBadge, StatusBadge,
useMatrixEditor, useMatrixEditor,
getPlannedMatrix, getPlannedMatrix,
getCurrentListPartition,
notifyActiveStep, notifyActiveStep,
useRequestTypeId, useRequestTypeId,
REQUEST_STATUS, REQUEST_STATUS,
@@ -189,6 +190,21 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
// requestId уже задан → редактируем существующую заявку, пишем в ту же
// партицию (editListId). Иначе — создаём новую, и партицию для записи
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
// это единственный запрос, который проверяет порог элементов текущего
// списка и ставит в очередь создание следующей партиции при достижении —
// без него архивация никогда не сработает. Сбой запроса не должен
// блокировать подачу заявки — откатываемся на список по конвенции имён.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveClearanceRequest( const { id, code } = await saveClearanceRequest(
requestId, requestId,
rtId, rtId,
@@ -196,7 +212,7 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
toApprovers(matrix.rows), toApprovers(matrix.rows),
toApprovers(executors.rows), toApprovers(executors.rows),
status, status,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
@@ -207,13 +223,14 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда фактически записали (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // не обязательно текущей на момент рендера формы: поток ищет строку
// маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
}); });
} }
}; };
@@ -94,10 +94,13 @@ export interface ISaveResult {
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
* Возвращает Id заявки (+ код при создании). * Возвращает Id заявки (+ код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadClearanceRequest). Имеет смысл * listId — партиция, в которую фактически пишем: при редактировании — партиция
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * ОБНОВЛЯЕМОЙ заявки (см. loadClearanceRequest), при создании — партиция,
* писать новые заявки в архив незачем и нельзя (approval-route переключает * полученная от approval-route API непосредственно перед вызовом (см.
* текущую партицию сам, по своему порогу элементов). * getCurrentListPartition в ClearanceForm.tsx) — именно этот вызов, а не кеш из
* onInit, проверяет порог элементов и запускает архивацию следующей партиции.
* Не задан (сбой запроса за текущей партицией) → откат на список по конвенции
* имён, как до появления партиционирования.
*/ */
export async function saveClearanceRequest( export async function saveClearanceRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -108,8 +111,7 @@ export async function saveClearanceRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const employee = await ensureEmployee(data.Employee); const employee = await ensureEmployee(data.Employee);
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee); const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
@@ -126,6 +128,6 @@ export async function saveClearanceRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(clearanceApprovalService(existingId ? listId : undefined), id, approvers, executors, status); await writeRoute(clearanceApprovalService(listId), id, approvers, executors, status);
return { id, code }; return { id, code };
} }
@@ -20,6 +20,7 @@ import {
StatusBadge, StatusBadge,
useMatrixEditor, useMatrixEditor,
getPlannedMatrix, getPlannedMatrix,
getCurrentListPartition,
notifyActiveStep, notifyActiveStep,
useRequestTypeId, useRequestTypeId,
REQUEST_STATUS, REQUEST_STATUS,
@@ -186,6 +187,21 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
// requestId уже задан → редактируем существующую заявку, пишем в ту же
// партицию (editListId). Иначе — создаём новую, и партицию для записи
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
// это единственный запрос, который проверяет порог элементов текущего
// списка и ставит в очередь создание следующей партиции при достижении —
// без него архивация никогда не сработает. Сбой запроса не должен
// блокировать подачу заявки — откатываемся на список по конвенции имён.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveHireRequest( const { id, code } = await saveHireRequest(
requestId, requestId,
rtId, rtId,
@@ -193,7 +209,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
toApprovers(matrix.rows), toApprovers(matrix.rows),
toApprovers(executors.rows), toApprovers(executors.rows),
status, status,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
@@ -201,21 +217,22 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
setRequestCode(code); setRequestCode(code);
} }
if (files.length > 0) { if (files.length > 0) {
await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); // показать добавленные setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); // показать добавленные
} }
if (onSaved) { if (onSaved) {
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда фактически записали (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // не обязательно текущей на момент рендера формы: поток ищет строку
// маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID), approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID),
}); });
} }
}; };
@@ -55,10 +55,13 @@ export interface ISaveResult {
* executors может быть пустым — тогда после согласования заявка станет «Согласовано». * executors может быть пустым — тогда после согласования заявка станет «Согласовано».
* Возвращает Id заявки (+ код при создании). * Возвращает Id заявки (+ код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadHireRequest). Имеет смысл * listId — партиция, в которую фактически пишем: при редактировании — партиция
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * ОБНОВЛЯЕМОЙ заявки (см. loadHireRequest), при создании — партиция, полученная
* писать новые заявки в архив незачем и нельзя (approval-route переключает * от approval-route API непосредственно перед вызовом (см. getCurrentListPartition
* текущую партицию сам, по своему порогу элементов). * в RecruitingHireForm.tsx) — именно этот вызов, а не кеш из onInit, проверяет
* порог элементов и запускает архивацию следующей партиции.
* Не задан (сбой запроса за текущей партицией) → откат на список по конвенции
* имён, как до появления партиционирования.
*/ */
export async function saveHireRequest( export async function saveHireRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -69,8 +72,7 @@ export async function saveHireRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const item = buildItem(data, status); const item = buildItem(data, status);
let id: number; let id: number;
@@ -86,6 +88,6 @@ export async function saveHireRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(hireApprovalService(existingId ? listId : undefined), id, approvers, executors, status); await writeRoute(hireApprovalService(listId), id, approvers, executors, status);
return { id, code }; return { id, code };
} }
@@ -20,6 +20,7 @@ import {
StatusBadge, StatusBadge,
useMatrixEditor, useMatrixEditor,
getPlannedMatrix, getPlannedMatrix,
getCurrentListPartition,
notifyActiveStep, notifyActiveStep,
useRequestTypeId, useRequestTypeId,
REQUEST_STATUS, REQUEST_STATUS,
@@ -160,13 +161,28 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
// requestId уже задан → редактируем существующую заявку, пишем в ту же
// партицию (editListId). Иначе — создаём новую, и партицию для записи
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
// это единственный запрос, который проверяет порог элементов текущего
// списка и ставит в очередь создание следующей партиции при достижении —
// без него архивация никогда не сработает. Сбой запроса не должен
// блокировать подачу заявки — откатываемся на список по конвенции имён.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveSelectionRequest( const { id, code } = await saveSelectionRequest(
requestId, requestId,
rtId, rtId,
data, data,
toApprovers(matrix.rows), toApprovers(matrix.rows),
status, status,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
@@ -174,21 +190,22 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
setRequestCode(code); setRequestCode(code);
} }
if (files.length > 0) { if (files.length > 0) {
await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id));
} }
if (onSaved) { if (onSaved) {
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда фактически записали (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // не обязательно текущей на момент рендера формы: поток ищет строку
// маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
}); });
} }
}; };
@@ -98,10 +98,13 @@ export interface ISaveResult {
* status = «Черновик» (сохранение) или «На согласовании» (отправка). Фазы исполнения * status = «Черновик» (сохранение) или «На согласовании» (отправка). Фазы исполнения
* нет — writeRoute вызывается с пустым списком исполнителей. Возвращает Id (+ код при создании). * нет — writeRoute вызывается с пустым списком исполнителей. Возвращает Id (+ код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest). Имеет смысл * listId — партиция, в которую фактически пишем: при редактировании — партиция
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest), при создании — партиция,
* писать новые заявки в архив незачем и нельзя (approval-route переключает * полученная от approval-route API непосредственно перед вызовом (см.
* текущую партицию сам, по своему порогу элементов). * getCurrentListPartition в RecruitingSelectionForm.tsx) — именно этот вызов,
* а не кеш из onInit, проверяет порог элементов и запускает архивацию
* следующей партиции. Не задан (сбой запроса за текущей партицией) → откат
* на список по конвенции имён, как до появления партиционирования.
*/ */
export async function saveSelectionRequest( export async function saveSelectionRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -111,8 +114,7 @@ export async function saveSelectionRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const item = buildScalarBody(data); const item = buildScalarBody(data);
item.RequestStatus = status; item.RequestStatus = status;
await applyPeople(item, data); await applyPeople(item, data);
@@ -130,6 +132,6 @@ export async function saveSelectionRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(selectionApprovalService(existingId ? listId : undefined), id, approvers, [], status); await writeRoute(selectionApprovalService(listId), id, approvers, [], status);
return { id, code }; return { id, code };
} }
@@ -20,6 +20,7 @@ import {
StatusBadge, StatusBadge,
useMatrixEditor, useMatrixEditor,
getPlannedMatrix, getPlannedMatrix,
getCurrentListPartition,
notifyActiveStep, notifyActiveStep,
useRequestTypeId, useRequestTypeId,
REQUEST_STATUS, REQUEST_STATUS,
@@ -225,12 +226,28 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
? REQUEST_STATUS.onApproval ? REQUEST_STATUS.onApproval
: REQUEST_STATUS.approved; : REQUEST_STATUS.approved;
// requestId уже задан → редактируем существующую заявку, пишем в ту же
// партицию (editListId). Иначе — создаём новую, и партицию для записи
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
// это единственный запрос, который проверяет порог элементов текущего
// списка и ставит в очередь создание следующей партиции при достижении —
// без него архивация никогда не сработает. Сбой запроса не должен
// блокировать подачу заявки — откатываемся на список по конвенции имён.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveTicketRequest( const { id, code } = await saveTicketRequest(
requestId, requestId,
data, data,
toApprovers(matrix.rows), toApprovers(matrix.rows),
finalStatus, finalStatus,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(finalStatus); setRequestStatus(finalStatus);
@@ -240,9 +257,9 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
let attachments = existingAttachments; let attachments = existingAttachments;
if (files.length > 0) { if (files.length > 0) {
await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files);
setFiles([]); setFiles([]);
attachments = await getAttachments(editListId || dataListTitle(FORM_ID), id); attachments = await getAttachments(writeListId || dataListTitle(FORM_ID), id);
setExistingAttachments(attachments); setExistingAttachments(attachments);
} }
@@ -251,12 +268,13 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
} }
if (finalStatus === REQUEST_STATUS.onApproval) { if (finalStatus === REQUEST_STATUS.onApproval) {
// Имена списков — той же партиции, что и сама заявка (editListId), не // Имена списков — той же партиции, куда фактически записали (writeListId),
// текущей: поток ищет строку маршрута именно в НЕЙ. // не обязательно текущей на момент рендера формы: поток ищет строку
// маршрута именно в НЕЙ.
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
}); });
} else if (finalStatus === REQUEST_STATUS.approved) { } else if (finalStatus === REQUEST_STATUS.approved) {
// Согласование не требуется для этого вида работ — сразу передаём в отдел 1С. // Согласование не требуется для этого вида работ — сразу передаём в отдел 1С.
@@ -268,7 +286,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
attachments, attachments,
webAbsoluteUrl: context.pageContext.web.absoluteUrl webAbsoluteUrl: context.pageContext.web.absoluteUrl
}); });
await setPlannerTaskId(id, taskId, editListId); await setPlannerTaskId(id, taskId, writeListId);
} }
}; };
@@ -48,10 +48,13 @@ export interface ISaveResult {
* Фазы исполнения нет: после согласования заявка уходит в Planner, а не исполнителю * Фазы исполнения нет: после согласования заявка уходит в Planner, а не исполнителю
* из матрицы. Возвращает Id заявки (+ код при создании). * из матрицы. Возвращает Id заявки (+ код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadTicketRequest). Имеет смысл * listId — партиция, в которую фактически пишем: при редактировании — партиция
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * ОБНОВЛЯЕМОЙ заявки (см. loadTicketRequest), при создании — партиция,
* писать новые заявки в архив незачем и нельзя (approval-route переключает * полученная от approval-route API непосредственно перед вызовом (см.
* текущую партицию сам, по своему порогу элементов). * getCurrentListPartition в Ticket1CForm.tsx) — именно этот вызов, а не кеш из
* onInit, проверяет порог элементов и запускает архивацию следующей партиции.
* Не задан (сбой запроса за текущей партицией) → откат на список по конвенции
* имён, как до появления партиционирования.
*/ */
export async function saveTicketRequest( export async function saveTicketRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -60,8 +63,7 @@ export async function saveTicketRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const item = buildItem(data, status); const item = buildItem(data, status);
let id: number; let id: number;
@@ -77,7 +79,7 @@ export async function saveTicketRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(ticket1cApprovalService(existingId ? listId : undefined), id, approvers, [], status); await writeRoute(ticket1cApprovalService(listId), id, approvers, [], status);
return { id, code }; return { id, code };
} }
@@ -26,7 +26,8 @@ import {
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
APPROVAL_PHASE, APPROVAL_PHASE,
findUnassignedStep findUnassignedStep,
getCurrentListPartition
} from '../../shared/approval'; } from '../../shared/approval';
import { import {
uploadAttachments, uploadAttachments,
@@ -202,6 +203,17 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
// Новая заявка — резолвим ТЕКУЩУЮ партицию через approval-route (заодно
// выполняется проверка порога и создание следующей архивной партиции при
// необходимости); при редактировании остаёмся в партиции самой заявки.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveTmcTransferRequest( const { id, code } = await saveTmcTransferRequest(
requestId, requestId,
rtId, rtId,
@@ -209,7 +221,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
toApprovers(matrix.rows), toApprovers(matrix.rows),
toApprovers(executors.rows), toApprovers(executors.rows),
status, status,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
@@ -217,21 +229,22 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
setRequestCode(code); setRequestCode(code);
} }
if (files.length > 0) { if (files.length > 0) {
await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files);
setFiles([]); setFiles([]);
setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id));
} }
if (onSaved) { if (onSaved) {
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда только что записали (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // а не editListId: для новой заявки это может быть только что созданная
// текущая партиция, отличная от editListId (который тут undefined).
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
}); });
} }
}; };
@@ -54,10 +54,11 @@ export interface ISaveResult {
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
* Возвращает Id заявки (+ уникальный код при создании). * Возвращает Id заявки (+ уникальный код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadTmcTransferRequest). Имеет смысл * listId — партиция, в которую пишем: для обновления существующей заявки —
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * партиция ЭТОЙ заявки (см. loadTmcTransferRequest), для новой — ТЕКУЩАЯ
* писать новые заявки в архив незачем и нельзя (approval-route переключает * партиция (см. getCurrentListPartition), которую вызывающая сторона резолвит
* текущую партицию сам, по своему порогу элементов). * заранее и передаёт сюда, а не текущая-по-умолчанию: именно на этом резолве
* держится проверка порога и создание следующей архивной партиции.
*/ */
export async function saveTmcTransferRequest( export async function saveTmcTransferRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -68,8 +69,7 @@ export async function saveTmcTransferRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const item = buildItem(data, status); const item = buildItem(data, status);
let id: number; let id: number;
@@ -85,6 +85,6 @@ export async function saveTmcTransferRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(tmcTransferApprovalService(existingId ? listId : undefined), id, approvers, executors, status); await writeRoute(tmcTransferApprovalService(listId), id, approvers, executors, status);
return { id, code }; return { id, code };
} }
@@ -21,7 +21,8 @@ import {
REQUEST_STATUS, REQUEST_STATUS,
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
findUnassignedStep findUnassignedStep,
getCurrentListPartition
} from '../../shared/approval'; } from '../../shared/approval';
import { IRequestFormProps } from '../../models/types'; import { IRequestFormProps } from '../../models/types';
import { import {
@@ -176,13 +177,24 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
// Новая заявка пишется в ТЕКУЩУЮ партицию — запрашиваем её у approval-route
// API непосредственно перед записью (не кеш из onInit): именно этот вызов
// проверяет порог элементов и запускает архивацию следующей партиции.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveUnpaidLeaveRequest( const { id, code } = await saveUnpaidLeaveRequest(
requestId, requestId,
rtId, rtId,
data, data,
toApprovers(matrix.rows), toApprovers(matrix.rows),
status, status,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
@@ -193,13 +205,13 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда фактически записана заявка (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // не текущей: поток ищет строку маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
}); });
} }
}; };
@@ -89,8 +89,7 @@ export async function saveUnpaidLeaveRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const employee = await ensureUser(data.Employee); const employee = await ensureUser(data.Employee);
// Замещающий необязателен: резолвим только если выбран. // Замещающий необязателен: резолвим только если выбран.
const substituteId = data.Substitute ? (await ensureUser(data.Substitute)).Id : undefined; const substituteId = data.Substitute ? (await ensureUser(data.Substitute)).Id : undefined;
@@ -109,6 +108,6 @@ export async function saveUnpaidLeaveRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(unpaidLeaveApprovalService(existingId ? listId : undefined), id, approvers, [], status); await writeRoute(unpaidLeaveApprovalService(listId), id, approvers, [], status);
return { id, code }; return { id, code };
} }
@@ -16,6 +16,7 @@ import {
StatusBadge, StatusBadge,
useMatrixEditor, useMatrixEditor,
getPlannedMatrix, getPlannedMatrix,
getCurrentListPartition,
notifyActiveStep, notifyActiveStep,
useRequestTypeId, useRequestTypeId,
REQUEST_STATUS, REQUEST_STATUS,
@@ -173,7 +174,29 @@ const VacationForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
const { id, code } = await saveVacationRequest(requestId, rtId, data, toApprovers(matrix.rows), status, editListId); // requestId уже задан → редактируем существующую заявку, пишем в ту же
// партицию (editListId). Иначе — создаём новую, и партицию для записи
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
// это единственный запрос, который проверяет порог элементов текущего
// списка и ставит в очередь создание следующей партиции при достижении —
// без него архивация никогда не сработает. Сбой запроса не должен
// блокировать подачу заявки — откатываемся на список по конвенции имён.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveVacationRequest(
requestId,
rtId,
data,
toApprovers(matrix.rows),
status,
writeListId
);
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
if (code) { if (code) {
@@ -183,13 +206,14 @@ const VacationForm: React.FC<IRequestFormProps> = ({
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда фактически записали (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // не обязательно текущей на момент рендера формы: поток ищет строку
// маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
}); });
} }
}; };
@@ -67,10 +67,13 @@ export interface ISaveResult {
* Фазы исполнения у отпуска нет — writeRoute вызывается с пустым списком исполнителей. * Фазы исполнения у отпуска нет — writeRoute вызывается с пустым списком исполнителей.
* Возвращает Id заявки (+ код при создании). * Возвращает Id заявки (+ код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadVacationRequest). Имеет смысл * listId — партиция, в которую фактически пишем: при редактировании — партиция
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * ОБНОВЛЯЕМОЙ заявки (см. loadVacationRequest), при создании — партиция,
* писать новые заявки в архив незачем и нельзя (approval-route переключает * полученная от approval-route API непосредственно перед вызовом (см.
* текущую партицию сам, по своему порогу элементов). * getCurrentListPartition в VacationForm.tsx) — именно этот вызов, а не кеш из
* onInit, проверяет порог элементов и запускает архивацию следующей партиции.
* Не задан (сбой запроса за текущей партицией) → откат на список по конвенции
* имён, как до появления партиционирования.
*/ */
export async function saveVacationRequest( export async function saveVacationRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -80,8 +83,7 @@ export async function saveVacationRequest(
status: string, status: string,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const employee = await ensureEmployee(data.Employee); const employee = await ensureEmployee(data.Employee);
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee); const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
@@ -98,6 +100,6 @@ export async function saveVacationRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(vacationApprovalService(existingId ? listId : undefined), id, approvers, [], status); await writeRoute(vacationApprovalService(listId), id, approvers, [], status);
return { id, code }; return { id, code };
} }
@@ -20,6 +20,7 @@ import {
StatusBadge, StatusBadge,
useMatrixEditor, useMatrixEditor,
getPlannedMatrix, getPlannedMatrix,
getCurrentListPartition,
notifyActiveStep, notifyActiveStep,
useRequestTypeId, useRequestTypeId,
REQUEST_STATUS, REQUEST_STATUS,
@@ -210,6 +211,21 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
if (!rtId) { if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.'); throw new Error('Тип заявки не определён — обновите страницу и повторите.');
} }
// requestId уже задан → редактируем существующую заявку, пишем в ту же
// партицию (editListId). Иначе — создаём новую, и партицию для записи
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
// это единственный запрос, который проверяет порог элементов текущего
// списка и ставит в очередь создание следующей партиции при достижении —
// без него архивация никогда не сработает. Сбой запроса не должен
// блокировать подачу заявки — откатываемся на список по конвенции имён.
let writeListId = editListId;
if (!requestId) {
try {
writeListId = (await getCurrentListPartition(context, rtId)).listId;
} catch (e) {
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
}
}
const { id, code } = await saveWorkplaceRequest( const { id, code } = await saveWorkplaceRequest(
requestId, requestId,
rtId, rtId,
@@ -218,7 +234,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
toApprovers(executors.rows), toApprovers(executors.rows),
status, status,
sourceId, sourceId,
editListId writeListId
); );
setRequestId(id); setRequestId(id);
setRequestStatus(status); setRequestStatus(status);
@@ -226,21 +242,22 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
setRequestCode(code); setRequestCode(code);
} }
if (files.length > 0) { if (files.length > 0) {
await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files);
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); // показать добавленные setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); // показать добавленные
} }
if (onSaved) { if (onSaved) {
onSaved(); onSaved();
} }
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения). // Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
// Имена списков — той же партиции, что и сама заявка (editListId), не текущей: // Имена списков — той же партиции, куда фактически записали (writeListId),
// поток ищет строку маршрута именно в НЕЙ. // не обязательно текущей на момент рендера формы: поток ищет строку
// маршрута именно в НЕЙ.
if (status === REQUEST_STATUS.onApproval) { if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, { await notifyActiveStep(context, approval, id, {
requestTypeId: rtId, requestTypeId: rtId,
dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID), approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID),
}); });
} }
}; };
@@ -74,10 +74,13 @@ export interface ISaveResult {
* Исполнители пишутся сразу — фаза исполнения стартует автоматически после согласования. * Исполнители пишутся сразу — фаза исполнения стартует автоматически после согласования.
* sourceHireId — если заявка создана на основе «прием». Возвращает Id заявки (+ код при создании). * sourceHireId — если заявка создана на основе «прием». Возвращает Id заявки (+ код при создании).
* *
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest). Имеет смысл * listId — партиция, в которую фактически пишем: при редактировании — партиция
* только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — * ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest), при создании — партиция,
* писать новые заявки в архив незачем и нельзя (approval-route переключает * полученная от approval-route API непосредственно перед вызовом (см.
* текущую партицию сам, по своему порогу элементов). * getCurrentListPartition в WorkplaceSetupForm.tsx) — именно этот вызов, а не
* кеш из onInit, проверяет порог элементов и запускает архивацию следующей
* партиции. Не задан (сбой запроса за текущей партицией) → откат на список по
* конвенции имён, как до появления партиционирования.
*/ */
export async function saveWorkplaceRequest( export async function saveWorkplaceRequest(
existingId: number | undefined, existingId: number | undefined,
@@ -89,8 +92,7 @@ export async function saveWorkplaceRequest(
sourceHireId?: number, sourceHireId?: number,
listId?: string listId?: string
): Promise<ISaveResult> { ): Promise<ISaveResult> {
const list = const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const item = buildItem(data, status, sourceHireId); const item = buildItem(data, status, sourceHireId);
let id: number; let id: number;
@@ -106,6 +108,6 @@ export async function saveWorkplaceRequest(
await list.items.getById(id).update({ RequestUniqueCode: code }); await list.items.getById(id).update({ RequestUniqueCode: code });
} }
await writeRoute(workplaceApprovalService(existingId ? listId : undefined), id, approvers, executors, status); await writeRoute(workplaceApprovalService(listId), id, approvers, executors, status);
return { id, code }; return { id, code };
} }
@@ -3,6 +3,7 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
import { APPROVAL_API_CLIENT_ID, getApprovalApiBaseUrl } from '../config'; import { APPROVAL_API_CLIENT_ID, getApprovalApiBaseUrl } from '../config';
import { import {
ApprovalStatus, ApprovalStatus,
ICurrentListPartition,
IApiResponse, IApiResponse,
IRequestType, IRequestType,
IRequestTypeListSeries, IRequestTypeListSeries,
@@ -54,6 +55,30 @@ export async function getListPartitions(context: WebPartContext): Promise<IReque
return body.data ?? []; return body.data ?? [];
} }
/**
* GET /api/request-types/{id}/lists/current — партиция, в которую нужно писать
* НОВУЮ заявку этого типа. Вызывать перед КАЖДЫМ созданием, не кешировать: этот
* запрос заодно проверяет порог элементов на бэкенде и ставит в очередь
* создание следующей партиции при достижении — без него архивация никогда не
* запустится, сколько бы заявок ни создали (bulk-эндпоинт lists, который
* портал и так грузит при onInit, только читает реестр и порог не проверяет).
*/
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;
}
// Кеш на уровне МОДУЛЯ (не useRef/useState) — список типов заявок админ меняет // Кеш на уровне МОДУЛЯ (не useRef/useState) — список типов заявок админ меняет
// редко, а без кеша каждая форма и каждый переход в режим согласующего дёргали // редко, а без кеша каждая форма и каждый переход в режим согласующего дёргали
// бы /api/request-types заново. Живёт до перезагрузки страницы — тот же принцип, // бы /api/request-types заново. Живёт до перезагрузки страницы — тот же принцип,
@@ -14,6 +14,7 @@ export type { IApprovalRow } from './models/IApprovalRow';
export type { export type {
ApprovalStatus, ApprovalStatus,
IListPartition, IListPartition,
ICurrentListPartition,
IRequestType, IRequestType,
IRequestTypeListSeries, IRequestTypeListSeries,
IResolvedRoute, IResolvedRoute,
@@ -21,7 +22,13 @@ export type {
IResolveFieldValue, IResolveFieldValue,
} from './models/IApprovalRouteApi'; } from './models/IApprovalRouteApi';
export { getRequestTypes, getRequestTypeIdByPageUrl, getListPartitions, resolveRoute } from './approvalRoute.service'; export {
getRequestTypes,
getRequestTypeIdByPageUrl,
getListPartitions,
getCurrentListPartition,
resolveRoute
} from './approvalRoute.service';
export { export {
initListPartitions, initListPartitions,
listPartitionsLoaded, listPartitionsLoaded,
@@ -50,6 +50,23 @@ export interface IRequestTypeListSeries {
partitions: IListPartition[]; partitions: IListPartition[];
} }
/**
* GET /api/request-types/{id}/lists/current → { data: ICurrentListPartition }
*
* Партиция, в которую нужно писать НОВУЮ заявку. Не чистый GET на бэкенде —
* этот же запрос проверяет порог элементов и при необходимости ставит в
* очередь создание следующей партиции, поэтому его нужно звать перед КАЖДЫМ
* созданием заявки, а не читать из кеша (см. shared/approval/listPartitions.ts,
* который для этого не подходит — грузится один раз при onInit).
*/
export interface ICurrentListPartition {
requestTypeId: number;
number: number;
siteUrl: string;
listId: string;
title?: string;
}
// Условие видимости шага маршрута (настраивается в админке маршрутов — // Условие видимости шага маршрута (настраивается в админке маршрутов —
// «Показывать шаг только при выполнении условия», сравнение «Поле списка»/ // «Показывать шаг только при выполнении условия», сравнение «Поле списка»/
// «Оператор»/«Значение»). fieldName — внутреннее имя колонки списка (напр. // «Оператор»/«Значение»). fieldName — внутреннее имя колонки списка (напр.