select and addable geometry global

This commit is contained in:
taDuc
2026-05-26 10:44:49 +07:00
parent e403413965
commit faf5c56219
4 changed files with 242 additions and 26 deletions
+164
View File
@@ -63,6 +63,7 @@ import {
import { FIXED_TIMELINE_RANGE, clampYearToFixedRange, normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline"; import { FIXED_TIMELINE_RANGE, clampYearToFixedRange, normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
import { useFeatureCommands } from "@/uhm/lib/editor/geometry/useFeatureCommands"; import { useFeatureCommands } from "@/uhm/lib/editor/geometry/useFeatureCommands";
import { deleteSubmission } from "@/uhm/api/projects"; import { deleteSubmission } from "@/uhm/api/projects";
import type { EntitySnapshot } from "@/uhm/types/entities";
import type { WikiSnapshot } from "@/uhm/types/wiki"; import type { WikiSnapshot } from "@/uhm/types/wiki";
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects"; import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
import { import {
@@ -820,6 +821,21 @@ function EditorPageContent() {
globalGeometries.features, globalGeometries.features,
]); ]);
const localFeatureIds = useMemo(() => {
const ids = new Set<string | number>();
for (const feature of editor.mainDraft.features) {
if (feature.properties?.id !== undefined && feature.properties.id !== null) {
ids.add(feature.properties.id);
}
}
for (const feature of baselineFeatureCollection.features) {
if (feature.properties?.id !== undefined && feature.properties.id !== null) {
ids.add(feature.properties.id);
}
}
return Array.from(ids);
}, [baselineFeatureCollection.features, editor.mainDraft.features]);
// Danh sách feature đang chọn, map từ selectedFeatureIds sang draft hiện tại. // Danh sách feature đang chọn, map từ selectedFeatureIds sang draft hiện tại.
const selectedFeatures = useMemo(() => { const selectedFeatures = useMemo(() => {
if (!selectedFeatureIds || selectedFeatureIds.length === 0) return []; if (!selectedFeatureIds || selectedFeatureIds.length === 0) return [];
@@ -2419,6 +2435,62 @@ function EditorPageContent() {
setSelectedFeatureIds, setSelectedFeatureIds,
]); ]);
// Add geometry đang xem từ global mode vào draft local, kèm entity refs đã map được.
const handleAddGlobalGeometryToProject = useCallback((feature: Feature) => {
const geoId = String(feature?.properties?.id || "").trim();
if (!geoId) return;
const existing = editor.mainDraft.features.find((item) => String(item.properties.id) === geoId) || null;
if (existing) {
setSelectedFeatureIds([existing.properties.id]);
flashEntityFormStatus("Geometry này đã nằm trong project.", 3000);
return;
}
if (isGlobalLoading) {
flashEntityFormStatus("Đang tải global geometry và entity mapping, thử lại sau.", 3000);
return;
}
const entityRefs = buildEntityRefsForFeature(feature, entities);
const entityIds = entityRefs.map((entity) => String(entity.id));
const featureClone = deepClone(feature);
const nextFeature: Feature = {
...featureClone,
properties: {
...featureClone.properties,
id: geoId,
source: "ref",
...buildFeatureEntityPatch(featureClone, entityIds, entityRefs),
},
};
const entitySnapshots = entityRefs.map(toEntityRefSnapshot);
editor.createFeatureWithSnapshotEntityRows(
nextFeature,
(prev) => mergeSnapshotEntityRefs(prev, entitySnapshots),
`Add global GEO #${geoId}`
);
if (entityRefs.length) {
setEntityCatalog((prev) => mergeEntityCatalogById(prev, entityRefs));
}
setSelectedFeatureIds([nextFeature.properties.id]);
flashEntityFormStatus(
entityRefs.length
? `Đã add geometry global vào project kèm ${entityRefs.length} entity. Commit khi sẵn sàng.`
: "Đã add geometry global vào project. Geometry này chưa có entity mapping.",
3000
);
}, [
editor,
entities,
flashEntityFormStatus,
isGlobalLoading,
setEntityCatalog,
setSelectedFeatureIds,
]);
// Commands thao tác metadata/entity binding cho feature đang chọn. // Commands thao tác metadata/entity binding cho feature đang chọn.
const featureCommands = useFeatureCommands({ const featureCommands = useFeatureCommands({
editor, editor,
@@ -2826,6 +2898,7 @@ function EditorPageContent() {
selectedFeatureIds={selectedFeatureIds} selectedFeatureIds={selectedFeatureIds}
onSelectFeatureIds={setSelectedFeatureIds} onSelectFeatureIds={setSelectedFeatureIds}
onCreateFeature={handleCreateFeature} onCreateFeature={handleCreateFeature}
onAddFeatureToProject={handleAddGlobalGeometryToProject}
onDeleteFeature={(id) => { onDeleteFeature={(id) => {
if (Array.isArray(id)) { if (Array.isArray(id)) {
editor.deleteFeatures(id); editor.deleteFeatures(id);
@@ -2870,6 +2943,7 @@ function EditorPageContent() {
imageOverlay={imageOverlay} imageOverlay={imageOverlay}
onImageOverlayChange={setImageOverlay} onImageOverlayChange={setImageOverlay}
onBindGeometries={handleBindGeometries} onBindGeometries={handleBindGeometries}
localFeatureIds={localFeatureIds}
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible} showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
isPreviewMode={isAnyPreviewMode} isPreviewMode={isAnyPreviewMode}
onEnterPreview={!isReplayEditMode && !isAnyPreviewMode ? openViewerPreview : undefined} onEnterPreview={!isReplayEditMode && !isAnyPreviewMode ? openViewerPreview : undefined}
@@ -3586,6 +3660,96 @@ function buildEntityLabelContextDraft(draft: FeatureCollection, entities: Entity
}; };
} }
function buildEntityRefsForFeature(feature: Feature, entities: Entity[]): Entity[] {
const entityIds = normalizeFeatureEntityIds(feature);
if (!entityIds.length) return [];
const entityById = new globalThis.Map<string, Entity>();
for (const entity of entities || []) {
const id = String(entity?.id || "").trim();
if (!id) continue;
entityById.set(id, entity);
}
const entityNames = Array.isArray(feature.properties.entity_names)
? feature.properties.entity_names
: [];
const primaryName = typeof feature.properties.entity_name === "string"
? feature.properties.entity_name.trim()
: "";
return entityIds.map((id, index) => {
const catalogEntity = entityById.get(id);
if (catalogEntity) return catalogEntity;
const name = String(entityNames[index] || (index === 0 ? primaryName : "") || id).trim() || id;
return {
id,
name,
description: null,
time_start: null,
time_end: null,
geometry_count: 0,
};
});
}
function toEntityRefSnapshot(entity: Entity): EntitySnapshot {
return {
id: String(entity.id),
source: "ref",
operation: "reference",
name: entity.name,
description: entity.description ?? null,
time_start: normalizeTimelineYearValue(entity.time_start),
time_end: normalizeTimelineYearValue(entity.time_end),
};
}
function mergeSnapshotEntityRefs(prev: EntitySnapshot[], refs: EntitySnapshot[]): EntitySnapshot[] {
if (!refs.length) return prev;
const refsById = new globalThis.Map<string, EntitySnapshot>();
for (const ref of refs) {
const id = String(ref?.id || "").trim();
if (!id) continue;
refsById.set(id, ref);
}
if (!refsById.size) return prev;
let changed = false;
const seen = new Set<string>();
const next = (prev || []).map((row) => {
const id = String(row?.id || "").trim();
if (!id || !refsById.has(id)) return row;
seen.add(id);
if (row.operation !== "delete") return row;
changed = true;
return refsById.get(id) || row;
});
const missing = Array.from(refsById.values()).filter((ref) => !seen.has(String(ref.id)));
if (missing.length) changed = true;
return changed ? [...missing, ...next] : prev;
}
function mergeEntityCatalogById(prev: Entity[], refs: Entity[]): Entity[] {
if (!refs.length) return prev;
const byId = new globalThis.Map<string, Entity>();
for (const entity of prev || []) {
const id = String(entity?.id || "").trim();
if (!id) continue;
byId.set(id, entity);
}
for (const entity of refs) {
const id = String(entity?.id || "").trim();
if (!id) continue;
byId.set(id, entity);
}
return Array.from(byId.values());
}
function parseOptionalEntityYearInput(value: string, fieldName: string): number | undefined { function parseOptionalEntityYearInput(value: string, fieldName: string): number | undefined {
const trimmed = String(value || "").trim(); const trimmed = String(value || "").trim();
if (!trimmed.length) return undefined; if (!trimmed.length) return undefined;
+12
View File
@@ -46,6 +46,7 @@ type MapProps = {
labelContextDraft?: FeatureCollection; labelContextDraft?: FeatureCollection;
labelTimelineYear?: number | null; labelTimelineYear?: number | null;
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void; onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
onAddFeatureToProject?: (feature: FeatureCollection["features"][number]) => void;
onDeleteFeature?: (id: string | number | (string | number)[]) => void; onDeleteFeature?: (id: string | number | (string | number)[]) => void;
onHideFeature?: (id: string | number) => void; onHideFeature?: (id: string | number) => void;
onUpdateFeature?: (id: string | number, geometry: Geometry) => void; onUpdateFeature?: (id: string | number, geometry: Geometry) => void;
@@ -61,6 +62,7 @@ type MapProps = {
imageOverlay?: MapImageOverlay | null; imageOverlay?: MapImageOverlay | null;
onImageOverlayChange?: (overlay: MapImageOverlay) => void; onImageOverlayChange?: (overlay: MapImageOverlay) => void;
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void; onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void;
localFeatureIds?: (string | number)[];
showViewportControls?: boolean; showViewportControls?: boolean;
isPreviewMode?: boolean; isPreviewMode?: boolean;
onEnterPreview?: () => void; onEnterPreview?: () => void;
@@ -81,6 +83,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
labelContextDraft, labelContextDraft,
labelTimelineYear, labelTimelineYear,
onCreateFeature, onCreateFeature,
onAddFeatureToProject,
onDeleteFeature, onDeleteFeature,
onHideFeature, onHideFeature,
onUpdateFeature, onUpdateFeature,
@@ -96,6 +99,7 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
imageOverlay = null, imageOverlay = null,
onImageOverlayChange, onImageOverlayChange,
onBindGeometries, onBindGeometries,
localFeatureIds,
showViewportControls = true, showViewportControls = true,
isPreviewMode = false, isPreviewMode = false,
onEnterPreview, onEnterPreview,
@@ -116,6 +120,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
const onFeatureClickRef = useRef<MapProps["onFeatureClick"]>(onFeatureClick); const onFeatureClickRef = useRef<MapProps["onFeatureClick"]>(onFeatureClick);
// Ref callback create mới nhất khi drawing engine tạo feature. // Ref callback create mới nhất khi drawing engine tạo feature.
const onCreateRef = useRef<MapProps["onCreateFeature"]>(onCreateFeature); const onCreateRef = useRef<MapProps["onCreateFeature"]>(onCreateFeature);
// Ref callback add geometry global vào project mới nhất cho context menu select.
const onAddFeatureToProjectRef = useRef<MapProps["onAddFeatureToProject"]>(onAddFeatureToProject);
// Ref callback delete mới nhất khi editing engine xóa feature. // Ref callback delete mới nhất khi editing engine xóa feature.
const onDeleteRef = useRef<MapProps["onDeleteFeature"]>(onDeleteFeature); const onDeleteRef = useRef<MapProps["onDeleteFeature"]>(onDeleteFeature);
// Ref callback hide local mới nhất khi context menu select ẩn feature khỏi map. // Ref callback hide local mới nhất khi context menu select ẩn feature khỏi map.
@@ -128,6 +134,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
const onImageOverlayChangeRef = useRef<MapProps["onImageOverlayChange"]>(onImageOverlayChange); const onImageOverlayChangeRef = useRef<MapProps["onImageOverlayChange"]>(onImageOverlayChange);
// Ref callback bind geometry mới nhất để interaction không stale. // Ref callback bind geometry mới nhất để interaction không stale.
const onBindGeometriesRef = useRef<MapProps["onBindGeometries"]>(onBindGeometries); const onBindGeometriesRef = useRef<MapProps["onBindGeometries"]>(onBindGeometries);
// Ref danh sách geometry thuộc local project để context menu phân biệt global-only feature.
const localFeatureIdsRef = useRef<MapProps["localFeatureIds"]>(localFeatureIds);
useEffect(() => { modeRef.current = mode; }, [mode]); useEffect(() => { modeRef.current = mode; }, [mode]);
useEffect(() => { renderDraftRef.current = renderDraft; }, [renderDraft]); useEffect(() => { renderDraftRef.current = renderDraft; }, [renderDraft]);
@@ -135,12 +143,14 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
useEffect(() => { onSetModeRef.current = onSetMode; }, [onSetMode]); useEffect(() => { onSetModeRef.current = onSetMode; }, [onSetMode]);
useEffect(() => { onFeatureClickRef.current = onFeatureClick; }, [onFeatureClick]); useEffect(() => { onFeatureClickRef.current = onFeatureClick; }, [onFeatureClick]);
useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]); useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]);
useEffect(() => { onAddFeatureToProjectRef.current = onAddFeatureToProject; }, [onAddFeatureToProject]);
useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]); useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]);
useEffect(() => { onHideRef.current = onHideFeature; }, [onHideFeature]); useEffect(() => { onHideRef.current = onHideFeature; }, [onHideFeature]);
useEffect(() => { onUpdateRef.current = onUpdateFeature; }, [onUpdateFeature]); useEffect(() => { onUpdateRef.current = onUpdateFeature; }, [onUpdateFeature]);
useEffect(() => { imageOverlayRef.current = imageOverlay; }, [imageOverlay]); useEffect(() => { imageOverlayRef.current = imageOverlay; }, [imageOverlay]);
useEffect(() => { onImageOverlayChangeRef.current = onImageOverlayChange; }, [onImageOverlayChange]); useEffect(() => { onImageOverlayChangeRef.current = onImageOverlayChange; }, [onImageOverlayChange]);
useEffect(() => { onBindGeometriesRef.current = onBindGeometries; }, [onBindGeometries]); useEffect(() => { onBindGeometriesRef.current = onBindGeometries; }, [onBindGeometries]);
useEffect(() => { localFeatureIdsRef.current = localFeatureIds; }, [localFeatureIds]);
// Hook sở hữu lifecycle MapLibre instance và các control camera/projection. // Hook sở hữu lifecycle MapLibre instance và các control camera/projection.
const { const {
@@ -189,6 +199,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
onUpdateRef, onUpdateRef,
onFeatureClickRef, onFeatureClickRef,
onBindGeometriesRef, onBindGeometriesRef,
localFeatureIdsRef,
onAddFeatureToProjectRef,
}); });
// Hook đồng bộ draft/layer/filter/highlight từ React state xuống MapLibre source/layer. // Hook đồng bộ draft/layer/filter/highlight từ React state xuống MapLibre source/layer.
@@ -36,6 +36,8 @@ type UseMapInteractionProps = {
onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>; onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>;
onFeatureClickRef: React.MutableRefObject<((payload: MapFeaturePayload | null) => void) | undefined>; onFeatureClickRef: React.MutableRefObject<((payload: MapFeaturePayload | null) => void) | undefined>;
onBindGeometriesRef?: React.MutableRefObject<((targetId: string | number, sourceIds: (string | number)[]) => void) | undefined>; onBindGeometriesRef?: React.MutableRefObject<((targetId: string | number, sourceIds: (string | number)[]) => void) | undefined>;
localFeatureIdsRef?: React.MutableRefObject<(string | number)[] | undefined>;
onAddFeatureToProjectRef?: React.MutableRefObject<((feature: FeatureCollection["features"][number]) => void) | undefined>;
}; };
export function useMapInteraction({ export function useMapInteraction({
@@ -53,6 +55,8 @@ export function useMapInteraction({
onUpdateRef, onUpdateRef,
onFeatureClickRef, onFeatureClickRef,
onBindGeometriesRef, onBindGeometriesRef,
localFeatureIdsRef,
onAddFeatureToProjectRef,
}: UseMapInteractionProps) { }: UseMapInteractionProps) {
const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null); const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null);
const engineBindingsRef = useRef<Partial<Record<EditorMode, EngineBinding>>>({}); const engineBindingsRef = useRef<Partial<Record<EditorMode, EngineBinding>>>({});
@@ -199,7 +203,27 @@ export function useMapInteraction({
...payload, ...payload,
feature: currentFeature, feature: currentFeature,
}); });
},
onAddFeatureToProjectRef?.current
? (feature) => {
const rawId = feature.id ?? feature.properties?.id;
if (rawId === undefined || rawId === null) return;
const originalFeature = renderDraftRef.current.features.find(
(item) => String(item.properties.id) === String(rawId)
);
if (!originalFeature) return;
onAddFeatureToProjectRef.current?.(originalFeature);
} }
: undefined,
onAddFeatureToProjectRef?.current
? (id) => {
if (!onAddFeatureToProjectRef?.current) return true;
const localIds = localFeatureIdsRef?.current;
if (!Array.isArray(localIds)) return true;
return localIds.some((localId) => String(localId) === String(id));
}
: undefined
); );
const cleanupPoint = initPoint( const cleanupPoint = initPoint(
+36 -20
View File
@@ -19,7 +19,9 @@ export function initSelect(
onReplayEdit?: (id: string | number) => void, onReplayEdit?: (id: string | number) => void,
isEditSessionActive?: () => boolean, isEditSessionActive?: () => boolean,
onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void, onBindGeometries?: (targetId: string | number, sourceIds: (string | number)[]) => void,
onFeatureClick?: (payload: SelectFeatureClickPayload | null) => void onFeatureClick?: (payload: SelectFeatureClickPayload | null) => void,
onAddToProject?: (feature: maplibregl.MapGeoJSONFeature) => void,
isLocalFeature?: (id: string | number) => boolean
) { ) {
const FEATURE_STATE_SOURCES = [ const FEATURE_STATE_SOURCES = [
@@ -28,7 +30,7 @@ export function initSelect(
"path-arrow-shapes", "path-arrow-shapes",
] as const; ] as const;
const selectedIds = new Set<number | string>(); const selectedIds = new Set<number | string>();
const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit || onBindGeometries); const hasContextActions = Boolean(onDelete || onEdit || onDuplicate || onHide || onReplayEdit || onBindGeometries || onAddToProject);
let contextMenu: HTMLDivElement | null = null; let contextMenu: HTMLDivElement | null = null;
let docClickHandler: ((ev: MouseEvent) => void) | null = null; let docClickHandler: ((ev: MouseEvent) => void) | null = null;
let cursorTimer: number | null = null; let cursorTimer: number | null = null;
@@ -306,31 +308,48 @@ export function initSelect(
return item; return item;
}; };
const selectedCount = selectedIds.size;
const effectiveCount = selectedCount || 1;
const targetId = clickedFeature.id ?? clickedFeature.properties?.id; const targetId = clickedFeature.id ?? clickedFeature.properties?.id;
const hasTargetId = targetId !== undefined && targetId !== null;
const isLocalTarget = hasTargetId ? (isLocalFeature?.(targetId) ?? true) : false;
const selectedLocalIds = Array.from(selectedIds).filter((id) => isLocalFeature?.(id) ?? true);
const localActionIds = selectedLocalIds.length
? selectedLocalIds
: isLocalTarget && hasTargetId
? [targetId]
: [];
const effectiveCount = localActionIds.length;
const isClickOutsideSelection = !isRightClickedItemAlreadySelected && hasSelection; const isClickOutsideSelection = !isRightClickedItemAlreadySelected && hasSelection;
type MenuItem = { type MenuItem = {
label: string; label: string;
onClick: () => void; onClick: () => void;
group: "edit" | "bind" | "replay" | "delete"; group: "add" | "edit" | "bind" | "replay" | "delete";
}; };
const items: MenuItem[] = []; const items: MenuItem[] = [];
if (isClickOutsideSelection && onBindGeometries && targetId !== undefined && targetId !== null) { if (onAddToProject && hasTargetId && !isLocalTarget) {
const sourceIds = Array.from(selectedIds); items.push({
group: "add",
label: "Add",
onClick: () => onAddToProject(clickedFeature),
});
}
if (isClickOutsideSelection && onBindGeometries && isLocalTarget && hasTargetId) {
const sourceIds = selectedLocalIds.filter((id) => String(id) !== String(targetId));
if (sourceIds.length) {
items.push({ items.push({
group: "bind", group: "bind",
label: `Bind ${selectedCount} geo đang chọn vào geo này`, label: `Bind ${sourceIds.length} geo đang chọn vào geo này`,
onClick: () => { onClick: () => {
onBindGeometries(targetId, sourceIds); onBindGeometries(targetId, sourceIds);
}, },
}); });
} }
}
if (!isClickOutsideSelection) { if (isLocalTarget && !isClickOutsideSelection) {
if ( if (
effectiveCount === 1 && effectiveCount === 1 &&
clickedFeature.source === "countries" && clickedFeature.source === "countries" &&
@@ -345,7 +364,7 @@ export function initSelect(
}); });
} }
if (effectiveCount === 1 && onDuplicate && targetId !== undefined && targetId !== null) { if (effectiveCount === 1 && onDuplicate && hasTargetId) {
items.push({ items.push({
group: "edit", group: "edit",
label: "Duplicate", label: "Duplicate",
@@ -353,7 +372,7 @@ export function initSelect(
}); });
} }
if (effectiveCount === 1 && onHide && targetId !== undefined && targetId !== null) { if (effectiveCount === 1 && onHide && hasTargetId) {
items.push({ items.push({
group: "edit", group: "edit",
label: "Hide", label: "Hide",
@@ -362,10 +381,10 @@ export function initSelect(
} }
} }
if (onReplayEdit) { if (isLocalTarget && onReplayEdit) {
const replayId = targetId; const replayId = targetId;
if (replayId !== undefined && replayId !== null) { if (replayId !== undefined && replayId !== null) {
const totalCount = isClickOutsideSelection ? selectedIds.size + 1 : effectiveCount; const totalCount = isClickOutsideSelection ? selectedLocalIds.length + 1 : effectiveCount;
items.push({ items.push({
group: "replay", group: "replay",
label: totalCount > 1 ? `Vào replay (${totalCount} geo)` : "Vào replay", label: totalCount > 1 ? `Vào replay (${totalCount} geo)` : "Vào replay",
@@ -374,18 +393,15 @@ export function initSelect(
} }
} }
if (onDelete) { if (isLocalTarget && onDelete && effectiveCount > 0) {
items.push({ items.push({
group: "delete", group: "delete",
label: effectiveCount > 1 ? `Xóa ${effectiveCount} mục` : "Xóa", label: effectiveCount > 1 ? `Xóa ${effectiveCount} mục` : "Xóa",
onClick: () => { onClick: () => {
const ids = selectedIds.size if (localActionIds.length === 1) {
? Array.from(selectedIds) onDelete(localActionIds[0]);
: [targetId];
if (ids.length === 1) {
onDelete(ids[0]);
} else { } else {
onDelete(ids); onDelete(localActionIds);
} }
clearSelection(); clearSelection();
}, },