pcp-tgu: редактор — стабильные витки, плейсхолдер без плана, черновик без авто-перехода
Вкладка «Планы»: - «Создать черновик на основе плана» больше не перекидывает в редактор: черновик создаётся, выбирается на схеме (тумблер «Черновики» вкл.) и показывается подсказка — оператор сам переходит в «Редактор». Редактор: - Витки больше не пропадают «иногда»: жизненный цикл flightLineData теперь полностью внутри fetch-эффекта на [noradId, mapRange] (сброс + перезагрузка атомарны). Раньше трассу сбрасывал общий reset по ссылке selectedPlan, а перезагружал отдельный эффект — фоновый loadData пересобирал selectedPlan с тем же окном, mapRange по значению не менялся, фетч не перезапускался, витки молча исчезали. - Явный индикатор вместо немой пустоты: «Загрузка трассы…» / «Нет данных трассы на это окно». - После удаления открытого черновика редактор не «всплывал» обратно: guard загрузки включений не тянет режимы по устаревшему draftPlanId, когда план не выбран. - Редактор включений гейтится на наличие плана (план == миссия): без плана — плейсхолдер «Выберите КА и план…». Иначе после удаления черновика вкладка оставалась активной по КА (витки, режим съёмки), хотя редактировать нечего.
This commit is contained in:
@@ -127,6 +127,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [passFrom, setPassFrom] = useState(1);
|
||||
const [passTo, setPassTo] = useState(999);
|
||||
const [flightLineData, setFlightLineData] = useState<FlightLinePoint[]>([]);
|
||||
// Статус загрузки трассы (витков): отличаем «ещё грузится» от «бэк вернул пусто на это окно»,
|
||||
// чтобы при отсутствии данных не было молчаливой пустоты вместо панели витков.
|
||||
const [flightLineLoading, setFlightLineLoading] = useState(false);
|
||||
// Команда карте центрироваться/подсветить объект выбранного включения (клик по включению).
|
||||
const [mapFocus, setMapFocus] = useState<{ point?: GeoPoint; objectId?: string; token: number }>({ token: 0 });
|
||||
const draft = history.draft;
|
||||
@@ -277,15 +280,26 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setPlanModes([]);
|
||||
setPassFrom(1);
|
||||
setPassTo(999);
|
||||
setFlightLineData([]);
|
||||
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
|
||||
|
||||
// Линия трассы (витки) живёт исключительно по своим входам запроса — noradId и mapRange.
|
||||
// НЕ сбрасываем её в общем reset-эффекте по ссылке selectedPlan: фоновый loadData (после
|
||||
// решения/создания черновика/обновления интервала) пересобирает объект selectedPlan с теми же
|
||||
// временами окна, mapRange по значению не меняется, и фетч бы не перезапустился — витки
|
||||
// молча пропадали бы до реальной смены окна. Привязка к [noradId, mapRange] делает это
|
||||
// идемпотентным: сброс и перезагрузка происходят вместе и только когда меняется само окно/КА.
|
||||
useEffect(() => {
|
||||
if (!noradId) return;
|
||||
if (!noradId) {
|
||||
setFlightLineData([]);
|
||||
setFlightLineLoading(false);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setFlightLineLoading(true);
|
||||
void fetchFlightLine(noradId, mapRange.fromMs, mapRange.toMs)
|
||||
.then((data) => { if (!cancelled) setFlightLineData(data); })
|
||||
.catch(() => { if (!cancelled) setFlightLineData([]); });
|
||||
.catch(() => { if (!cancelled) setFlightLineData([]); })
|
||||
.finally(() => { if (!cancelled) setFlightLineLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [noradId, mapRange]);
|
||||
|
||||
@@ -464,8 +478,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
|
||||
// Источник истины для включений — DRAFT, если он уже создан, иначе живой план (read-only превью).
|
||||
useEffect(() => {
|
||||
const missionId = draftPlanId ?? selectedPlan?.planId;
|
||||
if (!missionId) return;
|
||||
// Нет выбранного плана (например, открытый черновик удалили) — чистим включения и НЕ тянем
|
||||
// режимы по устаревшему draftPlanId. Он обнулится следующим коммитом (эффект выше), но в этом
|
||||
// ещё указывает на удалённый черновик; без гарда fetchPlanModes(удалённый id) вернул бы его
|
||||
// включения обратно — редактор «не чистился» бы после удаления.
|
||||
if (!selectedPlan) {
|
||||
setPlanModes([]);
|
||||
return;
|
||||
}
|
||||
const missionId = draftPlanId ?? selectedPlan.planId;
|
||||
let cancelled = false;
|
||||
void fetchPlanModes(missionId).then((modes) => {
|
||||
if (!cancelled) setPlanModes(modes);
|
||||
@@ -895,10 +916,13 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
);
|
||||
};
|
||||
|
||||
if (!selectedSpacecraftId && !selectedPlan) {
|
||||
// Редактор включений работает только при выбранном плане (план == миссия, в которую кладутся
|
||||
// включения). Без плана — плейсхолдер: иначе после удаления открытого черновика вкладка
|
||||
// оставалась бы «активной» по выбранному КА (витки, режим съёмки), хотя редактировать нечего.
|
||||
if (!selectedPlan) {
|
||||
return (
|
||||
<main className="tgu-editor-workspace">
|
||||
<div className="tgu-empty">Выберите КА или план, чтобы открыть редактор включений.</div>
|
||||
<div className="tgu-empty">Выберите КА и план, чтобы открыть редактор включений.</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -969,7 +993,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
>
|
||||
<div className="tgu-editor-map-stage">
|
||||
{rightMode === "shoot" && <div className="tgu-map-overlay">Кликните точку съёмки на карте.</div>}
|
||||
{orbitPasses.length > 0 && (
|
||||
{orbitPasses.length > 0 ? (
|
||||
<MapPassSelector
|
||||
passes={orbitPasses}
|
||||
fromIndex={effectiveFrom}
|
||||
@@ -977,7 +1001,14 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
onFromChange={setPassFrom}
|
||||
onToChange={setPassTo}
|
||||
/>
|
||||
)}
|
||||
) : noradId ? (
|
||||
<div className="map-pass-selector map-pass-selector--empty">
|
||||
<span className="map-pass-selector__title">Витки</span>
|
||||
<span className="map-pass-selector__empty-note">
|
||||
{flightLineLoading ? "Загрузка трассы…" : "Нет данных трассы на это окно"}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<Tgu2DMapView
|
||||
scene={mapScene}
|
||||
layers={EDITOR_MAP_LAYERS}
|
||||
|
||||
@@ -301,13 +301,17 @@ export function TguPlanningPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Создать черновик-копию на основе существующего плана (replace, окно 1:1, все включения) и
|
||||
// открыть его в редакторе. Это путь «сделать исправленный план», а не повторная отправка того же.
|
||||
// Создать черновик-копию на основе существующего плана (replace, окно 1:1, все включения).
|
||||
// Это путь «сделать исправленный план», а не повторная отправка того же. В редактор сами
|
||||
// не уходим — оператор выберет черновик и перейдёт в «Редактор» вручную.
|
||||
const handleCreateDraftFromPlan = async (planId: string) => {
|
||||
// Один открытый черновик на базу: если он уже есть — открываем его, второй не плодим.
|
||||
// Один открытый черновик на базу: если он уже есть — выбираем его, второй не плодим.
|
||||
const existing = drafts.find((item) => item.basePlanId === planId);
|
||||
if (existing) {
|
||||
handleEditDraft(existing.planId);
|
||||
setShowDrafts(true);
|
||||
setSelectedSpacecraftId(existing.spacecraftId);
|
||||
setSelectedPlanId(existing.planId);
|
||||
setDecisionNotice("Черновик на основе этого плана уже создан — выбран на схеме.");
|
||||
return;
|
||||
}
|
||||
setDraftActionInFlight(true);
|
||||
@@ -320,7 +324,7 @@ export function TguPlanningPage() {
|
||||
setShowDrafts(true);
|
||||
if (base) setSelectedSpacecraftId(base.spacecraftId);
|
||||
setSelectedPlanId(draftPlanId);
|
||||
setActiveTab("editor");
|
||||
setDecisionNotice("Черновик создан и выбран на схеме. Откройте «Редактор», чтобы поправить и отправить его.");
|
||||
} catch (error) {
|
||||
setDecisionError(error instanceof Error ? error.message : "Не удалось создать черновик на основе плана.");
|
||||
} finally {
|
||||
|
||||
@@ -308,6 +308,20 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.map-pass-selector--empty {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.65rem;
|
||||
width: auto;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.map-pass-selector__empty-note {
|
||||
color: var(--text-muted, #98a2ac);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.map-pass-selector__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
Reference in New Issue
Block a user