Skip to content

Commit

Permalink
Handle switching editors with unsaved edits (#5096)
Browse files Browse the repository at this point in the history
* Update RapiD tag to fix css issues

* Add RapiD context to redux store and use in rapidEditor

* Handle switching editors with unsaved edits

* Display error popup if JOSM is not running

Co-authored-by: Hel Nershing Thapa <[email protected]>
  • Loading branch information
Zack LaVergne and HelNershingThapa committed Apr 17, 2022
1 parent 90b3c10 commit b70388f
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 123 deletions.
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@turf/transform-scale": "^6.4.0",
"@turf/truncate": "^6.4.0",
"@webscopeio/react-textarea-autocomplete": "^4.7.3",
"RapiD": "facebookincubator/rapid#rapid-v1.1.8-tm.1",
"axios": "^0.21.1",
"chart.js": "^3.3.2",
"date-fns": "^2.28.0",
Expand Down Expand Up @@ -78,8 +79,8 @@
},
"scripts": {
"build-locales": "combine-messages -i './src/**/messages.js' -o './src/locales/en.json'",
"copy-static": "bash -c \"mkdir -p public/static/id; mkdir -p public/static/rapid; if ! (test -a public/static/id/index.js); then cp -R node_modules/@hotosm/id/dist/* public/static/id; elif ! (test -a public/static/rapid/index.js); then cp -R node_modules/RapiD/dist/* public/static/rapid; fi\"",
"update-static": "bash -c \"mkdir -p public/static/id; mkdir -p public/static/rapid; cp -R node_modules/@hotosm/id/dist/* public/static/id; cp -R node_modules/RapiD/dist/* public/static/rapid;\"",
"copy-static": "bash -c \"mkdir -p public/static/id; mkdir -p public/static/rapid; if ! (test -a public/static/id/index.js); then cp -R node_modules/@hotosm/id/dist/* public/static/id; fi; if ! (test -a public/static/rapid/index.js); then if ! (test -a node_modules/RapiD/dist/RapiD.css) then mv node_modules/RapiD/dist/iD.css node_modules/RapiD/dist/RapiD.css; fi; cp -R node_modules/RapiD/dist/* public/static/rapid; fi\"",
"update-static": "bash -c \"mkdir -p public/static/id; mkdir -p public/static/rapid; cp -R node_modules/@hotosm/id/dist/* public/static/id; if ! (test -a node_modules/RapiD/dist/RapiD.css) then mv node_modules/RapiD/dist/iD.css node_modules/RapiD/dist/RapiD.css; fi; cp -R node_modules/RapiD/dist/* public/static/rapid;\"",
"preparation": "bash -c \"if (test -a ../tasking-manager.env); then grep -hs ^ ../tasking-manager.env .env.expand > .env; else cp .env.expand .env; fi\"",
"start": "npm run preparation && npm run copy-static && react-scripts start",
"build": "npm run preparation && npm run update-static && react-scripts build",
Expand Down
176 changes: 86 additions & 90 deletions frontend/src/components/rapidEditor.js
Original file line number Diff line number Diff line change
@@ -1,119 +1,115 @@
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import * as RapiD from 'RapiD/dist/iD.legacy';
import 'RapiD/dist/iD.css';
import 'RapiD/dist/RapiD.css';

import { OSM_CONSUMER_KEY, OSM_CONSUMER_SECRET, OSM_SERVER_URL } from '../config';

export default function RapidEditor({ setDisable, comment, presets, imagery, gpxUrl, powerUser = false }) {
const dispatch = useDispatch();
const session = useSelector((state) => state.auth.get('session'));
const iDContext = useSelector((state) => state.editor.context);
const RapiDContext = useSelector((state) => state.editor.rapidContext);
const locale = useSelector((state) => state.preferences.locale);
const [customImageryIsSet, setCustomImageryIsSet] = useState(false);
const windowInit = typeof window !== undefined;
const customSource =
iDContext && iDContext.background() && iDContext.background().findSource('custom');
RapiDContext && RapiDContext.background() && RapiDContext.background().findSource('custom');

useEffect(() => {
if (!customImageryIsSet && imagery && customSource) {
if (imagery.startsWith('http')) {
iDContext.background().baseLayerSource(customSource.template(imagery));
setCustomImageryIsSet(true);
// this line is needed to update the value on the custom background dialog
window.iD.prefs('background-custom-template', imagery);
} else {
const imagerySource = iDContext.background().findSource(imagery);
if (imagerySource) {
iDContext.background().baseLayerSource(imagerySource);
}
useEffect(() => {
if (!customImageryIsSet && imagery && customSource) {
if (imagery.startsWith('http')) {
RapiDContext.background().baseLayerSource(customSource.template(imagery));
setCustomImageryIsSet(true);
// this line is needed to update the value on the custom background dialog
window.iD.prefs('background-custom-template', imagery);
} else {
const imagerySource = RapiDContext.background().findSource(imagery);
if (imagerySource) {
RapiDContext.background().baseLayerSource(imagerySource);
}
}
}, [customImageryIsSet, imagery, iDContext, customSource]);
}
}, [customImageryIsSet, imagery, RapiDContext, customSource]);

useEffect(() => {
return () => {
dispatch({ type: 'SET_VISIBILITY', isVisible: true });
};
// eslint-disable-next-line
}, []);
useEffect(() => {
return () => {
dispatch({ type: 'SET_VISIBILITY', isVisible: true });
};
// eslint-disable-next-line
}, []);

useEffect(() => {
if (windowInit) {
dispatch({ type: 'SET_VISIBILITY', isVisible: false });
if (iDContext === null) {
// we need to keep iD context on redux store because iD works better if
// the context is not restarted while running in the same browser session
dispatch({ type: 'SET_EDITOR', context: window.iD.coreContext()})
// } else{
// if (RapiDContext === null) {
// dispatch({ type: 'SET_EDITOR', context: iDContext, rapidContext: window.iD.coreRapidContext(())})
// }
// }
}
useEffect(() => {
if (windowInit) {
dispatch({ type: 'SET_VISIBILITY', isVisible: false });
if (RapiDContext === null) {
// we need to keep iD context on redux store because iD works better if
// the context is not restarted while running in the same browser session
dispatch({ type: 'SET_RAPIDEDITOR', context: window.iD.coreContext() })
}
}, [windowInit, iDContext, dispatch]);

useEffect(() => {
if (iDContext && comment) {
iDContext.defaultChangesetComment(comment);
}
}, [comment, iDContext]);
}
}, [windowInit, RapiDContext, dispatch]);

useEffect(() => {
if (session && locale && RapiD && iDContext) {
// if presets is not a populated list we need to set it as null
try {
if (presets.length) {
window.iD.presetManager.addablePresetIDs(presets);
} else {
window.iD.presetManager.addablePresetIDs(null);
}
} catch (e) {
window.iD.presetManager.addablePresetIDs(null);
}
// setup the context
iDContext
.embed(true)
.assetPath('/static/rapid/')
.locale(locale)
.setsDocumentTitle(false)
.containerNode(document.getElementById('id-container'));
// init the ui or restart if it was loaded previously
if (iDContext.ui() !== undefined) {
iDContext.reset();
iDContext.ui().restart();
useEffect(() => {
if (RapiDContext && comment) {
RapiDContext.defaultChangesetComment(comment);
}
}, [comment, RapiDContext]);

useEffect(() => {
if (session && locale && RapiD && RapiDContext) {
// if presets is not a populated list we need to set it as null
try {
if (presets.length) {
window.iD.presetManager.addablePresetIDs(presets);
} else {
iDContext.init();
}
if (gpxUrl) {
iDContext.layers().layer('data').url(gpxUrl, '.gpx');
window.iD.presetManager.addablePresetIDs(null);
}
} catch (e) {
window.iD.presetManager.addablePresetIDs(null);
}
// setup the context
RapiDContext
.embed(true)
.assetPath('/static/rapid/')
.locale(locale)
.setsDocumentTitle(false)
.containerNode(document.getElementById('rapid-container'));
// init the ui or restart if it was loaded previously
if (RapiDContext.ui() !== undefined) {
RapiDContext.reset();
RapiDContext.ui().restart();
} else {
RapiDContext.init();
}
if (gpxUrl) {
RapiDContext.layers().layer('data').url(gpxUrl, '.gpx');
}

iDContext.rapidContext().showPowerUser = powerUser;
RapiDContext.rapidContext().showPowerUser = powerUser;

let osm = iDContext.connection();
const auth = {
urlroot: OSM_SERVER_URL,
oauth_consumer_key: OSM_CONSUMER_KEY,
oauth_secret: OSM_CONSUMER_SECRET,
oauth_token: session.osm_oauth_token,
oauth_token_secret: session.osm_oauth_token_secret,
};
osm.switch(auth);
let osm = RapiDContext.connection();
const auth = {
urlroot: OSM_SERVER_URL,
oauth_consumer_key: OSM_CONSUMER_KEY,
oauth_secret: OSM_CONSUMER_SECRET,
oauth_token: session.osm_oauth_token,
oauth_token_secret: session.osm_oauth_token_secret,
};
osm.switch(auth);

const thereAreChanges = (changes) =>
changes.modified.length || changes.created.length || changes.deleted.length;
const thereAreChanges = (changes) =>
changes.modified.length || changes.created.length || changes.deleted.length;

iDContext.history().on('change', () => {
if (thereAreChanges(iDContext.history().changes())) {
setDisable(true);
} else {
setDisable(false);
}
});
}
}, [session, iDContext, setDisable, presets, locale, gpxUrl, powerUser]);
RapiDContext.history().on('change', () => {
if (thereAreChanges(RapiDContext.history().changes())) {
setDisable(true);
} else {
setDisable(false);
}
});
}
}, [session, RapiDContext, setDisable, presets, locale, gpxUrl, powerUser]);

return <div className="w-100 vh-minus-77-ns" id="id-container"></div>;
return <div className="w-100 vh-minus-77-ns" id="rapid-container"></div>;
}
85 changes: 64 additions & 21 deletions frontend/src/components/taskSelection/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { HeaderLine } from '../projectDetail/header';
import { Button } from '../button';
import Portal from '../portal';
import { SidebarIcon } from '../svgIcons';
import { openEditor, getTaskGpxUrl, formatImageryUrl } from '../../utils/openEditor';
import { openEditor, getTaskGpxUrl, formatImageryUrl, formatJosmUrl } from '../../utils/openEditor';
import { getTaskContributors } from '../../utils/getTaskContributors';
import { TaskHistory } from './taskActivity';
import { ChangesetCommentTags } from './changesetComment';
Expand All @@ -26,12 +26,13 @@ import {
CompletionTabForValidation,
SidebarToggle,
ReopenEditor,
UnsavedMapChangesModalContent,
} from './actionSidebars';
import { fetchLocalJSONAPI } from '../../network/genericJSONRequest';
import { MultipleTaskHistoriesAccordion } from './multipleTaskHistories';
import { ResourcesTab } from './resourcesTab';
import { ActionTabsNav } from './actionTabsNav';

import { LockedTaskModalContent } from './lockedTasks';
const Editor = React.lazy(() => import('../editor'));
const RapiDEditor = React.lazy(() => import('../rapidEditor'));

Expand All @@ -42,15 +43,16 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
const [activeSection, setActiveSection] = useState('completion');
const [activeEditor, setActiveEditor] = useState(editor);
const [showSidebar, setShowSidebar] = useState(true);
const [isJosmError, setIsJosmError] = useState(false);
const tasksIds = useMemo(
() =>
activeTasks
? activeTasks
.map((task) => task.taskId)
.sort((n1, n2) => {
// in ascending order
return n1 - n2;
})
.map((task) => task.taskId)
.sort((n1, n2) => {
// in ascending order
return n1 - n2;
})
: [],
[activeTasks],
);
Expand All @@ -61,6 +63,7 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
const [validationStatus, setValidationStatus] = useState({});
const [historyTabChecked, setHistoryTabChecked] = useState(false);
const [multipleTasksInfo, setMultipleTasksInfo] = useState({});
const [showMapChangesModal, setShowMapChangesModal] = useState(false);
const intl = useIntl();

const activeTask = activeTasks && activeTasks[0];
Expand Down Expand Up @@ -129,25 +132,42 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
}
}, [editor, project, projectIsReady, userDetails.defaultEditor, action, tasks, tasksIds]);

const callEditor = (arr) => {
setActiveEditor(arr[0].value);
const url = openEditor(
arr[0].value,
project,
tasks,
tasksIds,
[window.innerWidth, window.innerHeight],
null,
);
if (url) {
navigate(`./${url}`);
const callEditor = async (arr) => {
setIsJosmError(false);
if (!disabled) {
setActiveEditor(arr[0].value);
const url = openEditor(
arr[0].value,
project,
tasks,
tasksIds,
[window.innerWidth, window.innerHeight],
null,
);
if (url) {
navigate(`./${url}`);
if (arr[0].value === 'JOSM') {
try {
await fetch(formatJosmUrl('version', { jsonp: 'checkJOSM' }));
} catch (e) {
setIsJosmError(true);
return;
}
}
} else {
navigate(`./?editor=${arr[0].value}`);
}
} else {
navigate(`./?editor=${arr[0].value}`);
// we need to return a promise in order to be called by useAsync
return new Promise((resolve, reject) => {
setShowMapChangesModal('reload editor');
resolve();
});
}
window.location.reload();
};

return (
<>
<Portal>
<div className="cf w-100 vh-minus-77-ns overflow-y-hidden">
<div className={`fl h-100 relative ${showSidebar ? 'w-70' : 'w-100-minus-4rem'}`}>
Expand Down Expand Up @@ -300,6 +320,17 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
editor={activeEditor}
callEditor={callEditor}
/>
{disabled && showMapChangesModal && (
<Popup
modal
open
closeOnEscape={true}
closeOnDocumentClick={true}
onClose={() => setShowMapChangesModal(null)}
>
{(close) => <UnsavedMapChangesModalContent close={close} action={showMapChangesModal} />}
</Popup>
)}
{(editor === 'ID' || editor === 'RAPID') && (
<Popup
modal
Expand Down Expand Up @@ -388,5 +419,17 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
)}
</div>
</Portal>
{isJosmError && (
<Popup
modal
open
closeOnEscape={true}
closeOnDocumentClick={true}
onClose={() => setIsJosmError(false)}
>
{(close) => <LockedTaskModalContent project={project} error="JOSM" close={close} />}
</Popup>
)}
</>
);
}
Loading

0 comments on commit b70388f

Please sign in to comment.