diff --git a/frontend/src/components/button.js b/frontend/src/components/button.js index 50b3f7fd02..4deed2140f 100644 --- a/frontend/src/components/button.js +++ b/frontend/src/components/button.js @@ -3,7 +3,7 @@ import { Link } from '@reach/router'; import { LoadingIcon } from './svgIcons'; const IconSpace = ({ children }) => {children}; -const AnimatedLoadingIcon = () => ( +export const AnimatedLoadingIcon = () => ( diff --git a/frontend/src/components/projectCreate/messages.js b/frontend/src/components/projectCreate/messages.js index 6da745b853..2b57df0e1d 100644 --- a/frontend/src/components/projectCreate/messages.js +++ b/frontend/src/components/projectCreate/messages.js @@ -147,7 +147,7 @@ export default defineMessages({ }, showProjectsAOILayer: { id: 'management.projects.create.show_aois', - defaultMessage: 'Show existing projects', + defaultMessage: 'Show existing projects AoIs', }, disabledAOILayer: { id: 'management.projects.create.show_aois.disabled', diff --git a/frontend/src/components/projectCreate/projectCreationMap.js b/frontend/src/components/projectCreate/projectCreationMap.js index e26851638f..faf67951db 100644 --- a/frontend/src/components/projectCreate/projectCreationMap.js +++ b/frontend/src/components/projectCreate/projectCreationMap.js @@ -7,6 +7,7 @@ import MapboxLanguage from '@mapbox/mapbox-gl-language'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; import { useDropzone } from 'react-dropzone'; +import { mapboxLayerDefn } from '../projects/projectsMap'; import { MAPBOX_TOKEN, @@ -32,23 +33,31 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, const mapRef = React.createRef(); const locale = useSelector((state) => state.preferences['locale']); const token = useSelector((state) => state.auth.get('token')); - const [showProjectsAOILayer, setShowProjectsAOILayer] = useState(false); + const [showProjectsAOILayer, setShowProjectsAOILayer] = useState(true); const [aoiCanBeActivated, setAOICanBeActivated] = useState(false); + const [existingProjectsList, setExistingProjectsList] = useState([]); + const [isAoiLoading, setIsAoiLoading] = useState(false); const [debouncedGetProjectsAOI] = useDebouncedCallback(() => getProjectsAOI(), 1500); const { getRootProps, getInputProps } = useDropzone({ onDrop: step === 1 ? uploadFile : () => {}, // drag&drop is activated only on the first step noClick: true, noKeyboard: true, }); - const minZoomLevelToAOIVisualization = 11; + const minZoomLevelToAOIVisualization = 9; + + useEffect(() => { + fetchLocalJSONAPI('projects/').then((res) => setExistingProjectsList(res.mapResults)); + }, []); const getProjectsAOI = () => { if (aoiCanBeActivated && showProjectsAOILayer && step === 1) { + setIsAoiLoading(true); let bounds = mapObj.map.getBounds(); let bbox = `${bounds._sw.lng},${bounds._sw.lat},${bounds._ne.lng},${bounds._ne.lat}`; - fetchLocalJSONAPI(`projects/queries/bbox/?bbox=${bbox}&srid=4326`, token).then((res) => - mapObj.map.getSource('otherProjects').setData(res), - ); + fetchLocalJSONAPI(`projects/queries/bbox/?bbox=${bbox}&srid=4326`, token).then((res) => { + mapObj.map.getSource('otherProjects').setData(res); + setIsAoiLoading(false); + }); } }; @@ -187,6 +196,41 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, } }; + const noop = () => {}; + + useLayoutEffect(() => { + /* docs: https://docs.mapbox.com/mapbox-gl-js/example/cluster/ */ + const { map } = mapObj; + + const someResultsReady = + existingProjectsList && + existingProjectsList.features && + existingProjectsList.features.length > 0; + + const mapReadyProjectsReady = + map !== null && + map.isStyleLoaded() && + map.getSource('projects') === undefined && + someResultsReady; + const projectsReadyMapLoading = + map !== null && + !map.isStyleLoaded() && + map.getSource('projects') === undefined && + someResultsReady; + + /* set up style/sources for the map, either immediately or on base load */ + if (mapReadyProjectsReady) { + mapboxLayerDefn(map, existingProjectsList, noop, true); + } else if (projectsReadyMapLoading) { + map.on('load', () => mapboxLayerDefn(map, existingProjectsList, noop, true)); + } + + /* refill the source on existingProjectsList changes */ + if (map !== null && map.getSource('projects') !== undefined && someResultsReady) { + map.getSource('projects').setData(existingProjectsList); + } + }, [mapObj, existingProjectsList]); + useLayoutEffect(() => { if (mapObj.map !== null && mapboxgl.supported()) { mapObj.map.on('moveend', (event) => { @@ -247,6 +291,7 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, isActive={showProjectsAOILayer} setActive={setShowProjectsAOILayer} disabled={!aoiCanBeActivated} + isAoiLoading={isAoiLoading} /> )} diff --git a/frontend/src/components/projectCreate/projectsAOILayerCheckBox.js b/frontend/src/components/projectCreate/projectsAOILayerCheckBox.js index 36de58f1ff..14d784a9e2 100644 --- a/frontend/src/components/projectCreate/projectsAOILayerCheckBox.js +++ b/frontend/src/components/projectCreate/projectsAOILayerCheckBox.js @@ -5,8 +5,9 @@ import ReactTooltip from 'react-tooltip'; import messages from './messages'; import statusMessages from '../projectDetail/messages'; import { TASK_COLOURS } from '../../config'; +import { AnimatedLoadingIcon } from '../button'; -export const ProjectsAOILayerCheckBox = ({ isActive, setActive, disabled }) => { +export const ProjectsAOILayerCheckBox = ({ isActive, setActive, disabled, isAoiLoading }) => { return ( <>
@@ -19,6 +20,7 @@ export const ProjectsAOILayerCheckBox = ({ isActive, setActive, disabled }) => { + {isAoiLoading && } {disabled ? ( diff --git a/frontend/src/components/projectCreate/tests/projectsAOILayerCheckBox.test.js b/frontend/src/components/projectCreate/tests/projectsAOILayerCheckBox.test.js index fafc5d11f4..c63aa0334f 100644 --- a/frontend/src/components/projectCreate/tests/projectsAOILayerCheckBox.test.js +++ b/frontend/src/components/projectCreate/tests/projectsAOILayerCheckBox.test.js @@ -13,10 +13,10 @@ describe('ProjectsAOILayerCheckBox', () => { , ); - expect(screen.getByText('Show existing projects')).toBeInTheDocument(); + expect(screen.getByText('Show existing projects AoIs')).toBeInTheDocument(); expect(screen.getByRole('checkbox').className).toContain('b--grey-light'); expect(screen.getByRole('checkbox').className).not.toContain('b--red'); - userEvent.hover(screen.getByText('Show existing projects')); + userEvent.hover(screen.getByText('Show existing projects AoIs')); expect( screen.getByText( "Zoom in to be able to activate the visualization of other projects' areas of interest.", @@ -31,10 +31,10 @@ describe('ProjectsAOILayerCheckBox', () => { , ); - expect(screen.getByText('Show existing projects')).toBeInTheDocument(); + expect(screen.getByText('Show existing projects AoIs')).toBeInTheDocument(); expect(screen.getByRole('checkbox').className).not.toContain('b--grey-light'); expect(screen.getByRole('checkbox').className).toContain('b--red'); - userEvent.hover(screen.getByText('Show existing projects')); + userEvent.hover(screen.getByText('Show existing projects AoIs')); expect( screen.getByText("Enable the visualization of the existing projects' areas of interest."), ).toBeInTheDocument(); diff --git a/frontend/src/components/projects/projectsMap.js b/frontend/src/components/projects/projectsMap.js index 1f2f5576eb..b0d0f1f660 100644 --- a/frontend/src/components/projects/projectsMap.js +++ b/frontend/src/components/projects/projectsMap.js @@ -22,7 +22,7 @@ const licensedFonts = MAPBOX_TOKEN ? ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'] : ['Open Sans Semibold']; -export const mapboxLayerDefn = (map, mapResults, clickOnProjectID) => { +export const mapboxLayerDefn = (map, mapResults, clickOnProjectID, disablePoiClick = false) => { map.addImage('mapMarker', markerIcon, { width: 15, height: 15, data: markerIcon }); map.addSource('projects', { type: 'geojson', @@ -80,7 +80,9 @@ export const mapboxLayerDefn = (map, mapResults, clickOnProjectID) => { }); map.on('mouseenter', 'projects-unclustered-points', function (e) { // Change the cursor style as a UI indicator. - map.getCanvas().style.cursor = 'pointer'; + if (!disablePoiClick) { + map.getCanvas().style.cursor = 'pointer'; + } }); map.on('mouseleave', 'projects-unclustered-points', function (e) { // Change the cursor style as a UI indicator. diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 41add66813..bc206b65e0 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -254,7 +254,7 @@ "management.projects.create.errors.fileSize": "We only accept files up to {fileSize} MB. Please reduce the size of your file and try again.", "management.projects.create.split_task.description": "Make tasks smaller by clicking on specific tasks or drawing an area on the map.", "management.projects.create.reset.button": "Reset", - "management.projects.create.show_aois": "Show existing projects", + "management.projects.create.show_aois": "Show existing projects AoIs", "management.projects.create.show_aois.disabled": "Zoom in to be able to activate the visualization of other projects' areas of interest.", "management.projects.create.show_aois.enable": "Enable the visualization of the existing projects' areas of interest.", "management.projects.create.show_aois.legend": "Color legend:", @@ -359,6 +359,7 @@ "projects.formInputs.campaign.title": "Campaign", "projects.formInputs.categories.title": "Categories", "projects.formInputs.organisation.description": "Organization that is coordinating the project, if there is any. The managers of that organization will have administration rights over the project.", + "projects.formInputs.admins.title": "TM Admins", "projects.formInputs.imagery.select": "Select imagery", "projects.formInputs.license.select": "Select license", "projects.formInputs.organisation.select": "Select organization", @@ -855,6 +856,7 @@ "management.teams.visibility.public": "Public", "management.teams.visibility.private": "Private", "management.teams.invite_only.description": "Managers need to approve a member's request to join.", + "management.teams.newJoinRequestNotification": "Enable for team managers to receive (email) notifications each time a new join request is made", "teamsAndOrgs.management.teams.messages.waiting_approval": "Your request to join this team is waiting for approval.", "management.projects.no_found": "This {entity} doesn't have projects yet.", "management.organisation.teams.no_found": "No teams found.",