feat: extend map targeting and overlay controls

Support batch feature targeting and clear map highlights and overlays from the More panel. Remove misleading action chevrons and unused configuration export entries.

Validated the isolated patch with lint, typecheck, 36 focused tests and 5 browser cases using mocked data.
This commit is contained in:
2026-10-28 14:47:33 +08:00
parent e57e0f099f
commit 5b710d1247
17 changed files with 666 additions and 202 deletions
@@ -9,6 +9,112 @@ import {
import { WorkbenchMapController } from "./workbench-map-controller";
describe("WorkbenchMapController", () => {
it("clears every controller overlay, preserves layer styles, and invalidates pending analysis", async () => {
const map = createMap();
map.getLayer.mockReturnValue({} as never);
const setData = vi.fn();
map.getSource.mockReturnValue({ setData });
let resolveFeatures = (_value: FeatureCollection) => {};
const pendingAnalysis = new Promise<FeatureCollection>((resolve) => { resolveFeatures = resolve; });
const controller = new WorkbenchMapController({
getMap: () => map as unknown as MapLibreMap, isReady: () => true,
getPadding: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
fetchFeatures: (source) => source === "scada" ? pendingAnalysis : Promise.resolve(pointCollection)
});
await controller.highlight({ sourceId: "junctions", featureId: "J-1" });
controller.applyLayerGroupStyle("pipes", { color: "#123456" });
const showingFlow = controller.setFlowVisible(true);
const analysis = controller.renderScadaAnalysis([{ device_id: "J-1", level: "high" }]);
controller.clearOverlays();
await showingFlow;
resolveFeatures(pointCollection);
await expect(analysis).rejects.toThrow("ACTION_SUPERSEDED");
expect(setData).toHaveBeenCalledWith({ type: "FeatureCollection", features: [] });
expect(controller.getSnapshot()).toMatchObject({
target: null, targets: [], targetResult: null, scadaAnalysis: null,
flowVisible: false, pending: false, valueLabels: {}, styledGroups: ["pipes"]
});
expect(map.removeFeatureState).toHaveBeenCalledWith(expect.objectContaining({ id: "J-1" }), "highlighted");
expect(map.setLayoutProperty).toHaveBeenCalledWith(FLOW_LINE_LAYER_ID, "visibility", "none");
expect(map.easeTo).not.toHaveBeenCalled();
});
it("queries a batch once, fits all matches, and replaces and clears every highlight", async () => {
const map = createMap();
const collection: FeatureCollection = { type: "FeatureCollection", features: [
...pointCollection.features,
{ type: "Feature", properties: { id: "J-2" }, geometry: { type: "Point", coordinates: [121, 29] } }
] };
const fetchFeatures = vi.fn(async () => collection);
const controller = new WorkbenchMapController({
getMap: () => map as unknown as MapLibreMap, isReady: () => true,
getPadding: () => ({ top: 48, right: 48, bottom: 48, left: 48 }), fetchFeatures
});
await expect(controller.locateFeatures("junctions", ["J-1", "J-2", "J-1", "missing"])).resolves.toEqual({
located_ids: ["J-1", "J-2"], missing_ids: ["missing"], fitted: true
});
expect(fetchFeatures).toHaveBeenCalledExactlyOnceWith("junctions", ["J-1", "J-2", "missing"], undefined);
expect(map.fitBounds).toHaveBeenCalledWith([[120.7, 28], [121, 29]], expect.objectContaining({ maxZoom: 18 }));
for (const id of ["J-1", "J-2"]) {
expect(map.setFeatureState).toHaveBeenCalledWith({ source: "junctions", sourceLayer: "junctions", id }, { highlighted: true });
}
expect(controller.getSnapshot().targets).toHaveLength(2);
await controller.highlight({ sourceId: "junctions", featureId: "J-1" });
for (const id of ["J-1", "J-2"]) {
expect(map.removeFeatureState).toHaveBeenCalledWith({ source: "junctions", sourceLayer: "junctions", id }, "highlighted");
}
await controller.highlight({ sourceId: "junctions", featureIds: ["J-1", "J-2"] });
map.removeFeatureState.mockClear();
controller.clearHighlight();
expect(map.removeFeatureState).toHaveBeenCalledTimes(2);
expect(controller.getSnapshot()).toMatchObject({ targets: [], target: null, targetResult: null });
});
it("keeps highlight-only and zoom-only batch behavior separate", async () => {
const map = createMap();
const controller = createController(map, lineCollection);
const target = { sourceId: "pipes" as const, featureIds: ["P-1", "missing"] };
await controller.highlight(target);
expect(map.fitBounds).not.toHaveBeenCalled();
expect(map.easeTo).not.toHaveBeenCalled();
expect(controller.getSnapshot().targetResult).toMatchObject({ featureIds: ["P-1"], missingIds: ["missing"] });
map.setFeatureState.mockClear();
await controller.zoomToFeature(target);
expect(map.fitBounds).toHaveBeenCalledOnce();
expect(map.setFeatureState).not.toHaveBeenCalled();
expect(map.removeFeatureState).not.toHaveBeenCalled();
expect(controller.getSnapshot().targets).toEqual([{ sourceId: "pipes", featureId: "P-1" }]);
});
it("cancels an in-flight batch without applying late highlights or moving the camera", async () => {
const map = createMap();
const abort = new AbortController();
let resolveFeatures = (_value: FeatureCollection) => {};
const fetchFeatures = vi.fn(() => new Promise<FeatureCollection>((resolve) => { resolveFeatures = resolve; }));
const controller = new WorkbenchMapController({
getMap: () => map as unknown as MapLibreMap, isReady: () => true,
getPadding: () => ({ top: 0, right: 0, bottom: 0, left: 0 }), fetchFeatures
});
const locating = controller.locateFeatures("junctions", ["J-1", "J-2"], abort.signal);
abort.abort();
resolveFeatures(pointCollection);
await expect(locating).rejects.toThrow();
expect(fetchFeatures).toHaveBeenCalledWith("junctions", ["J-1", "J-2"], abort.signal);
expect(map.fitBounds).not.toHaveBeenCalled();
expect(map.setFeatureState).not.toHaveBeenCalled();
expect(controller.getSnapshot()).toMatchObject({ pending: false, targets: [] });
});
it("rejects empty and oversized batches without querying or moving", async () => {
const map = createMap();
const controller = createController(map, pointCollection);
for (const ids of [[], [" "], Array.from({ length: 101 }, (_, i) => `J-${i}`)]) {
await expect(controller.locateFeatures("junctions", ids)).rejects.toThrow("INVALID_FEATURE_IDS");
}
expect(map.fitBounds).not.toHaveBeenCalled();
expect(map.setFeatureState).not.toHaveBeenCalled();
});
it("returns only resolved IDs and rejects a missing target rather than fitting the whole network", async () => {
const map = createMap();
const controller = createController(map, pointCollection);
@@ -23,6 +23,7 @@ import {
import {
MAX_PIPE_FEATURES,
MAX_MAP_FEATURE_IDS,
MAX_MAP_FEATURE_ID_LENGTH,
createMapFeatureWfsUrl,
normalizeMapFeatureCollection,
parseMapFeatureQuery
@@ -44,9 +45,14 @@ import {
} from "./scada-analysis";
export type FeatureTarget = { sourceId: WaterNetworkSourceId; featureId: string };
export type FeatureBatchTarget = { sourceId: WaterNetworkSourceId; featureIds: string[] };
type FeatureCommandTarget = FeatureTarget | FeatureBatchTarget;
type FeatureCommandMode = "highlight" | "zoom" | "locate";
type FeatureCommandResult = FeatureBatchTarget & { missingIds: string[]; mode: FeatureCommandMode };
export type WorkbenchMapErrorCode =
| "MAP_NOT_READY"
| "FEATURE_NOT_FOUND"
| "INVALID_FEATURE_IDS"
| "WFS_UNAVAILABLE"
| "SCADA_API_UNAVAILABLE"
| "SCADA_FEATURES_NOT_FOUND"
@@ -57,6 +63,8 @@ export type WorkbenchMapErrorCode =
export type WorkbenchMapControllerState = {
target: FeatureTarget | null;
targets: FeatureTarget[];
targetResult: FeatureCommandResult | null;
valueLabels: Partial<Record<ValueLabelSourceId, ValueLabelStyle>>;
pending: boolean;
errorCode: WorkbenchMapErrorCode | null;
@@ -68,9 +76,9 @@ export type WorkbenchMapControllerState = {
export type ValueLabelRequest = Omit<ValueLabelStyle, "unit"> & { unit?: string };
export type WorkbenchMapCommands = {
zoomToFeature: (target: FeatureTarget) => Promise<void>;
locateAndHighlight: (target: FeatureTarget) => Promise<void>;
highlight: (target: FeatureTarget) => Promise<void>;
zoomToFeature: (target: FeatureCommandTarget) => Promise<void>;
locateAndHighlight: (target: FeatureCommandTarget) => Promise<void>;
highlight: (target: FeatureCommandTarget) => Promise<void>;
clearHighlight: () => void;
setFlowVisible: (visible: boolean) => Promise<void>;
replacePipeFlowCalculations: (records: readonly PipeFlowCalculation[]) => Promise<PipeFlowSummary | null>;
@@ -98,6 +106,8 @@ export type WorkbenchMapControllerOptions = {
export class WorkbenchMapController implements WorkbenchMapCommands {
private state: WorkbenchMapControllerState = {
target: null,
targets: [],
targetResult: null,
valueLabels: {},
pending: false,
errorCode: null,
@@ -107,15 +117,14 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
scadaAnalysis: null
};
private listeners = new Set<() => void>();
private highlightedTarget: FeatureTarget | null = null;
private cachedFeatures = new Map<string, Feature>();
private flowState: FlowOverlayState = createFlowOverlayState();
private flowUpdateRevision = 0;
private flowVisibilityRevision = 0;
private scadaAnalysisRevision = 0;
private targetRequestRevision = 0;
private pendingTargetSourceId: WaterNetworkSourceId | null = null;
private availableSourceIds: ReadonlySet<WaterNetworkSourceId> | null = null;
private sourceAvailabilityGenerations = new Map<WaterNetworkSourceId, number>();
constructor(private readonly options: WorkbenchMapControllerOptions) {}
@@ -126,47 +135,20 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
getSnapshot = () => this.state;
async zoomToFeature(target: FeatureTarget) {
const requestRevision = this.beginTargetCommand();
if (!this.requireTargetMap(target)) return;
const feature = await this.resolveTarget(target, requestRevision);
if (requestRevision !== this.targetRequestRevision) return;
const map = this.requireTargetMap(target);
if (!feature || !map) return;
this.moveCameraToFeature(map, feature);
async zoomToFeature(target: FeatureCommandTarget) {
await this.executeFeatureCommand(target, "zoom");
}
async locateAndHighlight(target: FeatureTarget) {
const requestRevision = this.beginTargetCommand();
if (!this.requireTargetMap(target)) return;
const feature = await this.resolveTarget(target, requestRevision);
if (requestRevision !== this.targetRequestRevision) return;
const map = this.requireTargetMap(target);
if (!feature || !map) return;
if (!this.moveCameraToFeature(map, feature)) return;
this.setHighlight(target);
async locateAndHighlight(target: FeatureCommandTarget) {
await this.executeFeatureCommand(target, "locate");
}
async locateFeatures(sourceId: WaterNetworkSourceId, featureIds: string[], signal?: AbortSignal) {
const requestRevision = this.beginTargetCommand();
const target = { sourceId, featureId: featureIds[0] ?? "" };
if (!this.requireTargetMap(target)) throw new Error(this.state.errorCode ?? "MAP_NOT_READY");
const found: Array<{ id: string; feature: Feature }> = [];
const missing: string[] = [];
for (const id of [...new Set(featureIds)]) {
signal?.throwIfAborted();
const feature = await this.resolveTarget({ sourceId, featureId: id }, requestRevision);
if (requestRevision !== this.targetRequestRevision) throw new Error("ACTION_SUPERSEDED");
if (this.state.errorCode === "WFS_UNAVAILABLE" || this.state.errorCode === "SCADA_API_UNAVAILABLE") throw new Error(this.state.errorCode);
if (feature) found.push({ id, feature }); else missing.push(id);
}
signal?.throwIfAborted();
const map = this.requireTargetMap(target);
if (!map || found.length === 0) throw new Error(this.state.errorCode ?? "FEATURE_NOT_FOUND");
const combined: Feature = { type: "Feature", properties: {}, geometry: { type: "MultiPoint", coordinates: found.flatMap(({ feature }) => collectCoordinates(feature.geometry)) } };
if (!this.moveCameraToFeature(map, combined, 0)) throw new Error("FEATURE_NOT_FOUND");
this.setHighlight({ sourceId, featureId: found[0].id });
return { located_ids: found.map(({ id }) => id), missing_ids: missing, fitted: true as const };
const requestRevision = this.targetRequestRevision + 1;
const result = await this.executeFeatureCommand({ sourceId, featureIds }, "locate", signal, 0);
if (requestRevision !== this.targetRequestRevision) throw new Error("ACTION_SUPERSEDED");
if (!result) throw new Error(this.state.errorCode ?? "ACTION_SUPERSEDED");
return { located_ids: result.featureIds, missing_ids: result.missingIds, fitted: true as const };
}
/** Resolve a task-owned overlay without changing the user's selection or controller highlight. */
@@ -209,36 +191,23 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
return true;
}
async highlight(target: FeatureTarget) {
const requestRevision = this.beginTargetCommand();
if (!this.requireTargetMap(target)) return;
const feature = await this.resolveTarget(target, requestRevision);
if (requestRevision !== this.targetRequestRevision) return;
if (feature) this.setHighlight(target);
async highlight(target: FeatureCommandTarget) {
await this.executeFeatureCommand(target, "highlight");
}
clearHighlight = () => {
this.beginTargetCommand();
const map = this.options.getMap();
if (map && this.highlightedTarget && map.getSource(this.highlightedTarget.sourceId)) {
map.removeFeatureState(toFeatureStateTarget(this.highlightedTarget), "highlighted");
for (const target of this.state.targets) {
if (map?.getSource(target.sourceId)) {
map.removeFeatureState(toFeatureStateTarget(target), "highlighted");
}
}
this.highlightedTarget = null;
this.patchState({ target: null, errorCode: null });
this.patchState({ target: null, targets: [], targetResult: null, errorCode: null });
};
syncAvailableSources(sourceIds: readonly WaterNetworkSourceId[]) {
const availableSourceIds = new Set<WaterNetworkSourceId>(sourceIds);
if (this.availableSourceIds) {
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.forEach((sourceId) => {
if (this.availableSourceIds?.has(sourceId) && !availableSourceIds.has(sourceId)) {
this.sourceAvailabilityGenerations.set(
sourceId,
(this.sourceAvailabilityGenerations.get(sourceId) ?? 0) + 1
);
}
});
}
this.availableSourceIds = availableSourceIds;
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.forEach((sourceId) => {
@@ -249,9 +218,9 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
}
});
const targetUnavailable = Boolean(
this.highlightedTarget && !availableSourceIds.has(this.highlightedTarget.sourceId)
);
const unavailableTargets = this.state.targets.filter((target) => !availableSourceIds.has(target.sourceId));
const targetUnavailable = unavailableTargets.length > 0;
const resultUnavailable = Boolean(this.state.targetResult && !availableSourceIds.has(this.state.targetResult.sourceId));
const pendingTargetUnavailable = Boolean(
this.pendingTargetSourceId && !availableSourceIds.has(this.pendingTargetSourceId)
);
@@ -259,12 +228,11 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
this.targetRequestRevision += 1;
this.pendingTargetSourceId = null;
}
if (targetUnavailable && this.highlightedTarget) {
const map = this.options.getMap();
if (map?.getSource(this.highlightedTarget.sourceId)) {
map.removeFeatureState(toFeatureStateTarget(this.highlightedTarget), "highlighted");
const map = this.options.getMap();
for (const target of unavailableTargets) {
if (map?.getSource(target.sourceId)) {
map.removeFeatureState(toFeatureStateTarget(target), "highlighted");
}
this.highlightedTarget = null;
}
const valueLabelEntries = Object.entries(this.state.valueLabels);
@@ -278,6 +246,7 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
const styledGroupsChanged = styledGroups.length !== this.state.styledGroups.length;
if (
!targetUnavailable &&
!resultUnavailable &&
!pendingTargetUnavailable &&
!valueLabelsChanged &&
!styledGroupsChanged
@@ -285,6 +254,8 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
this.patchState({
target: targetUnavailable ? null : this.state.target,
targets: targetUnavailable ? [] : this.state.targets,
targetResult: resultUnavailable ? null : this.state.targetResult,
pending: pendingTargetUnavailable ? false : this.state.pending,
errorCode: pendingTargetUnavailable ? "WFS_UNAVAILABLE" : this.state.errorCode,
valueLabels: valueLabelsChanged
@@ -295,6 +266,7 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
}
async setFlowVisible(visible: boolean) {
const revision = ++this.flowVisibilityRevision;
const map = this.requireMap();
if (!map) return;
if (!visible) {
@@ -309,8 +281,10 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
this.flowState,
this.options.reducedMotion?.()
);
if (revision !== this.flowVisibilityRevision) return;
this.patchState({ pending: false, flowVisible: true });
} catch {
if (revision !== this.flowVisibilityRevision) return;
hideFlowOverlay(map, this.flowState);
this.patchState({ pending: false, flowVisible: false, errorCode: "FLOW_UNAVAILABLE" });
}
@@ -533,10 +507,18 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
return { cleared: true as const };
}
resetAll = () => {
clearOverlays = () => {
this.clearHighlight();
this.clearValueLabel();
this.clearScadaAnalysis();
this.flowVisibilityRevision += 1;
const map = this.options.getMap();
if (map) hideFlowOverlay(map, this.flowState);
this.patchState({ flowVisible: false });
};
resetAll = () => {
this.clearOverlays();
const map = this.options.getMap();
if (map) {
hideFlowOverlay(map, this.flowState);
@@ -574,42 +556,86 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
return requestRevision;
}
private async resolveTarget(target: FeatureTarget, requestRevision: number) {
const map = this.requireMap();
if (!map) return null;
const key = `${target.sourceId}:${target.featureId}`;
const dataDependent = DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.includes(
target.sourceId as (typeof DATA_DEPENDENT_GEOSERVER_SOURCE_IDS)[number]
);
const cached = dataDependent ? undefined : this.cachedFeatures.get(key);
if (cached) return cached;
const sourceAvailabilityGeneration = this.sourceAvailabilityGenerations.get(target.sourceId) ?? 0;
this.pendingTargetSourceId = target.sourceId;
this.patchState({ pending: true, errorCode: null });
try {
const collection = await this.fetchFeatures(target.sourceId, [target.featureId]);
if (
requestRevision !== this.targetRequestRevision ||
sourceAvailabilityGeneration !== (this.sourceAvailabilityGenerations.get(target.sourceId) ?? 0)
) return null;
this.pendingTargetSourceId = null;
const feature = collection.features.find((item) => String(item.properties?.id ?? item.properties?.device_id ?? item.id) === target.featureId);
if (!feature) {
this.patchState({ pending: false, errorCode: "FEATURE_NOT_FOUND" });
return null;
}
if (!dataDependent) this.cachedFeatures.set(key, feature);
this.patchState({ pending: false });
return feature;
} catch {
if (requestRevision !== this.targetRequestRevision) return null;
this.pendingTargetSourceId = null;
this.patchState({
pending: false,
errorCode: target.sourceId === "scada" ? "SCADA_API_UNAVAILABLE" : "WFS_UNAVAILABLE"
});
private async executeFeatureCommand(
input: FeatureCommandTarget,
mode: FeatureCommandMode,
signal?: AbortSignal,
durationOverride?: number
): Promise<FeatureCommandResult | null> {
const requestRevision = this.beginTargetCommand();
const featureIds = [...new Set(("featureIds" in input ? input.featureIds : [input.featureId]).map((id) => id.trim()))];
this.patchState({ targetResult: null, errorCode: null });
if (!featureIds.length || featureIds.length > MAX_MAP_FEATURE_IDS ||
featureIds.some((id) => !id || id.length > MAX_MAP_FEATURE_ID_LENGTH)) {
this.setError("INVALID_FEATURE_IDS");
return null;
}
const { sourceId } = input;
const target = { sourceId, featureId: featureIds[0] };
if (!this.requireTargetMap(target)) return null;
const dataDependent = DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.includes(
sourceId as (typeof DATA_DEPENDENT_GEOSERVER_SOURCE_IDS)[number]
);
const found = new Map<string, Feature>();
const uncachedIds = featureIds.filter((id) => {
const cached = dataDependent ? undefined : this.cachedFeatures.get(`${sourceId}:${id}`);
if (cached) found.set(id, cached);
return !cached;
});
this.pendingTargetSourceId = sourceId;
this.patchState({ pending: true });
try {
signal?.throwIfAborted();
if (uncachedIds.length) {
const collection = await this.fetchFeatures(sourceId, uncachedIds, signal);
if (requestRevision !== this.targetRequestRevision) return null;
signal?.throwIfAborted();
const requestedIds = new Set(uncachedIds);
for (const feature of collection.features) {
const id = String(feature.properties?.id ?? feature.properties?.device_id ?? feature.id);
if (!requestedIds.has(id) || !collectCoordinates(feature.geometry).length) continue;
found.set(id, feature);
if (!dataDependent) this.cachedFeatures.set(`${sourceId}:${id}`, feature);
}
}
} catch (error) {
if (requestRevision !== this.targetRequestRevision) return null;
if (signal?.aborted) throw error;
this.setError(sourceId === "scada" ? "SCADA_API_UNAVAILABLE" : "WFS_UNAVAILABLE");
return null;
} finally {
if (requestRevision === this.targetRequestRevision) {
this.pendingTargetSourceId = null;
this.patchState({ pending: false });
}
}
if (requestRevision !== this.targetRequestRevision) return null;
signal?.throwIfAborted();
const map = this.requireTargetMap(target);
if (!map) return null;
const result: FeatureCommandResult = {
sourceId, mode,
featureIds: featureIds.filter((id) => found.has(id)),
missingIds: featureIds.filter((id) => !found.has(id))
};
this.patchState({ targetResult: result });
if (!result.featureIds.length) {
this.setError("FEATURE_NOT_FOUND");
return null;
}
if (mode !== "highlight") {
const features = result.featureIds.map((id) => found.get(id)!);
const combined: Feature = features.length === 1 && durationOverride === undefined ? features[0] : {
type: "Feature", properties: {},
geometry: { type: "MultiPoint", coordinates: features.flatMap((feature) => collectCoordinates(feature.geometry)) }
};
if (!this.moveCameraToFeature(map, combined, durationOverride)) return null;
}
if (mode !== "zoom") {
this.setHighlights(result.featureIds.map((featureId) => ({ sourceId, featureId })));
}
this.patchState({ errorCode: null });
return result;
}
private async fetchFeatures(sourceId: WaterNetworkSourceId, featureIds?: string[], signal?: AbortSignal) {
@@ -631,15 +657,18 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
return collection;
}
private setHighlight(target: FeatureTarget) {
const map = this.requireTargetMap(target);
private setHighlights(targets: FeatureTarget[]) {
const map = this.options.getMap();
if (!map) return;
if (this.highlightedTarget && map.getSource(this.highlightedTarget.sourceId)) {
map.removeFeatureState(toFeatureStateTarget(this.highlightedTarget), "highlighted");
for (const target of this.state.targets) {
if (map.getSource(target.sourceId)) {
map.removeFeatureState(toFeatureStateTarget(target), "highlighted");
}
}
map.setFeatureState(toFeatureStateTarget(target), { highlighted: true });
this.highlightedTarget = target;
this.patchState({ target, errorCode: null });
for (const target of targets) {
map.setFeatureState(toFeatureStateTarget(target), { highlighted: true });
}
this.patchState({ target: targets[0] ?? null, targets, errorCode: null });
}
private requireMap() {