diff --git a/src/GpsFormatToggle/index.js b/src/GpsFormatToggle/index.js
index 9935174b9..0f116626f 100644
--- a/src/GpsFormatToggle/index.js
+++ b/src/GpsFormatToggle/index.js
@@ -23,8 +23,7 @@ const GpsFormatToggle = (props) => {
});
};
- const gpsString = showGpsString && calcGpsDisplayString(lat, lng, currentFormat);
- const displayGpsString = gpsString || null;
+ const gpsString = showGpsString ? calcGpsDisplayString(lat, lng, currentFormat) : null;
return (
@@ -34,9 +33,9 @@ const GpsFormatToggle = (props) => {
onClick={() => onGpsFormatClick(gpsFormat)}>{gpsFormat}
)}
- {displayGpsString &&
-
{displayGpsString}
- {showCopyControl &&
}
+ {gpsString &&
+ {gpsString}
+ {showCopyControl && }
}
diff --git a/src/HeatmapToggleButton/index.js b/src/HeatmapToggleButton/index.js
index 0b044fecf..42032b3f0 100644
--- a/src/HeatmapToggleButton/index.js
+++ b/src/HeatmapToggleButton/index.js
@@ -2,18 +2,20 @@ import React, { memo } from 'react';
import noop from 'lodash/noop';
import PropTypes from 'prop-types';
import styles from './styles.module.scss';
-import LoadingOverlay from '../LoadingOverlay';
+
+import SubjectControlButton from '../SubjectControls/button';
const HeatmapToggleButton = (props) => {
- const { className: externalClass, heatmapVisible, heatmapPartiallyVisible, onButtonClick, showLabel, loading } = props;
- const className = heatmapVisible ? 'visible' : heatmapPartiallyVisible ? 'partial' : '';
- const labelText = className ? 'Heatmap on' : 'Heatmap off';
-
- return
- {loading && }
-
- {showLabel && {labelText} }
-
;
+ const { className: externalClass, heatmapVisible, heatmapPartiallyVisible, onButtonClick, ...rest } = props;
+
+ const stateClassName = heatmapVisible ? 'visible' : heatmapPartiallyVisible ? 'partial' : '';
+
+ const containerClassName = `${styles.container} ${stateClassName}`;
+ const buttonClassName = `${styles.button} ${styles[stateClassName]} ${externalClass || ''}`;
+
+ const labelText = stateClassName ? 'Heatmap on' : 'Heatmap off';
+
+ return
;
};
export default memo(HeatmapToggleButton);
diff --git a/src/HeatmapToggleButton/styles.module.scss b/src/HeatmapToggleButton/styles.module.scss
index 0613adcb9..c2ee9177b 100644
--- a/src/HeatmapToggleButton/styles.module.scss
+++ b/src/HeatmapToggleButton/styles.module.scss
@@ -1,92 +1,15 @@
@import '../common/styles/buttons';
-
-$background_image_url: '../common/images/icons/';
-
-@mixin button($image: 'heatmap') {
- background: url('#{$background-image_url+$image}.svg');
- background-size: cover;
- border: none;
- display: block;
- filter: saturate(50%) grayscale(100%);
- height: $square-button-dimension;
- outline: none;
- width: $square-button-dimension;
-
- &:focus {
- outline: none;
- }
-}
-
-.button {
- @include button;
- &.visible {
- filter: unset;
- }
-}
+@import '../SubjectControls/mixins';
.container {
- align-items: center;
- display: flex;
- flex-flow: row;
- position: relative;
-
- &.hasLabel {
- background-color: rgba($secondary-light-gray, .25);
- border: 0.06rem solid $secondary-medium-light-gray;
- border-radius: .12rem;
- max-width: 5.75rem;
- max-height: 1.85rem;
- border-radius: 0.2rem;
- cursor: pointer;
-
- [class*=spinner] {
- position: relative;
- top: -16%;
+ .button {
+ @include buttonBackground('heatmap');
+ filter: saturate(50%) grayscale(100%);
+ &.visible {
+ filter: unset;
}
-
- button {
- min-height: 1.80rem;
- height: 1.80rem;
- min-width: 1.80rem;
- width: 1.80rem;
- }
-
- span {
- font-size: 0.68rem;
- line-height: normal;
- padding: .1rem .7rem;
- margin: 0 -.2rem;
- }
-
- &[class*=visible] {
- border: 0.06px solid $bright-blue;
- background-color: rgba($bright-blue, 0.1);
- }
-
- &[class*=pinned] {
- border: 0.06px solid $green;
- background-color: rgba($green, 0.1);
+ &.partial {
+ filter: opacity(50%);
}
}
-
-}
-
-.visible {
- @include button;
}
-
-.partial {
- filter: opacity(50%);
-}
-
-div.loadingOverlay {
- background: none;
- z-index: 10;
- [class*=spinner] {
- height: 1rem;
- width: 1rem;
- &::after {
- background: none;
- }
- }
-}
\ No newline at end of file
diff --git a/src/SideBar/styles.module.scss b/src/SideBar/styles.module.scss
index 5d8d1bfa1..7070c5ac1 100644
--- a/src/SideBar/styles.module.scss
+++ b/src/SideBar/styles.module.scss
@@ -445,7 +445,9 @@
}
.controls {
- div > button, button:not(:last-child) {
+ > div:not(:last-child) {
+ height: 2.25rem;
+ width: 2.25rem;
margin-right: 0.5rem;
border-radius: 0.25rem;
}
diff --git a/src/StaticSensorsLayer/index.js b/src/StaticSensorsLayer/index.js
index 7b61654b7..fd06480d7 100644
--- a/src/StaticSensorsLayer/index.js
+++ b/src/StaticSensorsLayer/index.js
@@ -1,20 +1,20 @@
import React, { useContext, memo, useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
-import { Provider, connect } from 'react-redux';
-import ReactDOM from 'react-dom';
+import { connect } from 'react-redux';
import mapboxgl from 'mapbox-gl';
import set from 'lodash/set';
import isEmpty from 'lodash/isEmpty';
-import store from '../store';
import { MapContext } from '../App';
import { DEVELOPMENT_FEATURE_FLAGS, LAYER_IDS, SUBJECT_FEATURE_CONTENT_TYPE } from '../constants';
import { addFeatureCollectionImagesToMap } from '../utils/map';
import { getSubjectDefaultDeviceProperty } from '../utils/subjects';
+
+import { showPopup } from '../ducks/popup';
+
import { BACKGROUND_LAYER, LABELS_LAYER } from './layerStyles';
import LayerBackground from '../common/images/sprites/layer-background-sprite.png';
-import SubjectPopup from '../SubjectPopup';
const { ENABLE_NEW_CLUSTERING } = DEVELOPMENT_FEATURE_FLAGS;
@@ -38,7 +38,7 @@ const IMAGE_DATA = {
const popup = new mapboxgl.Popup({ offset: [0, 0], anchor: 'bottom', closeButton: false });
-const StaticSensorsLayer = ({ staticSensors = {}, isTimeSliderActive, showMapNames, simplifyMapDataOnZoom: { enabled: isDataInMapSimplified } }) => {
+const StaticSensorsLayer = ({ staticSensors = {}, isTimeSliderActive, showMapNames, simplifyMapDataOnZoom: { enabled: isDataInMapSimplified }, showPopup }) => {
const map = useContext(MapContext);
const showMapStaticSubjectsNames = showMapNames[STATIC_SENSOR]?.enabled ?? false;
const [clickedLayerID, setClickedLayerID] = useState('');
@@ -94,31 +94,32 @@ const StaticSensorsLayer = ({ staticSensors = {}, isTimeSliderActive, showMapNam
}
}, [map]);
- const createPopup = useCallback((layer) => {
- const { geometry } = layer;
-
- const elementContainer = document.createElement('div');
- ReactDOM.render(
-
- , elementContainer);
+ const showPopupForStationarySubject = useCallback((layer) => {
+ const { geometry, properties } = layer;
- popup.setLngLat(geometry.coordinates)
- .setDOMContent(elementContainer)
- .addTo(map);
-
- popup.on('close', () => {
+ const handleMapClick = () => {
setClickedLayerID('');
setLayerVisibility(layer.layer.id);
- });
- }, [map, setLayerVisibility]);
+ };
+
+ showPopup('subject', { geometry, properties, coordinates: geometry.coordinates });
+ setTimeout(() => map.once('click', handleMapClick));
+
+ }, [map, setLayerVisibility, showPopup]);
const onLayerClick = useCallback((event) => {
+ if (event?.originalEvent?.cancelBubble) return;
+
+ event?.preventDefault();
+ event?.originalEvent?.stopPropagation();
+
const clickedLayer = getStaticSensorLayer(event);
+
const clickedLayerID = clickedLayer.layer.id;
setClickedLayerID(clickedLayerID.replace(PREFIX_ID, ''));
- createPopup(clickedLayer);
+ showPopupForStationarySubject(clickedLayer);
setLayerVisibility(clickedLayerID, false);
- }, [getStaticSensorLayer, setClickedLayerID, createPopup, setLayerVisibility]);
+ }, [getStaticSensorLayer, showPopupForStationarySubject, setClickedLayerID, setLayerVisibility]);
const createLayer = useCallback((layerID, sourceId, layout, paint) => {
if (!map.getLayer(layerID)) {
@@ -198,7 +199,7 @@ const StaticSensorsLayer = ({ staticSensors = {}, isTimeSliderActive, showMapNam
const mapStatetoProps = ({ view: { showMapNames, simplifyMapDataOnZoom } }) => ({ showMapNames, simplifyMapDataOnZoom });
-export default connect(mapStatetoProps, null)(memo(StaticSensorsLayer));
+export default connect(mapStatetoProps, { showPopup })(memo(StaticSensorsLayer));
StaticSensorsLayer.propTypes = {
staticSensors: PropTypes.object.isRequired,
diff --git a/src/SubjectControls/_mixins.scss b/src/SubjectControls/_mixins.scss
new file mode 100644
index 000000000..4dd43a643
--- /dev/null
+++ b/src/SubjectControls/_mixins.scss
@@ -0,0 +1,37 @@
+@import '../common/styles/buttons';
+@import '../common/styles/vars/colors';
+
+
+@mixin buttonBackground($image: 'tracks_off') {
+ $background_image_url: '../common/images/icons/';
+ background-image: url('#{$background_image_url+$image}.svg');
+}
+
+@mixin control {
+ align-items: center;
+ background-size: cover;
+ cursor: pointer;
+ display: flex;
+ flex-flow: row;
+ line-height: normal;
+ position: relative;
+
+ .button {
+ @include buttonBackground();
+ background-size: cover;
+ background-color: $subject-control-btn-bg;
+ border: none;
+ display: block;
+ height: $square-button-dimension;
+ outline: none;
+ width: $square-button-dimension;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.visible {
+ filter: unset;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SubjectControls/button.js b/src/SubjectControls/button.js
new file mode 100644
index 000000000..163fbb6da
--- /dev/null
+++ b/src/SubjectControls/button.js
@@ -0,0 +1,35 @@
+import React, { forwardRef, memo } from 'react';
+import noop from 'lodash/noop';
+import PropTypes from 'prop-types';
+import LoadingOverlay from '../LoadingOverlay';
+import styles from './styles.module.scss';
+
+const SubjectControlButton = (props, ref) => {
+ const { buttonClassName = '', containerClassName = '', disabled = false, labelText, onClick, showLabel, loading, ...rest } = props;
+
+ return
+ {loading && }
+
+ {showLabel && labelText && {labelText} }
+
;
+};
+
+const memoizedSubjectControlButton = memo(forwardRef(SubjectControlButton));
+
+export default memoizedSubjectControlButton;
+
+memoizedSubjectControlButton.defaultProps = {
+ onClick: noop,
+ showLabel: true,
+ loading: false,
+};
+
+memoizedSubjectControlButton.propTypes = {
+ buttonClassName: PropTypes.string,
+ containerClassName: PropTypes.string,
+ disabled: PropTypes.bool,
+ labelText: PropTypes.string,
+ onClick: PropTypes.func,
+ showLabel: PropTypes.bool,
+ loading: PropTypes.bool,
+};
\ No newline at end of file
diff --git a/src/SubjectControls/index.js b/src/SubjectControls/index.js
index ab2c51408..1a7b87e36 100644
--- a/src/SubjectControls/index.js
+++ b/src/SubjectControls/index.js
@@ -1,8 +1,9 @@
-import React, { memo, useState } from 'react';
+import React, { lazy, memo, useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { usePermissions } from '../hooks';
+import { addModal } from '../ducks/modals';
import { PERMISSION_KEYS, PERMISSIONS } from '../constants';
@@ -11,24 +12,32 @@ import { addHeatmapSubjects, removeHeatmapSubjects, toggleTrackState } from '../
import TrackToggleButton from '../TrackToggleButton';
import HeatmapToggleButton from '../HeatmapToggleButton';
import SubjectMessagesPopover from '../SubjectMessagesPopover';
+import SubjectHistoryButton from '../SubjectHistoryButton';
import LocationJumpButton from '../LocationJumpButton';
import { trackEventFactory, MAP_LAYERS_CATEGORY } from '../utils/analytics';
+import { subjectIsStatic } from '../utils/subjects';
+
+
import { getSubjectControlState } from './selectors';
import { fetchTracksIfNecessary } from '../utils/tracks';
import styles from './styles.module.scss';
+const SubjectHistoricalDataModal = lazy(() => import('../SubjectHistoricalDataModal'));
+
const mapLayerTracker = trackEventFactory(MAP_LAYERS_CATEGORY);
const SubjectControls = (props) => {
const { subject,
+ addModal,
children,
showHeatmapButton,
showTrackButton,
showJumpButton,
showMessageButton,
+ showHistoryButton,
showTitles,
showLabels,
className,
@@ -39,6 +48,7 @@ const SubjectControls = (props) => {
tracksLoaded,
tracksVisible,
tracksPinned,
+ map: _map,
...rest } = props;
const [ loadingHeatmap, setHeatmapLoadingState ] = useState(false);
@@ -49,8 +59,11 @@ const SubjectControls = (props) => {
const isMessageable = !!canViewMessages && !!showMessageButton && !!subject?.messaging?.length;
+ const hasAdditionalDeviceProps = !!subject?.device_status_properties?.length;
const canShowTrack = canShowTrackForSubject(subject);
+ const canShowHistoryButton = showHistoryButton && (subjectIsStatic(subject) ? !!hasAdditionalDeviceProps : true);
+
const fetchSubjectTracks = () => {
if (tracksLoaded) return new Promise(resolve => resolve());
return fetchTracksIfNecessary([id]);
@@ -89,15 +102,15 @@ const SubjectControls = (props) => {
}
};
+ const onHistoricalDataClick = useCallback(() => {
+ addModal({ content: SubjectHistoricalDataModal, subjectId: subject.id, subjectIsStatic: subjectIsStatic(subject), title: `Historical Data: ${subject.name}` });
+ }, [addModal, subject]);
+
if (!showHeatmapButton && !showTrackButton && !showJumpButton) return null;
return
- {isMessageable &&
}
{showTrackButton && canShowTrack &&
{
heatmapVisible={subjectIsInHeatmap}
/>}
+ {isMessageable && }
+
+ {canShowHistoryButton && }
+
{showJumpButton && coordinates && getSubjectControlState(state, props);
-export default connect(mapStateToProps, { toggleTrackState, addHeatmapSubjects, removeHeatmapSubjects })(memo(SubjectControls));
+export default connect(mapStateToProps, { addModal, toggleTrackState, addHeatmapSubjects, removeHeatmapSubjects })(memo(SubjectControls));
diff --git a/src/SubjectControls/index.test.js b/src/SubjectControls/index.test.js
new file mode 100644
index 000000000..368ef7e0a
--- /dev/null
+++ b/src/SubjectControls/index.test.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { subjectIsStatic } from '../utils/subjects';
+
+import mockSubjectData from '../__test-helpers/fixtures/subjects';
+
+import { Provider } from 'react-redux';
+import { mockStore } from '../__test-helpers/MockStore';
+import NavigationWrapper from '../__test-helpers/navigationWrapper';
+
+import { addModal } from '../ducks/modals';
+
+import SubjectControls from './';
+
+jest.mock('../ducks/modals', () => {
+ const real = jest.requireActual('../ducks/modals');
+ return {
+ ...real,
+ addModal: jest.fn(),
+ };
+});
+
+describe('SubjectControls', () => {
+ let store, addModalMock;
+ const [subject] = mockSubjectData;
+ const buttonTestId = `history-button-${subject.id}`;
+
+ beforeEach(() => {
+ addModalMock = jest.fn(() => () => {});
+ addModal.mockImplementation(addModalMock);
+ store = mockStore({
+ data: {
+ tracks: {},
+ },
+ view: {
+ subjectTrackState: { pinned: [], visible: [] },
+ heatmapSubjectIDs: [],
+ },
+ });
+ });
+
+ test('rendering without crashing', () => {
+ render(
+
+
+
+
+ );
+ });
+
+ describe('the histrical data button', () => {
+
+
+ test('showing the historical data button', async () => {
+ render(
+
+
+
+
+
+ );
+
+ await screen.findByTestId(buttonTestId);
+
+ });
+
+ test('showing the history modal on button click', async () => {
+
+ render(
+
+
+
+
+
+ );
+
+ const button = await screen.findByTestId(buttonTestId);
+ userEvent.click(button);
+
+ expect(addModal).toHaveBeenCalledWith(expect.objectContaining({ subjectId: subject.id, subjectIsStatic: subjectIsStatic(subject), title: `Historical Data: ${subject.name}` }));
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/SubjectControls/styles.module.scss b/src/SubjectControls/styles.module.scss
index e4412cd39..e977c79ab 100644
--- a/src/SubjectControls/styles.module.scss
+++ b/src/SubjectControls/styles.module.scss
@@ -1,8 +1,11 @@
-@import '../common/styles/layout';
+
+@import '../common/styles/vars/colors';
+@import './mixins';
.controls {
+ align-items: center;
display: flex;
- justify-content: space-around;
+ justify-content: flex-start;
&.noTitles {
span {
display: none;
@@ -10,12 +13,43 @@
}
}
-.messagingButton {
- border: none;
- padding: 0rem;
+.container {
+ position: relative;
+ @include control;
+}
+
+.loadingOverlay {
+ z-index: 1000;
+}
+
+
+.hasLabel {
+ background-color: rgba($secondary-light-gray, .25);
+ border: 0.06rem solid $secondary-medium-light-gray;
+ border-radius: 0.12rem;
+ max-width: 5.75rem;
+ max-height: 1.85rem;
+ border-radius: 0.2rem;
+ overflow: hidden;
+ cursor: pointer;
- svg {
- height: 2.2rem;
- width: 2.2rem;
+ [class*=spinner] {
+ position: relative;
+ top: -16%;
}
+
+ .button {
+ min-height: 1.80rem;
+ height: 1.80rem;
+ min-width: 1.80rem;
+ width: 1.80rem;
+ }
+
+ span {
+ font-size: 0.65rem;
+ line-height: normal;
+ padding: 0.1rem 0.3rem;
+ margin: 0 -.2rem;
+ }
+
}
\ No newline at end of file
diff --git a/src/SubjectHistoricalDataModal/index.js b/src/SubjectHistoricalDataModal/index.js
index 516a70d7c..49548a749 100644
--- a/src/SubjectHistoricalDataModal/index.js
+++ b/src/SubjectHistoricalDataModal/index.js
@@ -10,6 +10,9 @@ import startCase from 'lodash/startCase';
import { fetchObservationsForSubject } from '../ducks/observations';
import { removeModal } from '../ducks/modals';
+
+import { calcGpsDisplayString } from '../utils/location';
+
import LoadingOverlay from '../LoadingOverlay';
import DateTime from '../DateTime';
@@ -26,7 +29,7 @@ export const getObservationUniqProperties = (observations) => {
return uniqPropertiesByLabel.map(property => property.label);
};
-const SubjectHistoricalDataModal = ({ title, subjectId, fetchObservationsForSubject }) => {
+const SubjectHistoricalDataModal = ({ gpsFormat, title, subjectId, subjectIsStatic, fetchObservationsForSubject }) => {
const [loading, setLoadState] = useState(true);
const [subjectObservations, setSubjectObservations] = useState([]);
const [observationsCount, setObservationsCount] = useState(1);
@@ -44,10 +47,6 @@ const SubjectHistoricalDataModal = ({ title, subjectId, fetchObservationsForSubj
});
}, [fetchObservationsForSubject, subjectId]);
- useEffect(() => {
- if (activePage === 1) fetchObservations();
- }, [activePage, fetchObservations]);
-
useEffect(() => {
fetchObservations(activePage);
}, [activePage, fetchObservations]);
@@ -75,15 +74,20 @@ const SubjectHistoricalDataModal = ({ title, subjectId, fetchObservationsForSubj
Date
{observationProperties.map(property => {startCase(property)} )}
+ {!subjectIsStatic && Location }
- {subjectObservations.map(({ id, recorded_at, device_status_properties }) =>
-
+ {subjectObservations.map(({ id, recorded_at, location, device_status_properties }) => {
+ const locationString = !subjectIsStatic && calcGpsDisplayString(location.latitude, location.longitude, gpsFormat);
+
+ return
{observationProperties.map(property => {getMatchedProperty(property, device_status_properties)} )}
-
- )}
+ {!!locationString && {locationString}
+ }
+ ;
+ })}
{observationsCount > ITEMS_PER_PAGE && ({ gpsFormat });
-export default connect(null, { fetchObservationsForSubject, removeModal })(memo(SubjectHistoricalDataModal));
\ No newline at end of file
+export default connect(mapStateToProps, { fetchObservationsForSubject, removeModal })(memo(SubjectHistoricalDataModal));
\ No newline at end of file
diff --git a/src/SubjectHistoricalDataModal/index.test.js b/src/SubjectHistoricalDataModal/index.test.js
index 40f05f313..38d79b4e8 100644
--- a/src/SubjectHistoricalDataModal/index.test.js
+++ b/src/SubjectHistoricalDataModal/index.test.js
@@ -7,6 +7,7 @@ import userEvent from '@testing-library/user-event';
import { fetchObservationsForSubject } from '../ducks/observations';
import { mockStore } from '../__test-helpers/MockStore';
+import { GPS_FORMATS } from '../utils/location';
import mockedObservationsData from '../__test-helpers/fixtures/observations';
import SubjectHistoricalDataModal, { ITEMS_PER_PAGE, getObservationUniqProperties } from './';
@@ -16,7 +17,7 @@ jest.mock('../ducks/observations', () => ({
fetchObservationsForSubject: jest.fn(),
}));
-const store = mockStore({ data: {}, view: {} });
+const store = mockStore({ data: {}, view: { userPreferences: { gpsFormat: GPS_FORMATS.DEG } } });
describe('SubjectHistoricalDataModal', () => {
let fetchObservationsForSubjectMock;
@@ -56,7 +57,8 @@ describe('SubjectHistoricalDataModal', () => {
expect(tableHeaders[2].childNodes[0]).toHaveTextContent('Temperature');
expect(tableCells[2].childNodes[0]).toHaveTextContent('1000 c');
- expect(tableHeaders[3]).toBe(undefined);
+ expect(tableHeaders[3].childNodes[0]).toHaveTextContent('Location');
+ expect(tableCells[3].childNodes[0]).toHaveTextContent('20.701133°, -103.572941°');
});
});
describe('pagination', () => {
diff --git a/src/SubjectHistoryButton/PatrolAwareTrackToggleButton.js b/src/SubjectHistoryButton/PatrolAwareTrackToggleButton.js
new file mode 100644
index 000000000..ef1176f16
--- /dev/null
+++ b/src/SubjectHistoryButton/PatrolAwareTrackToggleButton.js
@@ -0,0 +1,69 @@
+import React, { memo, useCallback, useMemo } from 'react';
+import { connect } from 'react-redux';
+import isEqual from 'react-fast-compare';
+
+import { togglePatrolTrackState } from '../ducks/patrols';
+import { toggleTrackState } from '../ducks/map-ui';
+
+import { trackEventFactory, PATROL_LIST_ITEM_CATEGORY } from '../utils/analytics';
+
+import TrackToggleButton from './';
+
+const patrolListItemTracker = trackEventFactory(PATROL_LIST_ITEM_CATEGORY);
+
+const PatrolAwareTrackToggleButton = ({ buttonRef, dispatch: _dispatch, patrolData, patrolTrackState, subjectTrackState, togglePatrolTrackState, toggleTrackState, ...rest }) => {
+ const { patrol, leader } = patrolData;
+
+ const patrolTrackPinned = useMemo(() => patrolTrackState.pinned.includes(patrol.id), [patrol.id, patrolTrackState.pinned]);
+ const patrolTrackVisible = useMemo(() => !patrolTrackPinned && patrolTrackState.visible.includes(patrol.id), [patrol.id, patrolTrackPinned, patrolTrackState.visible]);
+ const patrolTrackHidden = useMemo(() => !patrolTrackPinned && !patrolTrackVisible, [patrolTrackPinned, patrolTrackVisible]);
+
+ const subjectTrackPinned = useMemo(() => !!leader && subjectTrackState.pinned.includes(leader.id), [leader, subjectTrackState.pinned]);
+ const subjectTrackVisible = useMemo(() => !!leader && !subjectTrackPinned && subjectTrackState.visible.includes(leader.id), [leader, subjectTrackPinned, subjectTrackState.visible]);
+ const subjectTrackHidden = useMemo(() => !subjectTrackPinned && !subjectTrackVisible, [subjectTrackPinned, subjectTrackVisible]);
+ // trackVisible={patrolTrackVisible} trackPinned={patrolTrackPinned} onClick={onTrackButtonClick}
+
+ const patrolToggleStates = useMemo(() => [patrolTrackPinned, patrolTrackVisible, patrolTrackHidden], [patrolTrackHidden, patrolTrackPinned, patrolTrackVisible]);
+ const subjectToggleStates = useMemo(() => [subjectTrackPinned, subjectTrackVisible, subjectTrackHidden], [subjectTrackHidden, subjectTrackPinned, subjectTrackVisible]);
+
+ const onTrackButtonClick = useCallback(() => {
+ const nextPatrolTrackStateIfToggled = patrolTrackPinned
+ ? 'hidden'
+ : patrolTrackHidden
+ ? 'visible'
+ : 'pinned';
+
+ if (!leader) return;
+ const actionToTrack = `Toggle patrol track state to ${nextPatrolTrackStateIfToggled} from patrol card popover`;
+
+ if (isEqual(patrolToggleStates, subjectToggleStates)) {
+ toggleTrackState(leader.id);
+ togglePatrolTrackState(patrol.id);
+ patrolListItemTracker.track(actionToTrack);
+ return;
+ }
+ if (!patrolTrackHidden && subjectTrackHidden) {
+ togglePatrolTrackState(patrol.id);
+ patrolListItemTracker.track(actionToTrack);
+ return;
+ }
+ if (subjectTrackPinned && patrolTrackVisible) {
+ togglePatrolTrackState(patrol.id);
+ patrolListItemTracker.track(actionToTrack);
+ }
+ if (patrolTrackPinned && subjectTrackVisible) {
+ toggleTrackState(leader.id);
+ }
+ if (patrolTrackHidden && !subjectTrackHidden) {
+ toggleTrackState(leader.id);
+ return;
+ }
+ }, [leader, patrol.id, patrolToggleStates, patrolTrackHidden, patrolTrackPinned, patrolTrackVisible, subjectToggleStates, subjectTrackHidden, subjectTrackPinned, subjectTrackVisible, togglePatrolTrackState, toggleTrackState]);
+
+ return ;
+};
+
+
+const mapStateToProps = ({ view: { patrolTrackState, subjectTrackState } }) => ({ patrolTrackState, subjectTrackState });
+
+export default connect(mapStateToProps, { togglePatrolTrackState, toggleTrackState })(memo(PatrolAwareTrackToggleButton));
\ No newline at end of file
diff --git a/src/SubjectHistoryButton/index.js b/src/SubjectHistoryButton/index.js
new file mode 100644
index 000000000..ff9aefaf0
--- /dev/null
+++ b/src/SubjectHistoryButton/index.js
@@ -0,0 +1,26 @@
+import React, { forwardRef, memo } from 'react';
+import noop from 'lodash/noop';
+import PropTypes from 'prop-types';
+import styles from './styles.module.scss';
+
+import SubjectControlButton from '../SubjectControls/button';
+
+const SubjectHistoryButton = (props, ref) => {
+
+ return ;
+};
+
+export default memo(forwardRef(SubjectHistoryButton));
+
+SubjectHistoryButton.defaultProps = {
+ onClick: noop,
+ showLabel: true,
+};
+
+SubjectHistoryButton.propTypes = {
+ trackVisible: PropTypes.bool.isRequired,
+ trackPinned: PropTypes.bool.isRequired,
+ onClick: PropTypes.func,
+ showLabel: PropTypes.bool,
+ loading: PropTypes.bool,
+};
\ No newline at end of file
diff --git a/src/SubjectHistoryButton/index.test.js b/src/SubjectHistoryButton/index.test.js
new file mode 100644
index 000000000..83efb4792
--- /dev/null
+++ b/src/SubjectHistoryButton/index.test.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { render, screen, userEvent, waitFor } from '@testing-library/react';
+
+import SubjectHistoryButton from './';
+
+const onClick = jest.fn();
+
+test('rendering without crashing', () => {
+ render( );
+});
+
+test('call onClick', () => {
+ render( );
+ waitFor(async () => {
+ const toggleButton = await screen.getByRole('button');
+ userEvent.click(toggleButton);
+ expect(onClick).toHaveBeenCalled();
+ });
+});
+
+describe('setting label visibility', () => {
+ test('showing the label showLabel is true', () => {
+ render();
+ expect(screen.getByText('Historical Data')).toBeTruthy();
+ });
+
+
+ test('not showing any label if showLabel is false', () => {
+ render();
+ expect(() => screen.getByText('Historical Data')).toThrow();
+ });
+});
\ No newline at end of file
diff --git a/src/SubjectHistoryButton/styles.module.scss b/src/SubjectHistoryButton/styles.module.scss
new file mode 100644
index 000000000..b22539f3b
--- /dev/null
+++ b/src/SubjectHistoryButton/styles.module.scss
@@ -0,0 +1,47 @@
+@import '../common/styles/buttons';
+@import '../common/styles/vars/colors';
+@import '../SubjectControls/mixins';
+
+
+.container {
+ display: flex;
+ align-items: center;
+ flex-flow: row;
+ position: relative;
+ .button {
+ @include buttonBackground('historical-data');
+ pointer-events: none;
+ background-size: 90%;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ &.hasLabel {
+ background-color: $subject-control-btn-bg;
+ border: 0.06px solid $secondary-medium-light-gray;
+ max-width: 5.75rem;
+ max-height: 1.85rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+
+ [class*=spinner] {
+ position: relative;
+ top: -16%;
+ }
+
+ button {
+ min-height: 1.80rem;
+ height: 1.80rem;
+ min-width: 1.80rem;
+ width: 1.80rem;
+ }
+
+ span {
+ font-size: 0.65rem;
+ line-height: normal;
+ padding: .1rem .7rem;
+ margin: 0 -.2rem;
+ }
+ }
+
+}
diff --git a/src/SubjectMessagesPopover/index.js b/src/SubjectMessagesPopover/index.js
index 9c0ed4aa1..a86cf9880 100644
--- a/src/SubjectMessagesPopover/index.js
+++ b/src/SubjectMessagesPopover/index.js
@@ -1,6 +1,7 @@
import React, { memo, useMemo } from 'react';
import Popover from 'react-bootstrap/Popover';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import SubjectControlButton from '../SubjectControls/button';
import { usePermissions } from '../hooks';
import { PERMISSION_KEYS, PERMISSIONS } from '../constants';
@@ -14,10 +15,12 @@ import { ReactComponent as ChatIcon } from '../common/images/icons/chat-icon.svg
import styles from './styles.module.scss';
const SubjectMessagesPopover = (props) => {
- const { className = '', subject } = props;
+ const { className = '', subject, ...rest } = props;
const hasMessagingWritePermissions = usePermissions(PERMISSION_KEYS.MESSAGING, PERMISSIONS.CREATE);
+ const buttonClassName = `${className} ${styles.button}`;
+
const params = useMemo(() => {
return { subject_id: subject?.id };
}, [subject]);
@@ -29,15 +32,13 @@ const SubjectMessagesPopover = (props) => {
{subject.name}
-
+
{!!hasMessagingWritePermissions && }
;
return
-
-
-
+
;
};
diff --git a/src/SubjectMessagesPopover/styles.module.scss b/src/SubjectMessagesPopover/styles.module.scss
index 3e0b15bf7..2ebcd0ca4 100644
--- a/src/SubjectMessagesPopover/styles.module.scss
+++ b/src/SubjectMessagesPopover/styles.module.scss
@@ -1,4 +1,4 @@
-@import '../common/styles/vars/colors';
+@import '../SubjectControls/mixins';
.popover {
height: 22rem;
@@ -8,22 +8,10 @@
[class*=popover-body] {
height: 15.5rem;
}
+}
- h6 {
- svg {
- width: 2.5rem;
- height: 2.5rem;
- margin: -0.625rem;
- margin-right: -0.312rem;
- fill: $dark-gray;
-
- rect {
- fill: transparent;
- }
-
- path {
- fill: $dark-gray;
- }
- }
+.container {
+ .button {
+ @include buttonBackground('chat-icon');
}
-}
\ No newline at end of file
+}
diff --git a/src/SubjectPopup/index.js b/src/SubjectPopup/index.js
index a528932f4..142f93b1a 100644
--- a/src/SubjectPopup/index.js
+++ b/src/SubjectPopup/index.js
@@ -1,4 +1,4 @@
-import React, { lazy, memo, Fragment, useCallback, useMemo, useState } from 'react';
+import React, { memo, Fragment, useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import format from 'date-fns/format';
@@ -11,7 +11,6 @@ import TrackLength from '../TrackLength';
import SubjectControls from '../SubjectControls';
import AddReport from '../AddReport';
-import { addModal } from '../ducks/modals';
import { showPopup } from '../ducks/popup';
import { DEVELOPMENT_FEATURE_FLAGS } from '../constants';
@@ -23,11 +22,9 @@ import styles from './styles.module.scss';
const { ENABLE_UFA_NAVIGATION_UI } = DEVELOPMENT_FEATURE_FLAGS;
-const SubjectHistoricalDataModal = lazy(() => import('../SubjectHistoricalDataModal'));
-
const STORAGE_KEY = 'showSubjectDetailsByDefault';
-const SubjectPopup = ({ data, popoverPlacement, timeSliderState, addModal, showPopup }) => {
+const SubjectPopup = ({ data, popoverPlacement, timeSliderState, showPopup }) => {
const { geometry, properties } = data;
const { active: isTimeSliderActive } = timeSliderState;
@@ -39,8 +36,10 @@ const SubjectPopup = ({ data, popoverPlacement, timeSliderState, addModal, showP
const { tracks_available } = properties;
const coordProps = typeof properties.coordinateProperties === 'string' ? JSON.parse(properties.coordinateProperties) : properties.coordinateProperties;
+ const isStatic = subjectIsStatic(data);
+
const hasAdditionalDeviceProps = !!device_status_properties?.length;
- const additionalPropsShouldBeToggleable = hasAdditionalDeviceProps && device_status_properties.length > 2 && !subjectIsStatic(data);
+ const additionalPropsShouldBeToggleable = hasAdditionalDeviceProps && device_status_properties.length > 2 && !isStatic;
const [additionalPropsToggledOn, toggleAdditionalPropsVisibility] = useState(window.localStorage.getItem(STORAGE_KEY) === 'true' ? true : false);
const showAdditionalProps = hasAdditionalDeviceProps &&
@@ -61,10 +60,6 @@ const SubjectPopup = ({ data, popoverPlacement, timeSliderState, addModal, showP
showPopup('subject-messages', { geometry, properties: properties, coordinates: geometry.coordinates });
}, [geometry, properties, showPopup]);
- const onHistoricalDataClick = useCallback(() => {
- addModal({ title: 'Historical Data', content: SubjectHistoricalDataModal, subjectId: properties.id });
- }, [addModal, properties]);
-
const locationObject = {
longitude: geometry.coordinates[0],
latitude: geometry.coordinates[1],
@@ -78,7 +73,7 @@ const SubjectPopup = ({ data, popoverPlacement, timeSliderState, addModal, showP
{properties.default_status_value && <>
{properties.image &&
}
-
{!isTimeSliderActive ? properties.default_status_value : 'No historical data'}
+
{properties.default_status_value}
>}
{properties.name}
@@ -110,34 +105,27 @@ const SubjectPopup = ({ data, popoverPlacement, timeSliderState, addModal, showP
}
{tracks_available &&