Migrated first visits helper components to TS

This commit is contained in:
Alejandro Celaya 2020-09-02 20:13:31 +02:00
parent d0d664ef79
commit f9c57ca659
10 changed files with 106 additions and 130 deletions

10
package-lock.json generated
View file

@ -3432,6 +3432,16 @@
"@types/react": "*" "@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": { "@types/react-redux": {
"version": "7.1.9", "version": "7.1.9",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz",

View file

@ -88,6 +88,7 @@
"@types/react-copy-to-clipboard": "^4.3.0", "@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-datepicker": "~1.8.0", "@types/react-datepicker": "~1.8.0",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-leaflet": "^2.5.2",
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.5",
"@types/react-tagsinput": "^3.19.7", "@types/react-tagsinput": "^3.19.7",

View file

@ -14,6 +14,7 @@ import GraphCard from './helpers/GraphCard';
import LineChartCard from './helpers/LineChartCard'; import LineChartCard from './helpers/LineChartCard';
import VisitsTable from './VisitsTable'; import VisitsTable from './VisitsTable';
import { VisitsInfoType } from './types'; import { VisitsInfoType } from './types';
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
const propTypes = { const propTypes = {
children: PropTypes.node, children: PropTypes.node,
@ -35,7 +36,7 @@ const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.
const format = formatDate(); const format = formatDate();
let selectedBar; let selectedBar;
const VisitsStats = ({ processStatsFromVisits, normalizeVisits }, OpenMapModalBtn) => { const VisitsStats = ({ processStatsFromVisits, normalizeVisits }) => {
const VisitsStatsComp = ({ children, visitsInfo, getVisits, cancelGetVisits, matchMedia = window.matchMedia }) => { const VisitsStatsComp = ({ children, visitsInfo, getVisits, cancelGetVisits, matchMedia = window.matchMedia }) => {
const [ startDate, setStartDate ] = useState(undefined); const [ startDate, setStartDate ] = useState(undefined);
const [ endDate, setEndDate ] = useState(undefined); const [ endDate, setEndDate ] = useState(undefined);

View file

@ -1,7 +1,7 @@
@import '../../utils/base'; @import '../../utils/base';
@import '../../utils/mixins/fit-with-margin'; @import '../../utils/mixins/fit-with-margin';
.map-modal__modal { .map-modal__modal.map-modal__modal {
@media (min-width: $mdMin) { @media (min-width: $mdMin) {
$margin: 20px; $margin: 20px;
@ -15,11 +15,11 @@
} }
} }
.map-modal__modal-content { .map-modal__modal-content.map-modal__modal-content {
height: 100%; height: 100%;
} }
.map-modal__modal-title { .map-modal__modal-title.map-modal__modal-title {
position: absolute; position: absolute;
width: 100%; width: 100%;
z-index: 1001; z-index: 1001;
@ -29,17 +29,17 @@
background: linear-gradient(rgba(0, 0, 0, .5), rgba(0, 0, 0, 0)); 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; padding: 0;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
} }
.map-modal__modal .leaflet-container { .map-modal__modal.map-modal__modal .leaflet-container.leaflet-container {
flex: 1 1 auto; flex: 1 1 auto;
border-radius: .3rem; 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; margin-top: 60px;
} }

View file

@ -1,32 +1,25 @@
import React from 'react'; import React, { FC } from 'react';
import { Modal, ModalBody } from 'reactstrap'; 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 { prop } from 'ramda';
import * as PropTypes from 'prop-types'; import { CityStats } from '../types';
import './MapModal.scss'; import './MapModal.scss';
const propTypes = { interface MapModalProps {
toggle: PropTypes.func, toggle: () => void;
isOpen: PropTypes.bool, isOpen: boolean;
title: PropTypes.string, title: string;
locations: PropTypes.arrayOf(PropTypes.shape({ locations?: CityStats[];
cityName: PropTypes.string.isRequired, }
latLong: PropTypes.arrayOf(PropTypes.number).isRequired,
count: PropTypes.number.isRequired,
})),
};
const defaultProps = {
locations: [],
};
const OpenStreetMapTile = () => ( const OpenStreetMapTile: FC = () => (
<TileLayer <TileLayer
attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/> />
); );
const calculateMapProps = (locations) => { const calculateMapProps = (locations: CityStats[]): Partial<MapProps> => {
if (locations.length === 0) { if (locations.length === 0) {
return {}; return {};
} }
@ -39,10 +32,10 @@ const calculateMapProps = (locations) => {
// When that happens, we use zoom and center as a workaround // When that happens, we use zoom and center as a workaround
const [{ latLong: center }] = locations; 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) => (
<Modal toggle={toggle} isOpen={isOpen} className="map-modal__modal" contentClassName="map-modal__modal-content"> <Modal toggle={toggle} isOpen={isOpen} className="map-modal__modal" contentClassName="map-modal__modal-content">
<ModalBody className="map-modal__modal-body"> <ModalBody className="map-modal__modal-body">
<h3 className="map-modal__modal-title"> <h3 className="map-modal__modal-title">
@ -61,7 +54,4 @@ const MapModal = ({ toggle, isOpen, title, locations }) => (
</Modal> </Modal>
); );
MapModal.propTypes = propTypes;
MapModal.defaultProps = defaultProps;
export default MapModal; export default MapModal;

View file

@ -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 (
<React.Fragment>
<button className="btn btn-link open-map-modal-btn__btn" ref={buttonRef} onClick={onClick}>
<FontAwesomeIcon icon={mapIcon} />
</button>
<UncontrolledTooltip placement="left" target={() => buttonRef.current}>Show in map</UncontrolledTooltip>
<Dropdown isOpen={dropdownIsOpened} toggle={toggleDropdown} inNavbar>
<DropdownMenu right>
<DropdownItem onClick={openMapWithLocations(false)}>Show all locations</DropdownItem>
<DropdownItem onClick={openMapWithLocations(true)}>Show locations in current page</DropdownItem>
</DropdownMenu>
</Dropdown>
<MapModal toggle={closeMap} isOpen={mapIsOpened} title={modalTitle} locations={locationsToShow} />
</React.Fragment>
);
};
OpenMapModalBtn.propTypes = propTypes;
return OpenMapModalBtn;
};
export default OpenMapModalBtn;

View file

@ -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<CityStats[]>([]);
const buttonRef = useRef<HTMLElement>();
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 (
<React.Fragment>
<button className="btn btn-link open-map-modal-btn__btn" ref={buttonRef as any} onClick={onClick}>
<FontAwesomeIcon icon={mapIcon} />
</button>
<UncontrolledTooltip placement="left" target={(() => buttonRef.current) as any}>Show in map</UncontrolledTooltip>
<Dropdown isOpen={dropdownIsOpened} toggle={toggleDropdown} inNavbar>
<DropdownMenu right>
<DropdownItem onClick={openMapWithLocations(false)}>Show all locations</DropdownItem>
<DropdownItem onClick={openMapWithLocations(true)}>Show locations in current page</DropdownItem>
</DropdownMenu>
</Dropdown>
<MapModal toggle={closeMap} isOpen={mapIsOpened} title={modalTitle} locations={locationsToShow} />
</React.Fragment>
);
};
export default OpenMapModalBtn;

View file

@ -2,7 +2,6 @@ import Bottle from 'bottlejs';
import ShortUrlVisits from '../ShortUrlVisits'; import ShortUrlVisits from '../ShortUrlVisits';
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits'; import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
import { getShortUrlDetail } from '../reducers/shortUrlDetail'; import { getShortUrlDetail } from '../reducers/shortUrlDetail';
import OpenMapModalBtn from '../helpers/OpenMapModalBtn';
import MapModal from '../helpers/MapModal'; import MapModal from '../helpers/MapModal';
import VisitsStats from '../VisitsStats'; import VisitsStats from '../VisitsStats';
import { createNewVisit } from '../reducers/visitCreation'; import { createNewVisit } from '../reducers/visitCreation';
@ -13,9 +12,8 @@ import * as visitsParser from './VisitsParser';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components // Components
bottle.serviceFactory('OpenMapModalBtn', OpenMapModalBtn, 'MapModal');
bottle.serviceFactory('MapModal', () => MapModal); bottle.serviceFactory('MapModal', () => MapModal);
bottle.serviceFactory('VisitsStats', VisitsStats, 'VisitsParser', 'OpenMapModalBtn'); bottle.serviceFactory('VisitsStats', VisitsStats, 'VisitsParser');
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsStats'); bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsStats');
bottle.decorator('ShortUrlVisits', connect( bottle.decorator('ShortUrlVisits', connect(
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ], [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ],

View file

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Modal } from 'reactstrap'; import { Modal } from 'reactstrap';
import { Marker, Popup } from 'react-leaflet'; import { Marker, Popup } from 'react-leaflet';
import MapModal from '../../../src/visits/helpers/MapModal'; import MapModal from '../../../src/visits/helpers/MapModal';
import { CityStats } from '../../../src/visits/types';
describe('<MapModal />', () => { describe('<MapModal />', () => {
let wrapper; let wrapper: ShallowWrapper;
const toggle = () => ''; const toggle = () => '';
const isOpen = true; const isOpen = true;
const title = 'Foobar'; const title = 'Foobar';
@ -13,7 +14,7 @@ describe('<MapModal />', () => {
const zaragozaLong = -0.876566; const zaragozaLong = -0.876566;
const newYorkLat = 40.730610; const newYorkLat = 40.730610;
const newYorkLong = -73.935242; const newYorkLong = -73.935242;
const locations = [ const locations: CityStats[] = [
{ {
cityName: 'Zaragoza', cityName: 'Zaragoza',
count: 54, count: 54,
@ -34,12 +35,12 @@ describe('<MapModal />', () => {
it('renders modal with provided props', () => { it('renders modal with provided props', () => {
const modal = wrapper.find(Modal); 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('toggle')).toEqual(toggle);
expect(modal.prop('isOpen')).toEqual(isOpen); expect(modal.prop('isOpen')).toEqual(isOpen);
expect(headerheader.find('.close').prop('onClick')).toEqual(toggle); expect(header.find('.close').prop('onClick')).toEqual(toggle);
expect(headerheader.text()).toContain(title); expect(header.text()).toContain(title);
}); });
it('renders open street map tile', () => { it('renders open street map tile', () => {

View file

@ -1,30 +1,25 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Dropdown, DropdownItem, UncontrolledTooltip } from 'reactstrap'; 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('<OpenMapModalBtn />', () => { describe('<OpenMapModalBtn />', () => {
let wrapper; let wrapper: ShallowWrapper;
const title = 'Foo'; const title = 'Foo';
const locations = [ const locations = [
{ Mock.of<CityStats>({ cityName: 'foo', count: 30 }),
cityName: 'foo', Mock.of<CityStats>({ cityName: 'bar', count: 45 }),
count: 30,
},
{
cityName: 'bar',
count: 45,
},
]; ];
const MapModal = () => ''; const createWrapper = (activeCities: string[] = []) => {
const OpenMapModalBtn = createOpenMapModalBtn(MapModal); wrapper = shallow(<OpenMapModalBtn modalTitle={title} locations={locations} activeCities={activeCities} />);
const createWrapper = (activeCities) => {
wrapper = mount(<OpenMapModalBtn modalTitle={title} locations={locations} activeCities={activeCities} />);
return wrapper; return wrapper;
}; };
afterEach(() => wrapper && wrapper.unmount()); afterEach(() => wrapper?.unmount());
it('renders expected content', () => { it('renders expected content', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
@ -39,21 +34,6 @@ describe('<OpenMapModalBtn />', () => {
expect(modal).toHaveLength(1); 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) => { it('opens dropdown instead of modal when a list of active cities has been provided', (done) => {
const wrapper = createWrapper([ 'bar' ]); const wrapper = createWrapper([ 'bar' ]);
const button = wrapper.find('.open-map-modal-btn__btn'); const button = wrapper.find('.open-map-modal-btn__btn');