From c84df6e86acf83a07006f89c7b05118560be7311 Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Tue, 25 Aug 2026 13:27:56 -0500 Subject: [PATCH 1/4] Add a Compare layer entry to the layers list Each layer's three-dots menu offers "Compare layer", which hands that layer to the Comparison plugin as the first of the two sides it reads against each other. The hand-off is announced on the mmgisAPI bus rather than called directly: the layers list knows nothing about Comparison beyond the name of the event, and a mission without that plugin simply has nobody listening. --- .../LayerManager/MMGISLayerManagerAdapter.tsx | 2 + .../__tests__/LayerManagerPanel.spec.tsx | 44 +++++++++++++++++++ .../LayerManager/__tests__/handlers.spec.js | 16 +++++++ .../Tools/LayerManager/adapters/handlers.ts | 12 +++++ .../lib/geo/LayerLegend/LayerLegend.tsx | 19 ++++++++ .../geo/LayerLegendList/LayerLegendList.tsx | 3 ++ .../LayerManagerPanel/LayerManagerPanel.tsx | 3 ++ .../lib/geo/icons/compare-layer.svg | 3 ++ .../styles/components-geo/popover-menu.scss | 5 +++ 9 files changed, 107 insertions(+) create mode 100644 src/essence/Tools/LayerManager/lib/geo/icons/compare-layer.svg diff --git a/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx b/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx index 80b802ca1..d3469f9e1 100644 --- a/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx +++ b/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx @@ -13,6 +13,7 @@ import { setColormap, setRescale, zoomToLayer, + compareLayer, } from './adapters/handlers' import { mmgisGetLayerBounds } from '../_shared/adapters/mmgisAPI' @@ -76,6 +77,7 @@ export function MMGISLayerManagerAdapter() { onRescaleChange={(id, mn, mx) => { report('setRescale', setRescale(id, mn, mx, refresh)) }} onZoomToLayer={(id) => { report('zoomToLayer', zoomToLayer(id)) }} canZoomToLayer={canZoomToLayer} + onCompareLayer={compareLayer} /> ) } diff --git a/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx b/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx index d49578a36..a1b888181 100644 --- a/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx +++ b/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx @@ -64,6 +64,15 @@ const titlesIn = (container: HTMLElement) => const rampButtonsIn = (container: HTMLElement) => container.querySelectorAll('[title="Change color ramp"]') +/** Open a row's kebab and read its items out of the portal they render into. */ +const openMenu = async (container: HTMLElement) => { + await click(container.querySelector('[title="More options"]')!) + return Array.from(document.body.querySelectorAll('[role="menuitem"]')) +} + +const menuItem = (items: Element[], label: string) => + items.find((el) => el.textContent?.includes(label)) + beforeEach(() => { // Deleted rather than stubbed, so calling through either one throws. delete (window as { mmgisAPI?: unknown }).mmgisAPI @@ -154,6 +163,41 @@ describe('LayerManagerPanel without a host', () => { await unmount() }) + test('offers the comparison hand-off only when the host wires one', async () => { + const onCompareLayer = vi.fn() + const wired = await mount( + , + ) + await click(menuItem(await openMenu(wired.container), 'Compare layer')!) + expect(onCompareLayer).toHaveBeenCalledWith(GRADIENT_LAYER.id) + await wired.unmount() + + const unwired = await mount() + expect(menuItem(await openMenu(unwired.container), 'Compare layer')) + .toBeUndefined() + await unwired.unmount() + }) + + test('holds the comparison hand-off shut for a layer that is switched off', async () => { + const onCompareLayer = vi.fn() + const { container, unmount } = await mount( + , + ) + + const item = menuItem(await openMenu(container), 'Compare layer')! + expect(item.getAttribute('aria-disabled')).toBe('true') + expect(item.getAttribute('title')).toBe('Turn this layer on to compare it') + await click(item) + expect(onCompareLayer).not.toHaveBeenCalled() + await unmount() + }) + test('renders without any callbacks wired', async () => { const { container, unmount } = await mount( , diff --git a/src/essence/Tools/LayerManager/__tests__/handlers.spec.js b/src/essence/Tools/LayerManager/__tests__/handlers.spec.js index a1e56e17b..52b581071 100644 --- a/src/essence/Tools/LayerManager/__tests__/handlers.spec.js +++ b/src/essence/Tools/LayerManager/__tests__/handlers.spec.js @@ -5,6 +5,7 @@ import { setColormap, setRescale, zoomToLayer, + compareLayer, } from '../adapters/handlers.ts' import { ZOOM_TO_LAYER_PADDING, @@ -260,4 +261,19 @@ test.describe('handlers', () => { expect(requests).toHaveLength(0) warn.mockRestore() }) + + // Announced, not called: the layers list names the event and nothing else, + // so a mission without the Comparison plugin simply has nobody listening. + test('compareLayer announces the layer on the bus', () => { + const { emitCalls, requests } = setupMock() + compareLayer('layerA') + + expect(emitCalls).toEqual([ + { + event: 'plugin:comparison:startWithLayer', + payload: { layerId: 'layerA' }, + }, + ]) + expect(requests).toHaveLength(0) + }) }) diff --git a/src/essence/Tools/LayerManager/adapters/handlers.ts b/src/essence/Tools/LayerManager/adapters/handlers.ts index 45e46b471..0a9cc1a3b 100644 --- a/src/essence/Tools/LayerManager/adapters/handlers.ts +++ b/src/essence/Tools/LayerManager/adapters/handlers.ts @@ -65,6 +65,18 @@ export const zoomToLayer = async (layerId: string): Promise => { }) } +/** + * Hands a layer to the Comparison plugin as the first of the two sides it + * swipes between. + * + * Announced on the bus rather than called: the layers list knows nothing about + * Comparison beyond the name of the event, and a mission without that plugin + * simply has nobody listening. + */ +export const compareLayer = (layerId: string): void => { + mmgisEmit('plugin:comparison:startWithLayer', { layerId }) +} + export const setColormap = async (layerId: string, colormap: string, refresh: Refresh): Promise => { if (!(await canChangeColormap(layerId))) return await mmgisRequest('layers:updateConfig', { layerUUID: layerId, updates: { currentCogColormap: colormap } }) diff --git a/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx b/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx index a1c8476bc..2adf0dfe0 100644 --- a/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx +++ b/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx @@ -44,6 +44,7 @@ export type LayerLegendProps = { onRescaleChange?: (layerId: string, min: number, max: number) => void onZoomToLayer?: (layerId: string) => void canZoomToLayer?: (layerId: string) => Promise + onCompareLayer?: (layerId: string) => void } export function LayerLegend({ @@ -56,6 +57,7 @@ export function LayerLegend({ onRescaleChange, onZoomToLayer, canZoomToLayer, + onCompareLayer, }: LayerLegendProps) { const { id, @@ -162,6 +164,23 @@ export function LayerLegend({ : undefined, onSelect: () => onZoomToLayer?.(id), }, + // A comparison shows one layer against another, so the layer has to be + // drawn to be one of the two. Offered as inert rather than hidden, so + // the way to reach it stays visible on a layer that is switched off. + ...(onCompareLayer + ? [ + { + id: 'compare-layer', + label: 'Compare layer', + icon: 'compare-layer', + disabled: !isVisible, + title: !isVisible + ? 'Turn this layer on to compare it' + : undefined, + onSelect: () => onCompareLayer(id), + } satisfies PopoverMenuItem, + ] + : []), ] const handleVisibilityToggle = () => { diff --git a/src/essence/Tools/LayerManager/lib/geo/LayerLegendList/LayerLegendList.tsx b/src/essence/Tools/LayerManager/lib/geo/LayerLegendList/LayerLegendList.tsx index 9a110cba3..e94559881 100644 --- a/src/essence/Tools/LayerManager/lib/geo/LayerLegendList/LayerLegendList.tsx +++ b/src/essence/Tools/LayerManager/lib/geo/LayerLegendList/LayerLegendList.tsx @@ -12,6 +12,7 @@ export type LayerLegendListProps = { onRescaleChange?: LayerLegendProps['onRescaleChange'] onZoomToLayer?: LayerLegendProps['onZoomToLayer'] canZoomToLayer?: LayerLegendProps['canZoomToLayer'] + onCompareLayer?: LayerLegendProps['onCompareLayer'] } export function LayerLegendList({ @@ -24,6 +25,7 @@ export function LayerLegendList({ onRescaleChange, onZoomToLayer, canZoomToLayer, + onCompareLayer, }: LayerLegendListProps) { if (!layers || layers.length === 0) { return ( @@ -45,6 +47,7 @@ export function LayerLegendList({ onRescaleChange={onRescaleChange} onZoomToLayer={onZoomToLayer} canZoomToLayer={canZoomToLayer} + onCompareLayer={onCompareLayer} /> ))} diff --git a/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx b/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx index c930c16c7..75ca15ad5 100644 --- a/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx +++ b/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx @@ -17,6 +17,7 @@ export type LayerManagerPanelProps = { onRescaleChange?: (layerId: string, min: number, max: number) => void onZoomToLayer?: LayerLegendListProps['onZoomToLayer'] canZoomToLayer?: LayerLegendListProps['canZoomToLayer'] + onCompareLayer?: LayerLegendListProps['onCompareLayer'] } export function LayerManagerPanel({ @@ -30,6 +31,7 @@ export function LayerManagerPanel({ onRescaleChange, onZoomToLayer, canZoomToLayer, + onCompareLayer, }: LayerManagerPanelProps) { return (
@@ -49,6 +51,7 @@ export function LayerManagerPanel({ onRescaleChange={onRescaleChange} onZoomToLayer={onZoomToLayer} canZoomToLayer={canZoomToLayer} + onCompareLayer={onCompareLayer} /> )}
diff --git a/src/essence/Tools/LayerManager/lib/geo/icons/compare-layer.svg b/src/essence/Tools/LayerManager/lib/geo/icons/compare-layer.svg new file mode 100644 index 000000000..d10fb1254 --- /dev/null +++ b/src/essence/Tools/LayerManager/lib/geo/icons/compare-layer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/essence/Tools/LayerManager/lib/styles/components-geo/popover-menu.scss b/src/essence/Tools/LayerManager/lib/styles/components-geo/popover-menu.scss index 79e75950f..6242155c8 100644 --- a/src/essence/Tools/LayerManager/lib/styles/components-geo/popover-menu.scss +++ b/src/essence/Tools/LayerManager/lib/styles/components-geo/popover-menu.scss @@ -85,5 +85,10 @@ -webkit-mask-image: url('../../geo/icons/zoom-to-layer.svg'); mask-image: url('../../geo/icons/zoom-to-layer.svg'); } + + &--compare-layer { + -webkit-mask-image: url('../../geo/icons/compare-layer.svg'); + mask-image: url('../../geo/icons/compare-layer.svg'); + } } } From abb5d30b718320ef935bf5d21cdf92df5d86a472 Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Tue, 25 Aug 2026 14:18:20 -0500 Subject: [PATCH 2/4] Add buttons to add temp layer and show layer compare tool in layer manager --- src/essence/Tools/AddTempLayer/config.json | 8 --- .../LayerManager/MMGISLayerManagerAdapter.tsx | 9 +++- .../__tests__/LayerManagerPanel.spec.tsx | 22 ++++++++ .../LayerManagerPanel/LayerManagerPanel.tsx | 25 ++++++++++ .../Tools/LayerManager/lib/geo/icons/add.svg | 4 ++ .../components-geo/layer-manager-panel.scss | 50 +++++++++++++++++++ 6 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/essence/Tools/LayerManager/lib/geo/icons/add.svg diff --git a/src/essence/Tools/AddTempLayer/config.json b/src/essence/Tools/AddTempLayer/config.json index 92d33220a..5277f7748 100644 --- a/src/essence/Tools/AddTempLayer/config.json +++ b/src/essence/Tools/AddTempLayer/config.json @@ -12,14 +12,6 @@ }, "metadata": { "icon": "add", - "compatiblePositions": [ - "float-top-left", - "float-top-center", - "float-top-right", - "float-bottom-left", - "float-bottom-center", - "float-bottom-right" - ], "preferredPosition": "float-top-right", "startHidden": true, "modernLayoutSupport": true diff --git a/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx b/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx index d3469f9e1..c73c15926 100644 --- a/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx +++ b/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx @@ -15,10 +15,12 @@ import { zoomToLayer, compareLayer, } from './adapters/handlers' -import { mmgisGetLayerBounds } from '../_shared/adapters/mmgisAPI' +import { mmgisEmit, mmgisGetLayerBounds } from '../_shared/adapters/mmgisAPI' type ToolVars = { showOnlyVisible?: boolean; width?: number } +const ADD_TEMP_LAYER_PLUGIN_ID = 'AddTempLayerTool' + // Panel controls are event callbacks and cannot await the requests they fire, // so a rejected one would surface only as an unhandled rejection. Log it // against the action that produced it instead. @@ -47,6 +49,10 @@ export function MMGISLayerManagerAdapter() { } }, [toolVars.showOnlyVisible]) + const showAddLayer = useCallback(() => { + mmgisEmit('core:showPlugin', { pluginId: ADD_TEMP_LAYER_PLUGIN_ID }) + }, []) + // Whether the layer has somewhere to zoom to. Core answers null both for a // layer with no extent and for a core too old to know the question, and // either way the action leads nowhere. @@ -78,6 +84,7 @@ export function MMGISLayerManagerAdapter() { onZoomToLayer={(id) => { report('zoomToLayer', zoomToLayer(id)) }} canZoomToLayer={canZoomToLayer} onCompareLayer={compareLayer} + onAddLayer={showAddLayer} /> ) } diff --git a/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx b/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx index a1b888181..8f20733fb 100644 --- a/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx +++ b/src/essence/Tools/LayerManager/__tests__/LayerManagerPanel.spec.tsx @@ -112,6 +112,28 @@ describe('LayerManagerPanel without a host', () => { await unmount() }) + test('omits the add-layer button unless the host handles it', async () => { + const { container, unmount } = await mount( + , + ) + expect(container.querySelector('.blocks-layer-manager__add-layer')).toBeNull() + await unmount() + }) + + test('reports add-layer clicks through its callback', async () => { + const onAddLayer = vi.fn() + const { container, unmount } = await mount( + , + ) + + const button = container.querySelector('.blocks-layer-manager__add-layer')! + expect(button.textContent).toContain('Add layer from URL') + await click(button) + + expect(onAddLayer).toHaveBeenCalledTimes(1) + await unmount() + }) + test('reports visibility changes through its callback', async () => { const onVisibilityChange = vi.fn() const { container, unmount } = await mount( diff --git a/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx b/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx index 75ca15ad5..6c1dfbe69 100644 --- a/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx +++ b/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx @@ -18,6 +18,13 @@ export type LayerManagerPanelProps = { onZoomToLayer?: LayerLegendListProps['onZoomToLayer'] canZoomToLayer?: LayerLegendListProps['canZoomToLayer'] onCompareLayer?: LayerLegendListProps['onCompareLayer'] + /** + * Reveal the host's "add layer from URL" surface. The panel owns no such + * form of its own — the host decides what the button opens. The button is + * left out entirely when no handler is given. + */ + onAddLayer?: () => void + addLayerLabel?: string } export function LayerManagerPanel({ @@ -32,9 +39,27 @@ export function LayerManagerPanel({ onZoomToLayer, canZoomToLayer, onCompareLayer, + onAddLayer, + addLayerLabel = 'Add layer from URL', }: LayerManagerPanelProps) { return (
+ {onAddLayer && ( +
+ +
+ )}
{loading ? (
diff --git a/src/essence/Tools/LayerManager/lib/geo/icons/add.svg b/src/essence/Tools/LayerManager/lib/geo/icons/add.svg new file mode 100644 index 000000000..f494656c6 --- /dev/null +++ b/src/essence/Tools/LayerManager/lib/geo/icons/add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/essence/Tools/LayerManager/lib/styles/components-geo/layer-manager-panel.scss b/src/essence/Tools/LayerManager/lib/styles/components-geo/layer-manager-panel.scss index 965b5d10d..3768e3592 100644 --- a/src/essence/Tools/LayerManager/lib/styles/components-geo/layer-manager-panel.scss +++ b/src/essence/Tools/LayerManager/lib/styles/components-geo/layer-manager-panel.scss @@ -8,8 +8,58 @@ color: var(--theme-color-ink, #1b1b1b); font-family: 'Lato', sans-serif; + /* "Add layer from URL" trigger, pinned above the scrolling legend list */ + &__header { + flex: 0 0 auto; + padding: var(--theme-spacing-1, 0.5rem); + } + + &__add-layer { + display: flex; + align-items: center; + justify-content: center; + gap: var(--theme-spacing-05, 0.25rem); + width: 100%; + padding: var(--theme-spacing-1, 0.5rem); + background: transparent; + border: 1px solid var(--theme-color-primary-lighter, #c4dbfa); + border-radius: var(--theme-radius-md, 4px); + color: var(--theme-color-primary, #137480); + font-family: inherit; + font-size: var(--theme-font-size-2xs, 14px); + font-weight: var(--theme-font-weight-normal, 400); + cursor: pointer; + transition: background 0.12s ease; + + &:hover { + background: var(--theme-color-primary-lightest, #eaf0fd); + } + + &:focus-visible { + outline: 2px solid var(--theme-color-primary, #137480); + outline-offset: 1px; + } + } + + &__add-layer-icon { + flex: 0 0 auto; + width: 16px; + height: 16px; + background-color: currentColor; + -webkit-mask-image: url('../../geo/icons/add.svg'); + mask-image: url('../../geo/icons/add.svg'); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + } + &__content { flex: 1; + /* Scrolls under the header rather than pushing the panel taller */ + min-height: 0; /* Inset so hovered legend cards don't butt against the panel edges */ padding: var(--theme-spacing-1, 0.5rem); overflow-y: auto; From 7e954c3be8a96985f2d611dc824106a7b954dddc Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Mon, 31 Aug 2026 09:39:51 -0500 Subject: [PATCH 3/4] Place AddTempLayer in the top-right floating panel The modern runtime reads a tool's placement metadata only from its mission config entry, which the generator copies from the manifest. AddTempLayer declared no `defaults` block, so the generator skipped it and no mission carried an entry to place. Declare the block, restore the float-only compatiblePositions that panel assignment checks a position against, and give the tool to the demo's float-top-right panel. --- mission-profiles/full-demo.json | 4 +++- .../generated/full-demo-mission.json | 24 ++++++++++++++++++- src/essence/Tools/AddTempLayer/config.json | 11 +++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/mission-profiles/full-demo.json b/mission-profiles/full-demo.json index bee6b2d38..eaa1161cc 100644 --- a/mission-profiles/full-demo.json +++ b/mission-profiles/full-demo.json @@ -21,6 +21,7 @@ "on": [ "Title", "LayerManager", + "AddTempLayer", "AOI", "Card", "Chart", @@ -210,7 +211,8 @@ "defaultState": "expanded" }, "panelTools": [ - "ShareExport" + "ShareExport", + "AddTempLayer" ], "id": "float-share" }, diff --git a/mission-profiles/generated/full-demo-mission.json b/mission-profiles/generated/full-demo-mission.json index 73838da09..7ea998a94 100644 --- a/mission-profiles/generated/full-demo-mission.json +++ b/mission-profiles/generated/full-demo-mission.json @@ -161,7 +161,8 @@ "defaultState": "expanded" }, "panelTools": [ - "ShareExport" + "ShareExport", + "AddTempLayer" ], "id": "float-share" }, @@ -316,6 +317,27 @@ "justification": "left" } }, + { + "name": "AddTempLayer", + "icon": "add", + "js": "AddTempLayerTool", + "on": true, + "variables": {}, + "metadata": { + "icon": "add", + "compatiblePositions": [ + "float-top-left", + "float-top-center", + "float-top-right", + "float-bottom-left", + "float-bottom-center", + "float-bottom-right" + ], + "preferredPosition": "float-top-right", + "startHidden": true, + "modernLayoutSupport": true + } + }, { "name": "Chart", "icon": "chart-bar", diff --git a/src/essence/Tools/AddTempLayer/config.json b/src/essence/Tools/AddTempLayer/config.json index 5277f7748..0766cf6cc 100644 --- a/src/essence/Tools/AddTempLayer/config.json +++ b/src/essence/Tools/AddTempLayer/config.json @@ -1,4 +1,7 @@ { + "defaults": { + "variables": {} + }, "defaultIcon": "add", "description": "Add an external layer (WMS/WMTS/XYZ/GeoJSON) to the map for the current session.", "descriptionFull": { @@ -12,6 +15,14 @@ }, "metadata": { "icon": "add", + "compatiblePositions": [ + "float-top-left", + "float-top-center", + "float-top-right", + "float-bottom-left", + "float-bottom-center", + "float-bottom-right" + ], "preferredPosition": "float-top-right", "startHidden": true, "modernLayoutSupport": true From f5d1415d1fe36a0d11b295cee406908430a7807c Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Mon, 31 Aug 2026 09:39:57 -0500 Subject: [PATCH 4/4] Reveal the add-layer form through the plugin lifecycle The add-layer button emitted `core:showPlugin`, a command the request-bus rewrite removed; nothing listens for that event any more, so the button did nothing. Command `plugins:show` instead, which loads the tool when a mission starts it unloaded and answers with a refusal the caller can log. AddTempLayer's own `addTempLayer:show` event cannot serve here: it is listened for only while that tool is mounted. Cover the command and its refusal path. Drop the addLayerLabel prop, which was never passed, and the tooltip that repeated the button's own label. --- .../LayerManager/MMGISLayerManagerAdapter.tsx | 9 ++---- .../LayerManager/__tests__/handlers.spec.js | 29 +++++++++++++++++-- .../Tools/LayerManager/adapters/handlers.ts | 26 +++++++++++++---- .../lib/geo/LayerLegend/LayerLegend.tsx | 6 ++-- .../LayerManagerPanel/LayerManagerPanel.tsx | 11 ++----- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx b/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx index c73c15926..95b809306 100644 --- a/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx +++ b/src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx @@ -14,13 +14,12 @@ import { setRescale, zoomToLayer, compareLayer, + showAddLayer, } from './adapters/handlers' -import { mmgisEmit, mmgisGetLayerBounds } from '../_shared/adapters/mmgisAPI' +import { mmgisGetLayerBounds } from '../_shared/adapters/mmgisAPI' type ToolVars = { showOnlyVisible?: boolean; width?: number } -const ADD_TEMP_LAYER_PLUGIN_ID = 'AddTempLayerTool' - // Panel controls are event callbacks and cannot await the requests they fire, // so a rejected one would surface only as an unhandled rejection. Log it // against the action that produced it instead. @@ -49,10 +48,6 @@ export function MMGISLayerManagerAdapter() { } }, [toolVars.showOnlyVisible]) - const showAddLayer = useCallback(() => { - mmgisEmit('core:showPlugin', { pluginId: ADD_TEMP_LAYER_PLUGIN_ID }) - }, []) - // Whether the layer has somewhere to zoom to. Core answers null both for a // layer with no extent and for a core too old to know the question, and // either way the action leads nowhere. diff --git a/src/essence/Tools/LayerManager/__tests__/handlers.spec.js b/src/essence/Tools/LayerManager/__tests__/handlers.spec.js index 52b581071..241163e09 100644 --- a/src/essence/Tools/LayerManager/__tests__/handlers.spec.js +++ b/src/essence/Tools/LayerManager/__tests__/handlers.spec.js @@ -6,6 +6,7 @@ import { setRescale, zoomToLayer, compareLayer, + showAddLayer, } from '../adapters/handlers.ts' import { ZOOM_TO_LAYER_PADDING, @@ -30,6 +31,9 @@ const setupMock = (responses = {}, emitCalls = []) => { return { emitCalls, requests } } +// showAddLayer fires its request without returning it. +const flush = () => new Promise((resolve) => setTimeout(resolve, 0)) + const EDITABLE = { hasColormap: true, canChangeColormap: true } // What an image layer reports: a ramp to show, but nothing to change. const READ_ONLY = { hasColormap: true, canChangeColormap: false } @@ -262,8 +266,6 @@ test.describe('handlers', () => { warn.mockRestore() }) - // Announced, not called: the layers list names the event and nothing else, - // so a mission without the Comparison plugin simply has nobody listening. test('compareLayer announces the layer on the bus', () => { const { emitCalls, requests } = setupMock() compareLayer('layerA') @@ -276,4 +278,27 @@ test.describe('handlers', () => { ]) expect(requests).toHaveLength(0) }) + + test('showAddLayer commands the layout to reveal the form', async () => { + const { emitCalls, requests } = setupMock({ + 'plugins:show': { ok: true, state: 'visible', changed: true }, + }) + showAddLayer() + await flush() + + expect(requests).toEqual([ + { name: 'plugins:show', params: { pluginId: 'AddTempLayerTool' } }, + ]) + expect(emitCalls).toHaveLength(0) + }) + + test('showAddLayer logs a refusal instead of dropping it', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + setupMock({ 'plugins:show': { ok: false, reason: 'not-found' } }) + showAddLayer() + await flush() + + expect(warn).toHaveBeenCalledWith(expect.stringContaining('not-found')) + warn.mockRestore() + }) }) diff --git a/src/essence/Tools/LayerManager/adapters/handlers.ts b/src/essence/Tools/LayerManager/adapters/handlers.ts index 0a9cc1a3b..c349bc94a 100644 --- a/src/essence/Tools/LayerManager/adapters/handlers.ts +++ b/src/essence/Tools/LayerManager/adapters/handlers.ts @@ -1,6 +1,7 @@ import { mmgisRequest, mmgisEmit, + mmgisShowPlugin, mmgisGetLayerCogCapabilities, mmgisGetLayerBounds, mmgisFitBounds, @@ -67,16 +68,31 @@ export const zoomToLayer = async (layerId: string): Promise => { /** * Hands a layer to the Comparison plugin as the first of the two sides it - * swipes between. - * - * Announced on the bus rather than called: the layers list knows nothing about - * Comparison beyond the name of the event, and a mission without that plugin - * simply has nobody listening. + * swipes between. A mission without that plugin has nobody listening. */ export const compareLayer = (layerId: string): void => { mmgisEmit('plugin:comparison:startWithLayer', { layerId }) } +export const ADD_LAYER_PLUGIN_ID = 'AddTempLayerTool' + +/** + * Reveals the "add layer from URL" form, loading the tool first if the mission + * starts it unloaded. Its own `addTempLayer:show` event would not reach it + * there, being listened for only while the tool is mounted. + */ +export const showAddLayer = (): void => { + // Widened from CommandResult: without strictNullChecks a boolean + // discriminant does not narrow, so `reason` is unreachable on the union. + mmgisShowPlugin(ADD_LAYER_PLUGIN_ID) + .then((result: { ok: boolean; reason?: string }) => { + if (!result.ok) { + console.warn(`LayerManager: showAddLayer refused: ${result.reason}`) + } + }) + .catch((err) => console.warn('LayerManager: showAddLayer failed', err)) +} + export const setColormap = async (layerId: string, colormap: string, refresh: Refresh): Promise => { if (!(await canChangeColormap(layerId))) return await mmgisRequest('layers:updateConfig', { layerUUID: layerId, updates: { currentCogColormap: colormap } }) diff --git a/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx b/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx index 2adf0dfe0..c6ef0bf15 100644 --- a/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx +++ b/src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx @@ -164,9 +164,9 @@ export function LayerLegend({ : undefined, onSelect: () => onZoomToLayer?.(id), }, - // A comparison shows one layer against another, so the layer has to be - // drawn to be one of the two. Offered as inert rather than hidden, so - // the way to reach it stays visible on a layer that is switched off. + // Inert rather than absent on a layer that is switched off: a + // comparison reads two drawn layers against each other, and the way to + // reach it should stay visible meanwhile. ...(onCompareLayer ? [ { diff --git a/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx b/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx index 6c1dfbe69..7bb77f4a2 100644 --- a/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx +++ b/src/essence/Tools/LayerManager/lib/geo/LayerManagerPanel/LayerManagerPanel.tsx @@ -18,13 +18,8 @@ export type LayerManagerPanelProps = { onZoomToLayer?: LayerLegendListProps['onZoomToLayer'] canZoomToLayer?: LayerLegendListProps['canZoomToLayer'] onCompareLayer?: LayerLegendListProps['onCompareLayer'] - /** - * Reveal the host's "add layer from URL" surface. The panel owns no such - * form of its own — the host decides what the button opens. The button is - * left out entirely when no handler is given. - */ + /** Opens the host's "add layer" surface. No handler, no button. */ onAddLayer?: () => void - addLayerLabel?: string } export function LayerManagerPanel({ @@ -40,7 +35,6 @@ export function LayerManagerPanel({ canZoomToLayer, onCompareLayer, onAddLayer, - addLayerLabel = 'Add layer from URL', }: LayerManagerPanelProps) { return (
@@ -50,13 +44,12 @@ export function LayerManagerPanel({ type="button" className="blocks-layer-manager__add-layer" onClick={onAddLayer} - title={addLayerLabel} >
)}