feat: Корректировки 07.11.25
This commit is contained in:
@@ -420,7 +420,15 @@ export const RouteCreatePage = observer(() => {
|
||||
type="number"
|
||||
value={scaleMin}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
let value = e.target.value;
|
||||
if (Number(value) > 297) {
|
||||
value = "297";
|
||||
}
|
||||
|
||||
if (Number(value) < 10) {
|
||||
value = "10";
|
||||
}
|
||||
|
||||
setScaleMin(value);
|
||||
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
||||
setScaleMax(value);
|
||||
@@ -447,6 +455,10 @@ export const RouteCreatePage = observer(() => {
|
||||
value={scaleMax}
|
||||
required
|
||||
onChange={(e) => {
|
||||
if (Number(e.target.value) > 300) {
|
||||
e.target.value = "300";
|
||||
}
|
||||
|
||||
const value = e.target.value;
|
||||
setScaleMax(value);
|
||||
}}
|
||||
|
||||
@@ -393,20 +393,29 @@ export const RouteEditPage = observer(() => {
|
||||
type="number"
|
||||
value={editRouteData.scale_min ?? ""}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
e.target.value === "" ? null : parseFloat(e.target.value);
|
||||
let value = e.target.value === "" ? null : e.target.value;
|
||||
|
||||
if (value && Number(value) > 297) {
|
||||
value = "297";
|
||||
}
|
||||
|
||||
if (value && Number(value) < 10) {
|
||||
value = "10";
|
||||
}
|
||||
|
||||
routeStore.setEditRouteData({
|
||||
scale_min: value,
|
||||
scale_min: value ? Number(value) : null,
|
||||
});
|
||||
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||
if (
|
||||
value !== null &&
|
||||
editRouteData.scale_max !== null &&
|
||||
editRouteData.scale_max !== undefined &&
|
||||
value > editRouteData.scale_max
|
||||
value &&
|
||||
Number(value) > (editRouteData.scale_max ?? 0)
|
||||
) {
|
||||
routeStore.setEditRouteData({
|
||||
scale_max: value,
|
||||
scale_max: value ? Number(value) : null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -418,12 +427,17 @@ export const RouteEditPage = observer(() => {
|
||||
label="Масштаб (макс)"
|
||||
type="number"
|
||||
value={editRouteData.scale_max ?? ""}
|
||||
onChange={(e) =>
|
||||
routeStore.setEditRouteData({
|
||||
scale_max:
|
||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||
})
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
|
||||
if (Number(value) > 300) {
|
||||
value = "300";
|
||||
}
|
||||
|
||||
routeStore.setEditRouteData({
|
||||
scale_max: value === "" ? null : parseFloat(value),
|
||||
});
|
||||
}}
|
||||
error={
|
||||
editRouteData.scale_min !== null &&
|
||||
editRouteData.scale_min !== undefined &&
|
||||
|
||||
@@ -145,8 +145,8 @@ export function RightSidebar() {
|
||||
onChange={(e) => {
|
||||
let newMinScale = Number(e.target.value);
|
||||
|
||||
if (newMinScale < 1) {
|
||||
newMinScale = 1;
|
||||
if (newMinScale < 10) {
|
||||
newMinScale = 10;
|
||||
}
|
||||
|
||||
setMinScale(newMinScale);
|
||||
@@ -189,8 +189,8 @@ export function RightSidebar() {
|
||||
onChange={(e) => {
|
||||
let newMaxScale = Number(e.target.value);
|
||||
|
||||
if (newMaxScale < 3) {
|
||||
newMaxScale = 3;
|
||||
if (newMaxScale < 13) {
|
||||
newMaxScale = 13;
|
||||
}
|
||||
|
||||
if (newMaxScale > 300) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { Widgets } from "./Widgets";
|
||||
import { Application, extend } from "@pixi/react";
|
||||
import { extend } from "@pixi/react";
|
||||
import {
|
||||
Container,
|
||||
Graphics,
|
||||
@@ -12,22 +12,15 @@ import {
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||
import { TransformProvider, useTransform } from "./TransformContext";
|
||||
import { InfiniteCanvas } from "./InfiniteCanvas";
|
||||
|
||||
import { TravelPath } from "./TravelPath";
|
||||
import { LeftSidebar } from "./LeftSidebar";
|
||||
import { RightSidebar } from "./RightSidebar";
|
||||
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { languageStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Sight } from "./Sight";
|
||||
import { SightData } from "./types";
|
||||
import { Station } from "./Station";
|
||||
import { UP_SCALE } from "./Constants";
|
||||
import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
|
||||
extend({
|
||||
Container,
|
||||
@@ -43,7 +36,7 @@ const Loading = () => {
|
||||
|
||||
if (isRouteLoading || isStationLoading || isSightLoading) {
|
||||
return (
|
||||
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
|
||||
<div className="fixed flex z-1000000000 items-center justify-center h-screen w-screen bg-[#111]">
|
||||
<CircularProgress />
|
||||
</div>
|
||||
);
|
||||
@@ -91,15 +84,8 @@ export const RoutePreview = () => {
|
||||
};
|
||||
|
||||
export const RouteMap = observer(() => {
|
||||
const { language } = languageStore;
|
||||
const { setPosition, setTransform, screenCenter } = useTransform();
|
||||
const {
|
||||
routeData,
|
||||
stationData,
|
||||
sightData,
|
||||
originalRouteData,
|
||||
originalSightData,
|
||||
} = useMapData();
|
||||
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||
|
||||
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||
const [isSetup, setIsSetup] = useState(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ReactElement } from "react";
|
||||
import { useEffect, useRef, useState, type ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { languageStore } from "@shared";
|
||||
|
||||
@@ -114,7 +114,7 @@ const ArrowIcon = ({ rotation }: { rotation: number }) => (
|
||||
|
||||
const LanguageSelector = observer(
|
||||
({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => {
|
||||
const { language, setLanguage } = languageStore;
|
||||
const { setLanguage } = languageStore;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -145,20 +145,6 @@ const LanguageSelector = observer(
|
||||
|
||||
const toggle = () => setIsOpen((prev) => !prev);
|
||||
|
||||
const containerWidth = useMemo(() => {
|
||||
const BUTTON_SIZE = 56;
|
||||
const GAP = 8;
|
||||
const backWidth = onBack ? BUTTON_SIZE + GAP : 0;
|
||||
const toggleWidth = BUTTON_SIZE;
|
||||
const collapsedWidth = backWidth + toggleWidth + BUTTON_SIZE;
|
||||
const expandedWidth =
|
||||
backWidth +
|
||||
toggleWidth +
|
||||
LANGUAGES.length * BUTTON_SIZE +
|
||||
(LANGUAGES.length - 1) * GAP;
|
||||
return isOpen ? expandedWidth : collapsedWidth;
|
||||
}, [isOpen, onBack]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -370,6 +370,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
setSelectedSight,
|
||||
setStationOffset,
|
||||
setSightCoordinates,
|
||||
setMapCenter,
|
||||
} = useMapData();
|
||||
const { language } = languageStore;
|
||||
const { setScale: setSharedScale, scale: sharedScale } = useTransform();
|
||||
@@ -446,6 +447,12 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}>({ latitude: null, longitude: null });
|
||||
const pendingCenterRef = useRef<{
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
} | null>(null);
|
||||
const isUserInteractingRef = useRef(false);
|
||||
const commitCenterTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
const getRelativePointerPosition = useCallback(
|
||||
(clientX: number, clientY: number) => {
|
||||
@@ -532,6 +539,58 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
[rotationAngle]
|
||||
);
|
||||
|
||||
const cancelScheduledCenterCommit = useCallback(() => {
|
||||
if (commitCenterTimeoutRef.current !== null) {
|
||||
window.clearTimeout(commitCenterTimeoutRef.current);
|
||||
commitCenterTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const commitCenter = useCallback(() => {
|
||||
const center = lastCenterRef.current;
|
||||
if (
|
||||
!center ||
|
||||
center.latitude == null ||
|
||||
center.longitude == null ||
|
||||
!Number.isFinite(center.latitude) ||
|
||||
!Number.isFinite(center.longitude)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const epsilon = 1e-7;
|
||||
const prev = lastAppliedCenterRef.current;
|
||||
if (
|
||||
prev.latitude != null &&
|
||||
prev.longitude != null &&
|
||||
Math.abs(prev.latitude - center.latitude) < epsilon &&
|
||||
Math.abs(prev.longitude - center.longitude) < epsilon
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastAppliedCenterRef.current = {
|
||||
latitude: center.latitude,
|
||||
longitude: center.longitude,
|
||||
};
|
||||
setMapCenter(center.latitude, center.longitude);
|
||||
}, [setMapCenter]);
|
||||
|
||||
const scheduleCenterCommit = useCallback(() => {
|
||||
cancelScheduledCenterCommit();
|
||||
commitCenterTimeoutRef.current = window.setTimeout(() => {
|
||||
commitCenterTimeoutRef.current = null;
|
||||
isUserInteractingRef.current = false;
|
||||
commitCenter();
|
||||
}, 120);
|
||||
}, [cancelScheduledCenterCommit, commitCenter]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelScheduledCenterCommit();
|
||||
};
|
||||
}, [cancelScheduledCenterCommit]);
|
||||
|
||||
const updateTransform = useCallback(
|
||||
(next: Transform) => {
|
||||
const adjusted = clampTransformScale(next);
|
||||
@@ -1026,6 +1085,39 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
max: baseScale * 16,
|
||||
};
|
||||
}
|
||||
const centerLat =
|
||||
routeData?.center_latitude ?? originalRouteData?.center_latitude;
|
||||
const centerLon =
|
||||
routeData?.center_longitude ?? originalRouteData?.center_longitude;
|
||||
if (
|
||||
Number.isFinite(centerLat) &&
|
||||
Number.isFinite(centerLon) &&
|
||||
canvas.width > 0 &&
|
||||
canvas.height > 0
|
||||
) {
|
||||
const local = coordinatesToLocal(
|
||||
centerLat as number,
|
||||
centerLon as number
|
||||
);
|
||||
const baseX = local.x * UP_SCALE;
|
||||
const baseY = local.y * UP_SCALE;
|
||||
const cos = Math.cos(rotationAngle);
|
||||
const sin = Math.sin(rotationAngle);
|
||||
const rotatedX = baseX * cos - baseY * sin;
|
||||
const rotatedY = baseX * sin + baseY * cos;
|
||||
const scale = transform.scale || 1;
|
||||
transform = {
|
||||
scale,
|
||||
translation: {
|
||||
x: canvas.width / 2 - rotatedX * scale,
|
||||
y: canvas.height / 2 - rotatedY * scale,
|
||||
},
|
||||
};
|
||||
lastAppliedCenterRef.current = {
|
||||
latitude: centerLat as number,
|
||||
longitude: centerLon as number,
|
||||
};
|
||||
}
|
||||
transform = clampTransformScale(transform);
|
||||
updateTransform(transform);
|
||||
} else {
|
||||
@@ -1260,8 +1352,75 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
latitude: roundedLat,
|
||||
longitude: roundedLon,
|
||||
};
|
||||
|
||||
if (isUserInteractingRef.current) {
|
||||
pendingCenterRef.current = {
|
||||
latitude: roundedLat,
|
||||
longitude: roundedLon,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const transform =
|
||||
transformRef.current ?? lastTransformRef.current ?? transformState;
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas || !transform) {
|
||||
pendingCenterRef.current = {
|
||||
latitude: roundedLat,
|
||||
longitude: roundedLon,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const width = canvas.width || canvas.clientWidth;
|
||||
const height = canvas.height || canvas.clientHeight;
|
||||
if (!width || !height) {
|
||||
pendingCenterRef.current = {
|
||||
latitude: roundedLat,
|
||||
longitude: roundedLon,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const local = coordinatesToLocal(roundedLat, roundedLon);
|
||||
const baseX = local.x * UP_SCALE;
|
||||
const baseY = local.y * UP_SCALE;
|
||||
|
||||
const cos = Math.cos(rotationAngle);
|
||||
const sin = Math.sin(rotationAngle);
|
||||
const rotatedX = baseX * cos - baseY * sin;
|
||||
const rotatedY = baseX * sin + baseY * cos;
|
||||
|
||||
const scale = transform.scale || 1;
|
||||
const targetTranslation = {
|
||||
x: width / 2 - rotatedX * scale,
|
||||
y: height / 2 - rotatedY * scale,
|
||||
};
|
||||
|
||||
const currentTranslation = transform.translation;
|
||||
const distance = Math.hypot(
|
||||
targetTranslation.x - currentTranslation.x,
|
||||
targetTranslation.y - currentTranslation.y
|
||||
);
|
||||
|
||||
if (distance < 0.5) {
|
||||
pendingCenterRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTransform: Transform = {
|
||||
scale,
|
||||
translation: targetTranslation,
|
||||
};
|
||||
|
||||
transformRef.current = nextTransform;
|
||||
lastTransformRef.current = nextTransform;
|
||||
setTransformState(nextTransform);
|
||||
drawSceneRef.current();
|
||||
pendingCenterRef.current = null;
|
||||
},
|
||||
[]
|
||||
[rotationAngle, setTransformState, transformState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1290,6 +1449,18 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
applyCenterFromCoordinates,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingCenterRef.current || !transformRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { latitude, longitude } = pendingCenterRef.current;
|
||||
pendingCenterRef.current = null;
|
||||
window.requestAnimationFrame(() => {
|
||||
applyCenterFromCoordinates(latitude, longitude);
|
||||
});
|
||||
}, [transformState, applyCenterFromCoordinates]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
@@ -1333,6 +1504,8 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
isUserInteractingRef.current = true;
|
||||
cancelScheduledCenterCommit();
|
||||
const position = getEventPosition(event);
|
||||
activePointersRef.current.set(event.pointerId, position);
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
@@ -1420,6 +1593,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
dragStateRef.current = null;
|
||||
pinchStateRef.current = null;
|
||||
canvas.style.cursor = "grab";
|
||||
scheduleCenterCommit();
|
||||
} else if (activePointersRef.current.size === 1) {
|
||||
const remaining = Array.from(activePointersRef.current.values())[0];
|
||||
dragStateRef.current = { lastPos: remaining };
|
||||
@@ -1432,6 +1606,8 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
event.preventDefault();
|
||||
const transform = transformRef.current;
|
||||
if (!transform) return;
|
||||
isUserInteractingRef.current = true;
|
||||
cancelScheduledCenterCommit();
|
||||
|
||||
const position = getEventPosition(event);
|
||||
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
@@ -1459,6 +1635,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
},
|
||||
});
|
||||
drawSceneRef.current();
|
||||
scheduleCenterCommit();
|
||||
};
|
||||
|
||||
canvas.addEventListener("pointerdown", handlePointerDown);
|
||||
@@ -1476,7 +1653,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
||||
canvas.removeEventListener("pointerleave", handlePointerUp);
|
||||
canvas.removeEventListener("wheel", handleWheel as EventListener);
|
||||
};
|
||||
}, []);
|
||||
}, [cancelScheduledCenterCommit, scheduleCenterCommit, updateTransform]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -222,33 +222,55 @@ const LinkedStationsContentsInner = <
|
||||
setSelectedItems(updated);
|
||||
};
|
||||
|
||||
const handleBulkLink = () => {
|
||||
const handleBulkLink = async () => {
|
||||
if (selectedItems.size === 0) return;
|
||||
setError(null);
|
||||
|
||||
setIsLinkingBulk(true);
|
||||
Promise.all(
|
||||
Array.from(selectedItems).map((id) =>
|
||||
authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
const idsToLink = Array.from(selectedItems);
|
||||
const linkedIds: number[] = [];
|
||||
const failedIds: number[] = [];
|
||||
|
||||
for (const id of idsToLink) {
|
||||
try {
|
||||
await authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
station_id: id,
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
const newItems = allItems.filter((item) =>
|
||||
selectedItems.has(item.id)
|
||||
);
|
||||
setLinkedItems([...linkedItems, ...newItems]);
|
||||
setSelectedItems(new Set());
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error bulk linking stations:", error);
|
||||
setError("Failed to link stations");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLinkingBulk(false);
|
||||
});
|
||||
linkedIds.push(id);
|
||||
} catch (error) {
|
||||
console.error("Error linking station:", error);
|
||||
failedIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (linkedIds.length > 0) {
|
||||
const newItems = allItems.filter((item) => linkedIds.includes(item.id));
|
||||
setLinkedItems((prev) => {
|
||||
const existingIds = new Set(prev.map((item) => item.id));
|
||||
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||
return [...prev, ...additions];
|
||||
});
|
||||
onUpdate?.();
|
||||
}
|
||||
|
||||
setSelectedItems((prev) => {
|
||||
if (linkedIds.length === 0) {
|
||||
return prev;
|
||||
}
|
||||
const remaining = new Set(prev);
|
||||
linkedIds.forEach((id) => remaining.delete(id));
|
||||
return failedIds.length > 0 ? remaining : new Set();
|
||||
});
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
setError(
|
||||
failedIds.length === idsToLink.length
|
||||
? "Failed to link stations"
|
||||
: "Some stations failed to link"
|
||||
);
|
||||
}
|
||||
|
||||
setIsLinkingBulk(false);
|
||||
};
|
||||
|
||||
const toggleDetachSelection = (itemId: number) => {
|
||||
@@ -269,7 +291,7 @@ const LinkedStationsContentsInner = <
|
||||
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||
};
|
||||
|
||||
const handleBulkDetach = () => {
|
||||
const handleBulkDetach = async () => {
|
||||
const idsToDetach = Array.from(selectedToDetach);
|
||||
if (idsToDetach.length === 0) return;
|
||||
setError(null);
|
||||
@@ -281,32 +303,47 @@ const LinkedStationsContentsInner = <
|
||||
return next;
|
||||
});
|
||||
|
||||
Promise.all(
|
||||
idsToDetach.map((itemId) =>
|
||||
authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
const detachedIds: number[] = [];
|
||||
const failedIds: number[] = [];
|
||||
|
||||
for (const itemId of idsToDetach) {
|
||||
try {
|
||||
await authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
setLinkedItems(
|
||||
linkedItems.filter((item) => !idsToDetach.includes(item.id))
|
||||
});
|
||||
detachedIds.push(itemId);
|
||||
} catch (error) {
|
||||
console.error("Error deleting station:", error);
|
||||
failedIds.push(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
if (detachedIds.length > 0) {
|
||||
setLinkedItems((prev) =>
|
||||
prev.filter((item) => !detachedIds.includes(item.id))
|
||||
);
|
||||
setSelectedToDetach(new Set());
|
||||
setSelectedToDetach((prev) => {
|
||||
const remaining = new Set(prev);
|
||||
detachedIds.forEach((id) => remaining.delete(id));
|
||||
return failedIds.length > 0 ? remaining : new Set();
|
||||
});
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error bulk deleting stations:", error);
|
||||
setError("Failed to delete stations");
|
||||
})
|
||||
.finally(() => {
|
||||
}
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
setError(
|
||||
failedIds.length === idsToDetach.length
|
||||
? "Failed to delete stations"
|
||||
: "Some stations failed to delete"
|
||||
);
|
||||
}
|
||||
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
idsToDetach.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
setIsBulkDetaching(false);
|
||||
});
|
||||
};
|
||||
|
||||
const allSelectedForDetach =
|
||||
|
||||
@@ -223,33 +223,58 @@ const LinkedSightsContentsInner = <
|
||||
setSelectedItems(updated);
|
||||
};
|
||||
|
||||
const handleBulkLink = () => {
|
||||
const handleBulkLink = async () => {
|
||||
if (selectedItems.size === 0) return;
|
||||
setError(null);
|
||||
|
||||
setIsLinkingBulk(true);
|
||||
Promise.all(
|
||||
Array.from(selectedItems).map((id) =>
|
||||
authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
const idsToLink = Array.from(selectedItems);
|
||||
const linkedIds: number[] = [];
|
||||
const failedIds: number[] = [];
|
||||
|
||||
for (const id of idsToLink) {
|
||||
try {
|
||||
await authInstance.post(
|
||||
`/${parentResource}/${parentId}/${childResource}`,
|
||||
{
|
||||
sight_id: id,
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
const newItems = allItems.filter((item) =>
|
||||
selectedItems.has(item.id)
|
||||
}
|
||||
);
|
||||
setLinkedItems([...linkedItems, ...newItems]);
|
||||
setSelectedItems(new Set());
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error bulk linking sights:", error);
|
||||
setError("Failed to link sights");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLinkingBulk(false);
|
||||
linkedIds.push(id);
|
||||
} catch (error) {
|
||||
console.error("Error linking sight:", error);
|
||||
failedIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (linkedIds.length > 0) {
|
||||
const newItems = allItems.filter((item) => linkedIds.includes(item.id));
|
||||
setLinkedItems((prev) => {
|
||||
const existingIds = new Set(prev.map((item) => item.id));
|
||||
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||
return [...prev, ...additions];
|
||||
});
|
||||
onUpdate?.();
|
||||
}
|
||||
|
||||
setSelectedItems((prev) => {
|
||||
if (linkedIds.length === 0) {
|
||||
return prev;
|
||||
}
|
||||
const remaining = new Set(prev);
|
||||
linkedIds.forEach((id) => remaining.delete(id));
|
||||
return failedIds.length > 0 ? remaining : new Set();
|
||||
});
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
setError(
|
||||
failedIds.length === idsToLink.length
|
||||
? "Failed to link sights"
|
||||
: "Some sights failed to link"
|
||||
);
|
||||
}
|
||||
|
||||
setIsLinkingBulk(false);
|
||||
};
|
||||
|
||||
const toggleDetachSelection = (itemId: number) => {
|
||||
@@ -270,7 +295,7 @@ const LinkedSightsContentsInner = <
|
||||
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||
};
|
||||
|
||||
const handleBulkDetach = () => {
|
||||
const handleBulkDetach = async () => {
|
||||
const idsToDetach = Array.from(selectedToDetach);
|
||||
if (idsToDetach.length === 0) return;
|
||||
setError(null);
|
||||
@@ -282,32 +307,50 @@ const LinkedSightsContentsInner = <
|
||||
return next;
|
||||
});
|
||||
|
||||
Promise.all(
|
||||
idsToDetach.map((itemId) =>
|
||||
authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
const detachedIds: number[] = [];
|
||||
const failedIds: number[] = [];
|
||||
|
||||
for (const itemId of idsToDetach) {
|
||||
try {
|
||||
await authInstance.delete(
|
||||
`/${parentResource}/${parentId}/${childResource}`,
|
||||
{
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
setLinkedItems(
|
||||
linkedItems.filter((item) => !idsToDetach.includes(item.id))
|
||||
}
|
||||
);
|
||||
setSelectedToDetach(new Set());
|
||||
detachedIds.push(itemId);
|
||||
} catch (error) {
|
||||
console.error("Error deleting sight:", error);
|
||||
failedIds.push(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
if (detachedIds.length > 0) {
|
||||
setLinkedItems((prev) =>
|
||||
prev.filter((item) => !detachedIds.includes(item.id))
|
||||
);
|
||||
setSelectedToDetach((prev) => {
|
||||
const remaining = new Set(prev);
|
||||
detachedIds.forEach((id) => remaining.delete(id));
|
||||
return failedIds.length > 0 ? remaining : new Set();
|
||||
});
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error bulk deleting sights:", error);
|
||||
setError("Failed to delete sights");
|
||||
})
|
||||
.finally(() => {
|
||||
}
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
setError(
|
||||
failedIds.length === idsToDetach.length
|
||||
? "Failed to delete sights"
|
||||
: "Some sights failed to delete"
|
||||
);
|
||||
}
|
||||
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
idsToDetach.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
setIsBulkDetaching(false);
|
||||
});
|
||||
};
|
||||
|
||||
const allSelectedForDetach =
|
||||
@@ -465,8 +508,9 @@ const LinkedSightsContentsInner = <
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find((item) => item.id === selectedItemId) ||
|
||||
null
|
||||
availableItems?.find(
|
||||
(item) => item.id === selectedItemId
|
||||
) || null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setSelectedItemId(newValue?.id || null)
|
||||
|
||||
@@ -132,12 +132,16 @@ class SightsStore {
|
||||
common: boolean
|
||||
) => {
|
||||
if (common) {
|
||||
// @ts-ignore
|
||||
this.sight!.common = {
|
||||
// @ts-ignore
|
||||
...this.sight!.common,
|
||||
...content,
|
||||
};
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.sight![language] = {
|
||||
// @ts-ignore
|
||||
...this.sight![language],
|
||||
...content,
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user