diff --git a/package-lock.json b/package-lock.json index 7187451f..c17aa0ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3432,6 +3432,16 @@ "@types/react": "*" } }, + "@types/react-leaflet": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@types/react-leaflet/-/react-leaflet-2.5.2.tgz", + "integrity": "sha512-XBNFsBm4wQiz6BpzUMJAMXru+h2ESyW/mAdPoSzEirtF/g0NOwbUvPYnZtpbiW70AEXT40ZONtsu3lxwr/yliA==", + "dev": true, + "requires": { + "@types/leaflet": "*", + "@types/react": "*" + } + }, "@types/react-redux": { "version": "7.1.9", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz", diff --git a/package.json b/package.json index 32b42d36..f53e98e2 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/react-copy-to-clipboard": "^4.3.0", "@types/react-datepicker": "~1.8.0", "@types/react-dom": "^16.9.8", + "@types/react-leaflet": "^2.5.2", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", "@types/react-tagsinput": "^3.19.7", diff --git a/src/visits/VisitsStats.js b/src/visits/VisitsStats.js index dc4d539d..544c36dc 100644 --- a/src/visits/VisitsStats.js +++ b/src/visits/VisitsStats.js @@ -14,6 +14,7 @@ import GraphCard from './helpers/GraphCard'; import LineChartCard from './helpers/LineChartCard'; import VisitsTable from './VisitsTable'; import { VisitsInfoType } from './types'; +import OpenMapModalBtn from './helpers/OpenMapModalBtn'; const propTypes = { children: PropTypes.node, @@ -35,7 +36,7 @@ const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits. const format = formatDate(); let selectedBar; -const VisitsStats = ({ processStatsFromVisits, normalizeVisits }, OpenMapModalBtn) => { +const VisitsStats = ({ processStatsFromVisits, normalizeVisits }) => { const VisitsStatsComp = ({ children, visitsInfo, getVisits, cancelGetVisits, matchMedia = window.matchMedia }) => { const [ startDate, setStartDate ] = useState(undefined); const [ endDate, setEndDate ] = useState(undefined); diff --git a/src/visits/helpers/MapModal.scss b/src/visits/helpers/MapModal.scss index d37029c6..bcf0d938 100644 --- a/src/visits/helpers/MapModal.scss +++ b/src/visits/helpers/MapModal.scss @@ -1,7 +1,7 @@ @import '../../utils/base'; @import '../../utils/mixins/fit-with-margin'; -.map-modal__modal { +.map-modal__modal.map-modal__modal { @media (min-width: $mdMin) { $margin: 20px; @@ -15,11 +15,11 @@ } } -.map-modal__modal-content { +.map-modal__modal-content.map-modal__modal-content { height: 100%; } -.map-modal__modal-title { +.map-modal__modal-title.map-modal__modal-title { position: absolute; width: 100%; z-index: 1001; @@ -29,17 +29,17 @@ background: linear-gradient(rgba(0, 0, 0, .5), rgba(0, 0, 0, 0)); } -.map-modal__modal-body { +.map-modal__modal-body.map-modal__modal-body { padding: 0; display: flex; overflow: hidden; } -.map-modal__modal .leaflet-container { +.map-modal__modal.map-modal__modal .leaflet-container.leaflet-container { flex: 1 1 auto; border-radius: .3rem; } -.map-modal__modal .leaflet-top .leaflet-control { +.map-modal__modal.map-modal__modal .leaflet-top.leaflet-top .leaflet-control.leaflet-control { margin-top: 60px; } diff --git a/src/visits/helpers/MapModal.js b/src/visits/helpers/MapModal.tsx similarity index 65% rename from src/visits/helpers/MapModal.js rename to src/visits/helpers/MapModal.tsx index 45cc7304..85ce36e6 100644 --- a/src/visits/helpers/MapModal.js +++ b/src/visits/helpers/MapModal.tsx @@ -1,32 +1,25 @@ -import React from 'react'; +import React, { FC } from 'react'; import { Modal, ModalBody } from 'reactstrap'; -import { Map, TileLayer, Marker, Popup } from 'react-leaflet'; +import { Map, TileLayer, Marker, Popup, MapProps } from 'react-leaflet'; import { prop } from 'ramda'; -import * as PropTypes from 'prop-types'; +import { CityStats } from '../types'; import './MapModal.scss'; -const propTypes = { - toggle: PropTypes.func, - isOpen: PropTypes.bool, - title: PropTypes.string, - locations: PropTypes.arrayOf(PropTypes.shape({ - cityName: PropTypes.string.isRequired, - latLong: PropTypes.arrayOf(PropTypes.number).isRequired, - count: PropTypes.number.isRequired, - })), -}; -const defaultProps = { - locations: [], -}; +interface MapModalProps { + toggle: () => void; + isOpen: boolean; + title: string; + locations?: CityStats[]; +} -const OpenStreetMapTile = () => ( +const OpenStreetMapTile: FC = () => ( ); -const calculateMapProps = (locations) => { +const calculateMapProps = (locations: CityStats[]): Partial => { if (locations.length === 0) { return {}; } @@ -39,10 +32,10 @@ const calculateMapProps = (locations) => { // When that happens, we use zoom and center as a workaround const [{ latLong: center }] = locations; - return { zoom: '10', center }; + return { zoom: 10, center }; }; -const MapModal = ({ toggle, isOpen, title, locations }) => ( +const MapModal = ({ toggle, isOpen, title, locations = [] }: MapModalProps) => (

@@ -61,7 +54,4 @@ const MapModal = ({ toggle, isOpen, title, locations }) => ( ); -MapModal.propTypes = propTypes; -MapModal.defaultProps = defaultProps; - export default MapModal; diff --git a/src/visits/helpers/OpenMapModalBtn.js b/src/visits/helpers/OpenMapModalBtn.js deleted file mode 100644 index fab798c3..00000000 --- a/src/visits/helpers/OpenMapModalBtn.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useState } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons'; -import { Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap'; -import * as PropTypes from 'prop-types'; -import { useToggle } from '../../utils/helpers/hooks'; -import './OpenMapModalBtn.scss'; - -const propTypes = { - modalTitle: PropTypes.string.isRequired, - locations: PropTypes.arrayOf(PropTypes.object), - activeCities: PropTypes.arrayOf(PropTypes.string), -}; - -const OpenMapModalBtn = (MapModal) => { - const OpenMapModalBtn = ({ modalTitle, locations = [], activeCities }) => { - const [ mapIsOpened, , openMap, closeMap ] = useToggle(); - const [ dropdownIsOpened, toggleDropdown, openDropdown ] = useToggle(); - const [ locationsToShow, setLocationsToShow ] = useState([]); - - const buttonRef = React.createRef(); - const filterLocations = (locations) => locations.filter(({ cityName }) => activeCities.includes(cityName)); - const onClick = () => { - if (!activeCities) { - setLocationsToShow(locations); - openMap(); - - return; - } - - openDropdown(); - }; - const openMapWithLocations = (filtered) => () => { - setLocationsToShow(filtered ? filterLocations(locations) : locations); - openMap(); - }; - - return ( - - - buttonRef.current}>Show in map - - - Show all locations - Show locations in current page - - - - - ); - }; - - OpenMapModalBtn.propTypes = propTypes; - - return OpenMapModalBtn; -}; - -export default OpenMapModalBtn; diff --git a/src/visits/helpers/OpenMapModalBtn.tsx b/src/visits/helpers/OpenMapModalBtn.tsx new file mode 100644 index 00000000..6292ecba --- /dev/null +++ b/src/visits/helpers/OpenMapModalBtn.tsx @@ -0,0 +1,55 @@ +import React, { useRef, useState } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons'; +import { Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap'; +import { useToggle } from '../../utils/helpers/hooks'; +import { CityStats } from '../types'; +import MapModal from './MapModal'; +import './OpenMapModalBtn.scss'; + +interface OpenMapModalBtnProps { + modalTitle: string; + activeCities: string[]; + locations?: CityStats[]; +} + +const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapModalBtnProps) => { + const [ mapIsOpened, , openMap, closeMap ] = useToggle(); + const [ dropdownIsOpened, toggleDropdown, openDropdown ] = useToggle(); + const [ locationsToShow, setLocationsToShow ] = useState([]); + const buttonRef = useRef(); + + const filterLocations = (cities: CityStats[]) => cities.filter(({ cityName }) => activeCities.includes(cityName)); + const onClick = () => { + if (!activeCities) { + setLocationsToShow(locations); + openMap(); + + return; + } + + openDropdown(); + }; + const openMapWithLocations = (filtered: boolean) => () => { + setLocationsToShow(filtered ? filterLocations(locations) : locations); + openMap(); + }; + + return ( + + + buttonRef.current) as any}>Show in map + + + Show all locations + Show locations in current page + + + + + ); +}; + +export default OpenMapModalBtn; diff --git a/src/visits/services/provideServices.ts b/src/visits/services/provideServices.ts index 0d0c0b09..5880193c 100644 --- a/src/visits/services/provideServices.ts +++ b/src/visits/services/provideServices.ts @@ -2,7 +2,6 @@ import Bottle from 'bottlejs'; import ShortUrlVisits from '../ShortUrlVisits'; import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits'; import { getShortUrlDetail } from '../reducers/shortUrlDetail'; -import OpenMapModalBtn from '../helpers/OpenMapModalBtn'; import MapModal from '../helpers/MapModal'; import VisitsStats from '../VisitsStats'; import { createNewVisit } from '../reducers/visitCreation'; @@ -13,9 +12,8 @@ import * as visitsParser from './VisitsParser'; const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components - bottle.serviceFactory('OpenMapModalBtn', OpenMapModalBtn, 'MapModal'); bottle.serviceFactory('MapModal', () => MapModal); - bottle.serviceFactory('VisitsStats', VisitsStats, 'VisitsParser', 'OpenMapModalBtn'); + bottle.serviceFactory('VisitsStats', VisitsStats, 'VisitsParser'); bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsStats'); bottle.decorator('ShortUrlVisits', connect( [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ], diff --git a/test/visits/helpers/MapModal.test.js b/test/visits/helpers/MapModal.test.tsx similarity index 81% rename from test/visits/helpers/MapModal.test.js rename to test/visits/helpers/MapModal.test.tsx index 41a6a370..c58ab6fa 100644 --- a/test/visits/helpers/MapModal.test.js +++ b/test/visits/helpers/MapModal.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { Modal } from 'reactstrap'; import { Marker, Popup } from 'react-leaflet'; import MapModal from '../../../src/visits/helpers/MapModal'; +import { CityStats } from '../../../src/visits/types'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; const toggle = () => ''; const isOpen = true; const title = 'Foobar'; @@ -13,7 +14,7 @@ describe('', () => { const zaragozaLong = -0.876566; const newYorkLat = 40.730610; const newYorkLong = -73.935242; - const locations = [ + const locations: CityStats[] = [ { cityName: 'Zaragoza', count: 54, @@ -34,12 +35,12 @@ describe('', () => { it('renders modal with provided props', () => { const modal = wrapper.find(Modal); - const headerheader = wrapper.find('.map-modal__modal-title'); + const header = wrapper.find('.map-modal__modal-title'); expect(modal.prop('toggle')).toEqual(toggle); expect(modal.prop('isOpen')).toEqual(isOpen); - expect(headerheader.find('.close').prop('onClick')).toEqual(toggle); - expect(headerheader.text()).toContain(title); + expect(header.find('.close').prop('onClick')).toEqual(toggle); + expect(header.text()).toContain(title); }); it('renders open street map tile', () => { diff --git a/test/visits/helpers/OpenMapModalBtn.test.js b/test/visits/helpers/OpenMapModalBtn.test.tsx similarity index 65% rename from test/visits/helpers/OpenMapModalBtn.test.js rename to test/visits/helpers/OpenMapModalBtn.test.tsx index 895d9310..6bf9f04b 100644 --- a/test/visits/helpers/OpenMapModalBtn.test.js +++ b/test/visits/helpers/OpenMapModalBtn.test.tsx @@ -1,30 +1,25 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { Dropdown, DropdownItem, UncontrolledTooltip } from 'reactstrap'; -import createOpenMapModalBtn from '../../../src/visits/helpers/OpenMapModalBtn'; +import { Mock } from 'ts-mockery'; +import OpenMapModalBtn from '../../../src/visits/helpers/OpenMapModalBtn'; +import MapModal from '../../../src/visits/helpers/MapModal'; +import { CityStats } from '../../../src/visits/types'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; const title = 'Foo'; const locations = [ - { - cityName: 'foo', - count: 30, - }, - { - cityName: 'bar', - count: 45, - }, + Mock.of({ cityName: 'foo', count: 30 }), + Mock.of({ cityName: 'bar', count: 45 }), ]; - const MapModal = () => ''; - const OpenMapModalBtn = createOpenMapModalBtn(MapModal); - const createWrapper = (activeCities) => { - wrapper = mount(); + const createWrapper = (activeCities: string[] = []) => { + wrapper = shallow(); return wrapper; }; - afterEach(() => wrapper && wrapper.unmount()); + afterEach(() => wrapper?.unmount()); it('renders expected content', () => { const wrapper = createWrapper(); @@ -39,21 +34,6 @@ describe('', () => { expect(modal).toHaveLength(1); }); - it('sets provided props to the map', (done) => { - const wrapper = createWrapper(); - const button = wrapper.find('.open-map-modal-btn__btn'); - - button.simulate('click'); - setImmediate(() => { - const modal = wrapper.find(MapModal); - - expect(modal.prop('title')).toEqual(title); - expect(modal.prop('locations')).toEqual(locations); - expect(modal.prop('isOpen')).toEqual(true); - done(); - }); - }); - it('opens dropdown instead of modal when a list of active cities has been provided', (done) => { const wrapper = createWrapper([ 'bar' ]); const button = wrapper.find('.open-map-modal-btn__btn');