Skip to content

[WC-3450] Complete LeafletMap MobX migration with ViewModel#2276

Open
iobuhov wants to merge 28 commits into
mainfrom
3429/complete-mobx-migration
Open

[WC-3450] Complete LeafletMap MobX migration with ViewModel#2276
iobuhov wants to merge 28 commits into
mainfrom
3429/complete-mobx-migration

Conversation

@iobuhov

@iobuhov iobuhov commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Pull request type

Refactoring (e.g. file rename, variable rename, etc.)


Description

This PR completes the MobX migration for the LeafletMap component by:

Key Changes:

  • ✨ Created LeafletMapViewModel to handle all map logic with MobX reactions
  • 🔨 Simplified LeafletMap.tsx component (removed 184 lines) to focus on rendering
  • 📦 Extracted marker creation logic into src/utils/leaflet-markers.ts
  • 🏗️ Set up proper DI with useLeafletMapVM injection hook
  • 🧪 Updated tests to work with new ViewModel architecture
  • 🎨 Fixed all linting issues (import ordering, return types)

Architecture:

  • ViewModel (LeafletMapViewModel): Manages map instance, tile layer, markers, and reactivity
  • Component (LeafletMap): Pure presentational component using ref callback for setup
  • Services: Integrated with LocationResolverService and CurrentLocationService
  • Reactive updates: MobX reaction syncs markers when locations change

Testing:

  • ✅ All 90 tests passing
  • ✅ 87% code coverage maintained
  • ✅ Linting clean (no errors or warnings)

Impact:

  • 18 files changed
  • 311 insertions(+), 284 deletions(-)
  • Better separation of concerns
  • More maintainable and testable code

This PR builds on the existing MobX infrastructure and completes the migration started in the parent branch.


What should be covered while testing?

  1. Map Rendering: Verify the map renders correctly with markers
  2. Marker Updates: Check that markers update reactively when data changes
  3. Auto Zoom: Test automatic zoom behavior with multiple markers
  4. Manual Zoom: Verify manual zoom controls work correctly
  5. Current Location: Test current location marker appears and updates
  6. Map Interactions: Drag, scroll, and zoom controls function properly
  7. Different Providers: Test with both OpenStreetMap and other providers

Test in Mendix Studio Pro:

  • Add Maps widget to a page with data source
  • Verify markers appear at correct locations
  • Test adding/removing markers dynamically
  • Check zoom and pan behavior
  • Verify no console errors or memory leaks

🤖 Generated with Claude Code

@iobuhov iobuhov requested a review from a team as a code owner June 18, 2026 14:25
@iobuhov iobuhov changed the title [WC-3429] Complete LeafletMap MobX migration with ViewModel [WC-3450] Complete LeafletMap MobX migration with ViewModel Jun 18, 2026
Base automatically changed from 3429/migrate-maps-to-mobx to main June 24, 2026 14:51
@github-actions

This comment has been minimized.

Comment on lines +11 to +13
- We migrated the widget's internal state management to a MobX container architecture, in line with other data widgets.

- We replaced the react-leaflet wrapper with a direct Leaflet integration, reducing dependencies while keeping the same map behavior.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is too technical and internal, I believe. We describe to customers, so what is changing from their perspective and what they can expect.

r0b1n
r0b1n previously approved these changes Jun 29, 2026
iobuhov and others added 22 commits June 30, 2026 09:40
- Replace all setTimeout/Promise delays with MobX when() helper
- Mock convertAddressToLatLng at module level for deterministic tests
- Add waitForLocations() helper using when() for observable changes
- Configure MobX with enforceActions: 'never' for testing
- Add timeout handling for error cases
- Improve test reliability and speed (3.5s vs 13s)
- All 42 tests still passing with 100% model layer coverage
Split 598-line test file into 3 self-contained files:
- LocationResolver.unit.spec.ts (247 lines) - Basic functionality, empty inputs, API keys
- LocationResolver.integration.spec.ts (321 lines) - Mixed markers, caching, errors, integration
- LocationResolver.reactivity.spec.ts (226 lines) - MobX reactions and observable behavior

Benefits:
- Each file self-contained with inline setup (no helpers folder)
- Follows Gallery/Datagrid patterns in the repo
- Easy to locate specific test types
- Can run test files independently
- No abstraction layers to learn
- Clear test intent without jumping to other files

All 45 tests passing with 100% model layer coverage maintained
Add 26 unit tests for convertStaticModeledMarker and convertDynamicModeledMarker:

convertStaticModeledMarker (5 tests):
- All fields present
- Undefined optional fields
- Number parsing with comma/period decimal separators
- Custom marker image handling

convertDynamicModeledMarker (21 tests):
- Datasource availability (undefined, Loading, Unavailable, empty)
- Coordinates location type (single/multiple markers, missing attributes)
- Address location type (with/without address attribute)
- Optional fields (title, onClick action, custom marker)
- Edge cases (item IDs, NaN handling, empty strings, mixed attributes)

Test results:
- 26/26 tests passing
- 100% code coverage on data.ts
- Self-contained tests using @mendix/widget-plugin-test-utils
- Uses list(), obj(), listAttribute(), listAction(), dynamic() helpers
Remove test 'should handle NaN from invalid coordinate strings' because:
- Dynamic markers use ListAttributeValue<Big>, not string attributes
- Mendix runtime ensures type safety - we never receive invalid coordinate types
- The scenario being tested doesn't occur in practice

Tests: 70 passed (was 71)
Coverage: Still 100% on data.ts
Remove 'should handle multiple markers with different attributes' test because:
- Already covered by 'should convert multiple markers with coordinates' test
- Doesn't add unique value - tests same scenario with different attribute values
- Simplifies test suite without losing coverage

Tests: 69 passed (was 70)
Coverage: Still 100% on data.ts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Wire MapsContainer into Maps.tsx via useMapsContainer + ContainerProvider
- Add MapsWidget observer component reading gate props and services
- Add CurrentLocationService (reactive showCurrentLocation handling,
  stale-request guard, clears location when disabled)
- Add injection-hooks following the gallery-web pattern
- Rewrite LeafletMap on the imperative Leaflet API; drop react-leaflet
  and @types/react-leaflet; add explicit mobx + mobx-react-lite
- Remove legacy useLocationResolver from utils/geodecode.ts
- Replace react-leaflet snapshots with structural LeafletMap tests (15),
  add CurrentLocationService tests (6) and Maps integration tests (2)
- Add OpenSpec change complete-mobx-migration (tdd-refactor schema)

Tests: 77 passed across 9 suites; tsc and eslint clean; Maps.mpk builds

Co-Authored-By: Claude <noreply@anthropic.com>
…ct marker utils, fix linting

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ived in maps-web)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…emove baseMapLayer utility

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… cleanup

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
iobuhov and others added 6 commits June 30, 2026 09:45
…/tile-layer.ts

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…fect

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tile-layer utility

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…c apiKey

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@iobuhov iobuhov force-pushed the 3429/complete-mobx-migration branch from 10f854b to 8ff28a2 Compare June 30, 2026 07:51
@github-actions

Copy link
Copy Markdown
Contributor

AI Code Review

⚠️ Approved with suggestions — low-severity items only, safe to merge


What was reviewed

File Change
src/Maps.tsx Replaced legacy hook-based implementation with useMapsContainer + ContainerProvider
src/Maps.editorConfig.ts Removed platform branching, removed advanced prop, added deprecation warnings
src/Maps.xml Removed advanced property
src/components/LeafletMap.tsx Rewritten to use LeafletMapViewModel via ref-callback; removed react-leaflet
src/components/MapsWidget.tsx New observer component bridging MobX model to MapSwitcher; API-key gate for non-openStreet providers
src/model/atoms/apiKey.atom.ts New reactive cached API key atom
src/model/atoms/geodecodeApiKey.atom.ts Same pattern for geodecode key
src/model/configs/Maps.config.ts Replaced apiKey with showCurrentLocation in MapsConfig
src/model/containers/Maps.container.ts Wired CurrentLocationService, LeafletMapViewModel, and key atoms
src/model/containers/Root.container.ts Bound getCurrentUserLocation function
src/model/services/CurrentLocation.service.ts New MobX service with stale-response guard
src/model/services/LocationResolver.service.ts Uses injected geodecodeApiKeyAtom instead of reading props directly
src/model/viewmodels/LeafletMap.viewModel.ts New ViewModel: manages Leaflet map instance, tile layer, and marker reactions
src/model/hooks/injection-hooks.ts New injection hooks for DI tokens
src/model/tokens.ts Added apiKey, geodecodeApiKey, getLocationFunction, currentLocation, leafletMapVM tokens
src/utils/leaflet-markers.ts Extracted marker creation utility
src/utils/leaflet.ts Deleted (react-leaflet dependency removed)
src/utils/mock-container-props.ts Removed advanced default prop
typings/MapsProps.d.ts Removed advanced from both container and preview props
typings/declare-png.ts New PNG module declaration
package.json Replaced react-leaflet with explicit mobx + mobx-react-lite
src/__tests__/Maps.spec.tsx New integration tests
src/components/__tests__/LeafletMap.spec.tsx Rewritten with structural RTL assertions (15 tests)
src/components/__tests__/MapsWidget.spec.tsx New render-gating tests
src/model/atoms/__tests__/apiKey.atom.spec.ts Unit tests for API key atom
src/model/atoms/__tests__/geodecodeApiKey.atom.spec.ts Unit tests for geodecode key atom
src/model/services/__tests__/CurrentLocation.spec.ts Service unit tests
openspec/changes/** OpenSpec design/proposal/task artifacts (archived + in-progress)

Skipped (out of scope): pnpm-lock.yaml, openspec/**/*.md (design docs only)


Findings

⚠️ Low — CurrentLocationService.setup() does not react to showCurrentLocation prop changes at runtime

File: src/model/services/CurrentLocation.service.ts line 26–44
Note: setup() reads this.config.showCurrentLocation once at setup time and never re-runs when the prop changes. The proposal doc explicitly lists "Resolves location when option becomes true" (integration) and "Clears location when option becomes false" as design goals, but the corresponding test cases (those two integration scenarios from design.md) are not present in CurrentLocation.spec.ts. The config is a plain snapshot object, so reactive prop changes won't be reflected. If showCurrentLocation is toggled at runtime, the service won't respond. The design doc also mentions a "version counter" pattern to discard stale responses but the current implementation uses a simple boolean flag — that works for a single call, but if setup() is called again (e.g. via re-mount) there could be a double-fire. Not blocking, but worth aligning the implementation with the stated reactive design goals.


⚠️ Low — MapsWidget loading state uses string template for className instead of classNames()

File: src/components/MapsWidget.tsx line 19
Note: The empty-state <div> uses a template literal `widget-maps ${props.class}` which will produce a trailing space if props.class is empty. The full render path below uses className={props.class} directly (passed to MapSwitcher). For consistency and safety, prefer classNames("widget-maps", props.class)classNames is already imported in LeafletMap.tsx and handles empty/falsy values:

import classNames from "classnames";
// ...
return <div className={classNames("widget-maps", props.class)} style={{ ...props.style, ...getDimensions(props) }} />;

⚠️ Low — LeafletMapViewModel does not react to tile-layer prop changes (provider/token) after map is set up

File: src/model/viewmodels/LeafletMap.viewModel.ts lines 52–63
Note: The tile layer URL and options are captured once in setupMap() and never updated. If mapProvider or the apiKey atom value changes after the map is mounted, the tile layer will become stale. The reaction only observes locationResolver.locations and currentLocationService.location. In practice, the provider is unlikely to change at runtime, but the apiKey atom is explicitly designed to resolve lazily — if the key arrives after setup, tiles will load without the key. Consider adding the tile layer config to the reaction (or a separate reaction) so it can be updated when the key resolves:

this.disposeReaction = reaction(
    () => ({
        locations: this.locationResolver.locations,
        currentLocation: this.currentLocationService.location,
        tileConfig: this.getTileLayerConfig(this.gate.props.mapProvider)
    }),
    ({ locations, currentLocation, tileConfig }) => {
        this.tileLayer?.setUrl(tileConfig.url);
        this.syncMarkers(locations, currentLocation, autoZoom);
    },
    { fireImmediately: true }
);

⚠️ Low — Missing test for CurrentLocationService reactivity scenarios defined in design.md

File: src/model/services/__tests__/CurrentLocation.spec.ts
Note: The design.md specifies two integration tests that are missing from the spec: "Resolves location when option becomes true" and "Clears location when option becomes false". The current test suite covers static showCurrentLocation: true/false at creation, dispose-guard, and error logging (4 of 6 planned tests), but the two reactive prop-change scenarios are absent. These are noted as out-of-scope in tasks.md task 6.1, but since they're in the design doc they're worth calling out.


Positives

  • The apiKeyAtom caching pattern is clean and idiomatic: the plain closure variable works correctly with MobX because observable reads are the only thing tracked, not plain variable writes. The design doc's explanation of why this is safe is accurate.
  • Removing react-leaflet and replacing it with direct Leaflet imperative API eliminates the known default-icon workaround and removes the dependency entirely — the typings/declare-png.ts module declaration is the right solution for importing images.
  • CurrentLocationService correctly guards against stale responses with a disposed boolean flag, avoiding the classic "set state after unmount" class of bugs.
  • Test migration from snapshot tests to structural/behavioral RTL assertions is the right direction — the old snapshots captured react-leaflet internals and would have been brittle.
  • Injection hooks in injection-hooks.ts follow the same pattern as the Gallery widget, maintaining consistency across the codebase.
  • The API-key render gate in MapsWidget (props.mapProvider !== "openStreet" && apiKey.get() === null) correctly handles all provider variants and has dedicated unit tests covering each case.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants