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