Skip to content

Commit 3c62c65

Browse files
Merge pull request #7298 from hotosm/develop
iD editor related updates to staging server
2 parents 521073e + a1e8fe4 commit 3c62c65

4 files changed

Lines changed: 76 additions & 91 deletions

File tree

frontend/src/components/editor.js

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useLayoutEffect, useState } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { useSelector, useDispatch } from 'react-redux';
33
import { useIntl } from 'react-intl';
44
import { gpx } from '@tmcw/togeojson';
@@ -7,19 +7,9 @@ import '@openstreetmap/id/dist/iD.css';
77

88
import { OSM_CLIENT_ID, OSM_REDIRECT_URI, OSM_SERVER_URL } from '../config';
99
import messages from './messages';
10-
import {
11-
registerIdEditorStylesheet,
12-
activateIdEditorStylesheet,
13-
} from '../utils/idEditorStylesheets';
10+
import { captureIdEditorPackage, resolveIdEditorContext } from '../utils/idEditorContext';
1411

15-
// @openstreetmap/id has no real ESM/CJS exports — it only assigns itself to
16-
// window.iD as a side effect on import. Capture it here, right after the
17-
// import above forces that assignment, so this module keeps its own private
18-
// reference instead of the shared global, which @osm-sandbox/sandbox-id
19-
// (imported by sandboxEditor.js) later overwrites.
20-
const officialID = window.iD;
21-
22-
registerIdEditorStylesheet('official');
12+
const officialID = captureIdEditorPackage();
2313

2414
export default function Editor({ setDisable, comment, presets, imagery, gpxUrl, extraIdParams }) {
2515
const dispatch = useDispatch();
@@ -32,14 +22,6 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
3222
const customSource =
3323
iDContext && iDContext.background() && iDContext.background().findSource('custom');
3424

35-
// Only one of the OSM iD editor / Sandbox editor is ever mounted at a
36-
// time, but both of their stylesheets stay loaded for the whole page
37-
// session once visited. Disable the other one's so its rules can't leak
38-
// into this editor via their shared ".ideditor" root class.
39-
useLayoutEffect(() => {
40-
activateIdEditorStylesheet('official');
41-
}, []);
42-
4325
useEffect(() => {
4426
if (!customImageryIsSet && imagery && customSource) {
4527
if (imagery.startsWith('http')) {
@@ -83,31 +65,21 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
8365
}, [customImageryIsSet, imagery, iDContext, customSource, extraIdParams]);
8466

8567
useEffect(() => {
86-
if (windowInit) {
87-
if (iDContext === null) {
88-
// we need to keep iD context on redux store because iD works better if
89-
// the context is not restarted while running in the same browser session
90-
dispatch({ type: 'SET_EDITOR', context: officialID.coreContext() });
91-
}
68+
if (!windowInit) return;
69+
const context = resolveIdEditorContext(iDContext, 'official', () => officialID.coreContext());
70+
if (context && context !== iDContext) {
71+
dispatch({ type: 'SET_EDITOR', context });
9272
}
9373
}, [windowInit, iDContext, dispatch]);
9474

95-
// Reset context on unmount so the sandbox editor always gets a fresh context
96-
// from its own iD module (sandbox-id), preventing cross-editor context bleed.
97-
useEffect(() => {
98-
return () => {
99-
dispatch({ type: 'SET_EDITOR', context: null });
100-
};
101-
}, [dispatch]);
102-
10375
useEffect(() => {
10476
if (iDContext && comment) {
10577
iDContext.defaultChangesetComment(comment);
10678
}
10779
}, [comment, iDContext]);
10880

10981
useEffect(() => {
110-
if (session && locale && iD && iDContext) {
82+
if (session && locale && iD && iDContext && iDContext.__idEditorType === 'official') {
11183
// if presets is not a populated list we need to set it as null
11284
try {
11385
if (presets.length) {
@@ -125,7 +97,10 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
12597
.locale(locale)
12698
.setsDocumentTitle(false)
12799
.containerNode(document.getElementById('id-container'));
128-
// init the ui or restart if it was loaded previously
100+
// init the ui or restart if it was loaded previously. Either path ends
101+
// with osm.switch() below, which resets the (module-scoped, session-wide)
102+
// tile cache and triggers a fresh load from the current view — so a
103+
// reused/fresh context never serves stale, pre-edit data.
129104
if (iDContext.ui() !== undefined) {
130105
iDContext.reset();
131106
iDContext.ui().restart();

frontend/src/components/sandboxEditor.js

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useLayoutEffect, useState } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { useSelector, useDispatch } from 'react-redux';
44
import { useIntl } from 'react-intl';
@@ -15,19 +15,9 @@ import {
1515
import { useSandboxOAuthCallback } from '../hooks/UseSandboxOAuthCallback';
1616
import { getValidTokenOrInitiateAuth, fetchSandboxLicense } from '../utils/sandboxUtils';
1717
import { useOsmFeaturesQuery } from '../api/projects';
18-
import {
19-
registerIdEditorStylesheet,
20-
activateIdEditorStylesheet,
21-
} from '../utils/idEditorStylesheets';
22-
23-
// @osm-sandbox/sandbox-id has no real ESM/CJS exports — it only assigns
24-
// itself to window.iD as a side effect on import. Capture it here, right
25-
// after the imports above force that assignment, so this module keeps its
26-
// own private reference instead of the shared global, which
27-
// @openstreetmap/id (imported by editor.js) later overwrites.
28-
const sandboxID = window.iD;
18+
import { captureIdEditorPackage, resolveIdEditorContext } from '../utils/idEditorContext';
2919

30-
registerIdEditorStylesheet('sandbox');
20+
const sandboxID = captureIdEditorPackage();
3121

3222
export default function SandboxEditor({
3323
setDisable,
@@ -62,14 +52,6 @@ export default function SandboxEditor({
6252

6353
useSandboxOAuthCallback(sandboxId);
6454

65-
// Only one of the OSM iD editor / Sandbox editor is ever mounted at a
66-
// time, but both of their stylesheets stay loaded for the whole page
67-
// session once visited. Disable the other one's so its rules can't leak
68-
// into this editor via their shared ".ideditor" root class.
69-
useLayoutEffect(() => {
70-
activateIdEditorStylesheet('sandbox');
71-
}, []);
72-
7355
const customSource =
7456
iDContext && iDContext.background() && iDContext.background().findSource('custom');
7557

@@ -90,10 +72,9 @@ export default function SandboxEditor({
9072
}, [customImageryIsSet, imagery, iDContext, customSource]);
9173

9274
useEffect(() => {
93-
if (iDContext === null) {
94-
// we need to keep iD context on redux store because iD works better if
95-
// the context is not restarted while running in the same browser session
96-
dispatch({ type: 'SET_EDITOR', context: sandboxID.coreContext() });
75+
const context = resolveIdEditorContext(iDContext, 'sandbox', () => sandboxID.coreContext());
76+
if (context && context !== iDContext) {
77+
dispatch({ type: 'SET_EDITOR', context });
9778
}
9879
}, [iDContext, dispatch]);
9980

@@ -106,7 +87,14 @@ export default function SandboxEditor({
10687
// Initialize sandbox editor
10788
useEffect(() => {
10889
const initializeSandbox = async () => {
109-
if (!session || !locale || !iD || !iDContext || isInitialized) {
90+
if (
91+
!session ||
92+
!locale ||
93+
!iD ||
94+
!iDContext ||
95+
iDContext.__idEditorType !== 'sandbox' ||
96+
isInitialized
97+
) {
11098
return;
11199
}
112100
const authStatus = sandboxAuthStatus?.[sandboxId];
@@ -151,7 +139,16 @@ export default function SandboxEditor({
151139
.setsDocumentTitle(false)
152140
.containerNode(document.getElementById('id-container'));
153141

154-
// init the ui or restart if it was loaded previously
142+
// @osm-sandbox/sandbox-id has no dark theme of its own, but it shares the
143+
// "ideditor" root class with @openstreetmap/id, which does have one keyed
144+
// off the OS/browser's prefers-color-scheme. Force light explicitly so
145+
// this editor doesn't pick up a dark theme it was never styled for.
146+
iDContext.container().classed('theme-light', true);
147+
148+
// init the ui or restart if it was loaded previously. Either path ends
149+
// with connection().switch() below, which resets the (module-scoped,
150+
// session-wide) tile cache and triggers a fresh load from the current
151+
// view — so a reused/fresh context never serves stale, pre-edit data.
155152
if (iDContext.ui() !== undefined) {
156153
iDContext.reset();
157154
iDContext.ui().restart();
@@ -288,9 +285,6 @@ export default function SandboxEditor({
288285
return () => {
289286
// Reset auth status for this sandbox on unmount
290287
dispatch(setSandboxAuthStatus(sandboxId, 'idle'));
291-
// Reset context on unmount so the OSM iD editor always gets a fresh context
292-
// from its own iD module (@openstreetmap/id), preventing cross-editor context bleed.
293-
dispatch({ type: 'SET_EDITOR', context: null });
294288
};
295289
}, [dispatch, sandboxId]);
296290

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Both @openstreetmap/id and @osm-sandbox/sandbox-id have no real ESM/CJS
2+
// exports — each only assigns itself to window.iD as a side effect on
3+
// import, and whichever loaded last wins that global for the rest of the
4+
// session. Call this at module scope, immediately after importing a
5+
// package's own module, so it captures that package's own reference before
6+
// the other one can overwrite the global.
7+
export function captureIdEditorPackage() {
8+
return window.iD;
9+
}
10+
11+
// The OSM iD editor and the Sandbox editor keep their iD context on the redux
12+
// store rather than recreating it on every mount — iD works better warm, and
13+
// reopening the same task shouldn't need a page reload to see recently-saved
14+
// edits. But the two editors can't share a context: they're built by different
15+
// iD forks, so a context from the other editor is missing methods it never
16+
// calls (e.g. Sandbox's own `.license()`). Each context is tagged with who
17+
// built it.
18+
//
19+
// This resolves the context for a mounting editor:
20+
// - same type as the stored context → reuse it (stays warm, no reload)
21+
// - no stored context yet (fresh page load) → build a new tagged one
22+
// - a DIFFERENT type is stored (a cross-editor switch within a live SPA
23+
// session) → force a full page reload and return null. Initializing a
24+
// fresh fork into the container mid-SPA-transition is unreliable (the
25+
// task's own features sometimes never load until a manual reload), whereas
26+
// a full reload lands back on the same task+editor via the URL and
27+
// initializes cleanly — the one path that always works. On that reload the
28+
// store is empty, so we take the "build a new one" branch, not this one.
29+
export function resolveIdEditorContext(existingContext, editorType, buildContext) {
30+
if (existingContext) {
31+
if (existingContext.__idEditorType === editorType) {
32+
return existingContext;
33+
}
34+
window.location.reload();
35+
return null;
36+
}
37+
const context = buildContext();
38+
context.__idEditorType = editorType;
39+
return context;
40+
}

frontend/src/utils/idEditorStylesheets.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)