fix(tgu-ui): подхватывать существующий черновик вместо ошибки «Conflict» при добавлении съёмки
При первой правке плана ensureDraft→createDraft(replace) падал 409, если по КА уже есть открытый черновик на это окно (осиротевший с прошлой сессии — авто-отмена best-effort). В UI это показывалось голым «Conflict» (на стенде не подхвачен include-message у tgu). Теперь на 409 от createDraft редактор подхватывает существующий черновик того же КА с тем же окном (adoptExistingDraft через fetchDraftPlans, сверка окна ±2 c) и продолжает в нём; если подходящего нет — читаемое сообщение со ссылкой на вкладку «Планы» вместо «Conflict».
This commit is contained in:
@@ -15,7 +15,9 @@ import {
|
|||||||
validateMission,
|
validateMission,
|
||||||
type SurveyRoute
|
type SurveyRoute
|
||||||
} from "../../api/missionPlanningApi";
|
} from "../../api/missionPlanningApi";
|
||||||
import { abandonDraft, createDraft } from "../../api/tguPlanApi";
|
import { abandonDraft, createDraft, fetchDraftPlans } from "../../api/tguPlanApi";
|
||||||
|
import { TguApiError } from "../../model/tguTypes";
|
||||||
|
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||||
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
||||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||||
@@ -627,6 +629,19 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
// Гарантирует наличие черновика: при первой правке клонирует текущий план в DRAFT (replace, тот же
|
// Гарантирует наличие черновика: при первой правке клонирует текущий план в DRAFT (replace, тот же
|
||||||
// слот) и возвращает его id. Дальше все правки/валидация/promote адресуются именно сюда. Создание
|
// слот) и возвращает его id. Дальше все правки/валидация/promote адресуются именно сюда. Создание
|
||||||
// плана на другую ЗРВ / новой цепочки вынесено во вкладку «Планы».
|
// плана на другую ЗРВ / новой цепочки вынесено во вкладку «Планы».
|
||||||
|
// Ищет уже открытый DRAFT этого КА с тем же окном, что у базового плана (replace 1:1 совпадает
|
||||||
|
// по окну). Используется при 409 от createDraft, чтобы подхватить осиротевший черновик.
|
||||||
|
const adoptExistingDraft = useCallback(async (basePlan: TguPlanUi): Promise<string | undefined> => {
|
||||||
|
if (!basePlan.spacecraftId) return undefined;
|
||||||
|
const drafts = await fetchDraftPlans(basePlan.spacecraftId).catch(() => [] as Awaited<ReturnType<typeof fetchDraftPlans>>);
|
||||||
|
const match = drafts.find(
|
||||||
|
(draft) =>
|
||||||
|
Math.abs(parseLocal(draft.startTime) - basePlan.startMs) < 2000 &&
|
||||||
|
Math.abs(parseLocal(draft.endTime) - basePlan.endMs) < 2000
|
||||||
|
);
|
||||||
|
return match?.planId;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const ensureDraft = useCallback(async (): Promise<string> => {
|
const ensureDraft = useCallback(async (): Promise<string> => {
|
||||||
if (draftPlanId) return draftPlanId;
|
if (draftPlanId) return draftPlanId;
|
||||||
if (!selectedPlan) throw new Error("План не выбран — нечего редактировать.");
|
if (!selectedPlan) throw new Error("План не выбран — нечего редактировать.");
|
||||||
@@ -635,11 +650,32 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
setDraftPlanId(selectedPlan.planId);
|
setDraftPlanId(selectedPlan.planId);
|
||||||
return selectedPlan.planId;
|
return selectedPlan.planId;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const created = await createDraft(selectedPlan.planId, { mode: "replace" });
|
const created = await createDraft(selectedPlan.planId, { mode: "replace" });
|
||||||
createdImplicitlyRef.current = true;
|
createdImplicitlyRef.current = true;
|
||||||
setDraftPlanId(created.draftPlanId);
|
setDraftPlanId(created.draftPlanId);
|
||||||
return created.draftPlanId;
|
return created.draftPlanId;
|
||||||
}, [draftPlanId, selectedPlan]);
|
} catch (error) {
|
||||||
|
// 409: по этому КА уже открыт черновик на это же окно (осиротевший после прошлой сессии —
|
||||||
|
// авто-отмена при уходе best-effort и могла не отработать). Вместо тупиковой ошибки
|
||||||
|
// «Conflict» подхватываем существующий черновик того же окна и правим его (replace 1:1 —
|
||||||
|
// окно совпадает с базой). Не нашли подходящий — пробрасываем читаемую ошибку.
|
||||||
|
if (error instanceof TguApiError && error.status === 409) {
|
||||||
|
const adopted = await adoptExistingDraft(selectedPlan);
|
||||||
|
if (adopted) {
|
||||||
|
createdImplicitlyRef.current = true;
|
||||||
|
setDraftPlanId(adopted);
|
||||||
|
return adopted;
|
||||||
|
}
|
||||||
|
throw new TguApiError(
|
||||||
|
"По этому КА уже есть открытый черновик на это окно. Отмените его во вкладке «Планы» (раздел «Черновики») или продолжите редактирование там.",
|
||||||
|
"http",
|
||||||
|
409,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [draftPlanId, selectedPlan, adoptExistingDraft]);
|
||||||
|
|
||||||
// Бэкенд-валидация черновика (Ф4): авторитетные конфликты по режимам → карта по workId.
|
// Бэкенд-валидация черновика (Ф4): авторитетные конфликты по режимам → карта по workId.
|
||||||
// MVP-заглушка возвращает пусто; ошибки валидации не эскалируем в основной поток правок.
|
// MVP-заглушка возвращает пусто; ошибки валидации не эскалируем в основной поток правок.
|
||||||
|
|||||||
Reference in New Issue
Block a user