Skip to content

[Map layers]: add readOnly option to MapLayerOptions - #1831

Open
romradchenko wants to merge 5 commits into
iTwin:masterfrom
romradchenko:map-layers/read-only-option
Open

romradchenko wants to merge 5 commits into
iTwin:masterfrom
romradchenko:map-layers/read-only-option

Conversation

@romradchenko

@romradchenko romradchenko commented Oct 9, 2026 •

Copy link
Copy Markdown

Summary

Adds an optional MapLayerOptions.readOnly flag so desktop apps can keep the Map Layers widget visible while offline instead of hiding it.

Today iTwin Studio hides the widget when offline, because attach, base map provider change, sign-in and map settings need the network. With readOnly: true:

Kept Hidden / disabled
Map layers on/off switch Attach layer buttons
Layer visibility, Show all / Hide all / Invert Map settings popup
Base map visibility, transparency, color Base map provider select (disabled)
Layer transparency, Zoom to layer, sub-layer visibility Detach (toolbar, checkboxes, layer menu)
Sign-in (RequireAuth) buttons
Drag reorder

Map layer sources (MapLayerSources.create, MapLayerPreferences.getSources) are not loaded in read-only mode; they load again when the flag switches back to false.

Compatibility

readOnly is optional and defaults to false; no existing behavior, props or exports change. A test covers that all actions are still shown when it is not set.

Usage

<MapLayersWidget mapLayerOptions={{ readOnly: !isOnline }} />

Open question

Should read-only allow switching the base map to "Color fill" (works offline)? This PR disables the provider select entirely to keep the change minimal.

Testing

  • New tests in MapLayerManager.test.tsx (read-only mode: hidden actions, layer menu, visibility controls, no source loading, default unchanged).
  • pnpm build, pnpm test, pnpm lint, pnpm format, cspell pass locally.

@CLAassistant

CLAassistant commented Oct 9, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@romradchenko
romradchenko marked this pull request as ready for review October 9, 2026 18:51
Copilot AI balanced review requested due to automatic review settings October 9, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An in-flight source request can continue after switching to read-only mode, violating the offline-loading guarantee.

4 open findings
What changed in this PR

Adds an optional read-only mode to keep Map Layers usable offline while preventing network-dependent configuration changes.

Changes:

  • Hides or disables attach, detach, sign-in, reorder, settings, and provider controls.
  • Preserves visibility, transparency, zoom, color, and sub-layer controls.
  • Skips source loading and adds read-only tests and documentation.
File Description
MapLayersList.tsx Hides attach controls.
MapLayerSettingsMenu.tsx Hides detach action.
MapLayerManager.tsx Propagates read-only state.
MapLayerItem.tsx Hides selection and authentication controls.
MapLayerDroppable.tsx Disables reordering and attachment.
MapLayerActionButtons.tsx Hides selection and bulk detach.
BasemapPanel.tsx Disables provider selection.
Interfaces.ts Defines the public option.
useMapLayerSources.ts Skips source loading.
MapLayerManager.test.tsx Tests read-only behavior.
Changeset Documents the new option.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/map-layers/src/ui/hooks/map-layer/useMapLayerSources.ts
Comment thread .changeset/@itwin-map-layers-quiet-maps-rest.md Outdated
Comment thread packages/map-layers/src/test/MapLayerManager.test.tsx Outdated
Comment thread packages/map-layers/src/ui/Interfaces.ts Outdated
}, [mapLayerOptions?.baseMapLayers]);
const hideExternalMapLayersSection = mapLayerOptions?.hideExternalMapLayers ?? false;
const fetchPublicMapLayerSources = mapLayerOptions?.fetchPublicMapLayerSources ?? false;
const readOnly = mapLayerOptions?.readOnly ?? false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We work out readOnly here, and then again in 4 other components (MapLayerActionButtons, MapLayerDroppable, MapLayerItem, MapLayersList). Could we add readOnly: boolean to MapLayerListContext and set it once here? Then the others can just read context.readOnly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 14f6c97. readOnly is in the context now.

export function MapLayerItem(props: MapLayerItemProps) {
const context = useMapLayerListContext();
const readOnly = context.mapLayerOptions?.readOnly ?? false;
const signInOpenRef = React.useRef(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ref gets set or cleared in 5 places, plus there's an effect that watches readOnly. Most of that comes from the sign-in button block being copied twice in this file (same ~36 lines at the two RequireAuth spots).

Could we move it into a small MapLayerSignInButton component that closes its dialog when it unmounts, if it opened one? Then hiding the button in read-only mode closes the dialog on its own, and the effect isn't needed. AttachLayerPanel already closes its modal on unmount, so there's a precedent.

Or: do we need to close it at all? If you go offline in the middle of signing in, I'd expect the dialog to just fail to validate.

Nambot 🤖 (powered by Claude Opus 5.5)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 14f6c97. Moved to MapLayerSignInButton. It closes its dialog on unmount, so the effect is gone.

}

if (!isMounted.current) {
if (!isMounted.current || cancelled) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelled is also set when the component unmounts, because the cleanup runs then. So I think we can drop the isMounted checks in this effect and keep only cancelled.

Nambot 🤖 (powered by Claude Opus 5.5)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 14f6c97. Removed isMounted.

const isMounted = useIsMountedRef();

React.useEffect(() => {
if (skipLoading) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small bug: if read-only turns on while sources are loading, cancelled skips setLoadingSources(false), and this early return never resets it either. So loadingSources stays true. You can't see it in read-only mode, but the state is wrong. Maybe call setLoadingSources(false) before the return?

Also, skipLoading could be required. There's only one caller, and the other args are required?

Nambot 🤖 (powered by Claude Opus 5.5)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 14f6c97, with a test. skipLoading is required now.

disabled={props.disabled || props.readOnly}
// Use key to reset internal state of Select when disabled changes, ensuring the dropdown menu is responsive.
key={String(props.disabled)}
key={String(props.disabled || props.readOnly)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The String() is fine, since key can't be a boolean. But props.disabled || props.readOnly is written twice. Could we put it in one const and use that for both disabled and key?

Nambot 🤖 (powered by Claude Opus 5.5)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 14f6c97.

<MenuItem key={1} onClick={() => handleRemoveLayer()}>
{labelDetach}
</MenuItem>,
...(readOnly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the spread with an empty array is a bit hard to read. Maybe put each menu item in a named const first, then build the list as [zoomItem, ...(readOnly ? [] : [detachItem]), sliderItem]?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 14f6c97.

"@itwin/map-layers": minor
---

Added `MapLayerOptions.readOnly` to show the Map Layers widget in read-only mode, e.g. while the application is offline. Attached layers can still be shown, hidden and made transparent, but layers cannot be attached, detached, reordered or signed in to, and the base map provider and map settings cannot be changed. Base map visibility, transparency and color stay editable. Map layer sources are not loaded. Defaults to `false`, so existing consumers are not affected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this repeats most of the doc comment from Interfaces.ts. One sentence plus the example is probably enough

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 14f6c97.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants