diff --git a/public/locales/en-US/tracks.json b/public/locales/en-US/tracks.json
index 57edff7aa..d6e4147ca 100644
--- a/public/locales/en-US/tracks.json
+++ b/public/locales/en-US/tracks.json
@@ -1,7 +1,7 @@
{
"patrolTrackLegend": {
- "icon": "Icon for {{title}}",
- "itemDescription": "{{length}} covered",
+ "icon": "Icon for {{patrolTitle}}",
+ "itemTitle": "Patrol: {{patrolTitle}}",
"trackLegendItemsName": "patrols"
},
"subjectTrackLegend": {
diff --git a/public/locales/es/tracks.json b/public/locales/es/tracks.json
index 11919afa7..47f4368dc 100644
--- a/public/locales/es/tracks.json
+++ b/public/locales/es/tracks.json
@@ -1,7 +1,7 @@
{
"patrolTrackLegend": {
"icon": "Ícono para {{title}}",
- "itemDescription": "{{length}} cubiertos",
+ "itemTitle": "Patrulla: {{patrolTitle}}",
"trackLegendItemsName": "patrullas"
},
"subjectTrackLegend": {
diff --git a/public/locales/fr/tracks.json b/public/locales/fr/tracks.json
index 20304b5cc..5c0e3fed1 100644
--- a/public/locales/fr/tracks.json
+++ b/public/locales/fr/tracks.json
@@ -1,7 +1,7 @@
{
"patrolTrackLegend": {
"icon": "Icône pour {{title}}",
- "itemDescription": "{{length}} parcourue",
+ "itemTitle": "Patrouille : {{patrolTitle}}",
"trackLegendItemsName": "patrouilles"
},
"subjectTrackLegend": {
diff --git a/public/locales/ne-NP/tracks.json b/public/locales/ne-NP/tracks.json
index e1964f90b..59cb562b1 100644
--- a/public/locales/ne-NP/tracks.json
+++ b/public/locales/ne-NP/tracks.json
@@ -1,7 +1,7 @@
{
"patrolTrackLegend": {
"icon": "{{title}}को लागि आइकन",
- "itemDescription": "{{length}} ढाकिएको",
+ "itemTitle": "गस्ती: {{patrolTitle}}",
"trackLegendItemsName": "गस्तीहरु"
},
"subjectTrackLegend": {
diff --git a/public/locales/pt/tracks.json b/public/locales/pt/tracks.json
index 6131fd5f7..2e2b18267 100644
--- a/public/locales/pt/tracks.json
+++ b/public/locales/pt/tracks.json
@@ -1,7 +1,7 @@
{
"patrolTrackLegend": {
"icon": "Ícone para {{title}}",
- "itemDescription": "{{length}} percorridos",
+ "itemTitle": "Patrulha: {{patrolTitle}}",
"trackLegendItemsName": "patrulhas"
},
"subjectTrackLegend": {
diff --git a/public/locales/sw/tracks.json b/public/locales/sw/tracks.json
index 7b6b9bf60..c017e262c 100644
--- a/public/locales/sw/tracks.json
+++ b/public/locales/sw/tracks.json
@@ -1,7 +1,7 @@
{
"patrolTrackLegend": {
"icon": "Ikoni kwa {{title}}",
- "itemDescription": "{{length}} imefunikwa",
+ "itemTitle": "Doria: {{patrolTitle}}",
"trackLegendItemsName": "doria"
},
"subjectTrackLegend": {
diff --git a/src/MapLegend/styles.module.scss b/src/MapLegend/styles.module.scss
index f833a9d46..3078a8247 100644
--- a/src/MapLegend/styles.module.scss
+++ b/src/MapLegend/styles.module.scss
@@ -12,6 +12,7 @@
flex-wrap: wrap;
height: var(--legend-height);
line-height: normal;
+ margin-bottom: 0.5rem;
padding: 0.5rem;
position: relative;
diff --git a/src/PatrolFilter/index.js b/src/PatrolFilter/index.js
index 93e9ad428..83dd27d89 100644
--- a/src/PatrolFilter/index.js
+++ b/src/PatrolFilter/index.js
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
import { caseInsensitiveCompare } from '../utils/string';
import { INITIAL_FILTER_STATE, updatePatrolFilter } from '../ducks/patrol-filter';
import { resetGlobalDateRange } from '../ducks/global-date-range';
+import { selectPatrolsFeedMappedFromStore } from '../selectors/patrols';
import { isFilterModified } from '../utils/patrol-filter';
import { trackEventFactory, PATROL_FILTER_CATEGORY } from '../utils/analytics';
@@ -33,7 +34,7 @@ const PatrolFilter = ({ className }) => {
const containerRef = useRef(null);
const { t } = useTranslation('filters', { keyPrefix: 'patrolFilters' });
const dispatch = useDispatch();
- const patrolStore = useSelector((state) => state.data.patrolStore);
+ const patrolsFeedMappedFromStore = useSelector(selectPatrolsFeedMappedFromStore);
const patrolFilter = useSelector(state => state.data.patrolFilter);
const [filterText, setFilterText] = useState(patrolFilter.filter.text);
@@ -159,7 +160,7 @@ const PatrolFilter = ({ className }) => {
className={styles.friendlyFilterString}
dateRange={patrolFilter.filter.date_range}
isFiltered={isFilterModified(patrolFilter)}
- totalFeedCount={Object.keys(patrolStore).length ?? 0}
+ totalFeedCount={patrolsFeedMappedFromStore.length}
/>
{
diff --git a/src/PatrolFilter/index.test.js b/src/PatrolFilter/index.test.js
index 550c49389..d01ccf607 100644
--- a/src/PatrolFilter/index.test.js
+++ b/src/PatrolFilter/index.test.js
@@ -41,9 +41,7 @@ describe('PatrolFilter', () => {
},
status: INITIAL_FILTER_STATE.status,
},
- patrols: {
- results: [],
- },
+ patrolsFeed: [],
subjectStore: {},
},
};
diff --git a/src/PatrolTrackLegend/index.js b/src/PatrolTrackLegend/index.js
index 2b322ea1f..1d71ee53e 100644
--- a/src/PatrolTrackLegend/index.js
+++ b/src/PatrolTrackLegend/index.js
@@ -36,15 +36,18 @@ const PatrolTrackLegend = () => {
// Build the items array with the description, icon, id and title of each tracked patrol.
const items = useMemo(() => patrolsWithTrackData.map((patrolData) => {
const iconId = iconTypeForPatrol(patrolData.patrol);
- const title = displayTitleForPatrol(patrolData.patrol, patrolData.leader);
+ const patrolTitle = displayTitleForPatrol(patrolData.patrol, patrolData.leader);
return {
- description: t('itemDescription', {
- length: `${patrolData.trackData ? length(patrolData.trackData.track).toFixed(2): 0.00}km`,
- }),
- icon: ,
+ description: `${patrolData.trackData ? length(patrolData.trackData.track).toFixed(2): 0.00}km`,
+ icon: ,
id: patrolData.patrol.id,
- title,
+ title: t('itemTitle', { patrolTitle }),
};
}), [patrolsWithTrackData, t]);
diff --git a/src/SideBar/PatrolsFeedTab/index.js b/src/SideBar/PatrolsFeedTab/index.js
index 5438efc17..aedfd83cf 100644
--- a/src/SideBar/PatrolsFeedTab/index.js
+++ b/src/SideBar/PatrolsFeedTab/index.js
@@ -1,22 +1,24 @@
-import React, { memo, useCallback, useMemo } from 'react';
+import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import useNavigate from '../../hooks/useNavigate';
+import { selectPatrolsFeedMappedFromStore } from '../../selectors/patrols';
+import { sortPatrolList } from '../../utils/patrols';
import PatrolFilter from '../../PatrolFilter';
import PatrolList from '../../PatrolList';
-import { sortPatrolList } from '../../utils/patrols';
const PatrolsFeedTab = ({ loadingPatrolsFeed }) => {
const navigate= useNavigate();
- const patrolStore = useSelector((state) => state.data.patrolStore);
- const sortedPatrols = useMemo(() => sortPatrolList(Object.values(patrolStore)), [patrolStore]);
- const onItemClick = useCallback((id) => navigate(id), [navigate]);
+ const patrolsFeedMappedFromStore = useSelector(selectPatrolsFeedMappedFromStore);
+
+ const sortedPatrols = useMemo(() => sortPatrolList(patrolsFeedMappedFromStore), [patrolsFeedMappedFromStore]);
return <>
-
+
+ navigate(id)} patrols={sortedPatrols} />
>;
};
diff --git a/src/SideBar/PatrolsFeedTab/index.test.js b/src/SideBar/PatrolsFeedTab/index.test.js
index 5fd1e4e21..66fc89a84 100644
--- a/src/SideBar/PatrolsFeedTab/index.test.js
+++ b/src/SideBar/PatrolsFeedTab/index.test.js
@@ -21,7 +21,7 @@ const patrolFilter = { filter: {
let store = patrolDefaultStoreData;
store.data.patrolFilter = patrolFilter;
store.data.patrolStore = { [activePatrol.id]: activePatrol };
-store.data.patrols.results = [activePatrol.id];
+store.data.patrolsFeed = [activePatrol.id];
describe('PatrolsFeedTab', () => {
let navigate, useNavigateMock;
diff --git a/src/SideBar/useFetchPatrolsFeed/index.js b/src/SideBar/useFetchPatrolsFeed/index.js
index 9c65aba0c..dbacecfd1 100644
--- a/src/SideBar/useFetchPatrolsFeed/index.js
+++ b/src/SideBar/useFetchPatrolsFeed/index.js
@@ -1,8 +1,8 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { useDispatch, useSelector } from 'react-redux';
-import { fetchPatrols } from '../../ducks/patrols';
+import { fetchPatrolsFeed } from '../../ducks/patrols';
const useFetchPatrolsFeed = () => {
const dispatch = useDispatch();
@@ -20,18 +20,15 @@ const useFetchPatrolsFeed = () => {
return filterParams;
}, [patrolFilter]);
- const fetchAndLoadPatrolData = useCallback(() => {
- patrolFetchRef.current = dispatch(fetchPatrols());
+ useEffect(() => {
+ setLoadingPatrolsFeed(true);
+
+ patrolFetchRef.current = dispatch(fetchPatrolsFeed());
patrolFetchRef.current.request.finally(() => {
setLoadingPatrolsFeed(false);
patrolFetchRef.current = null;
});
- }, [dispatch]);
-
- useEffect(() => {
- setLoadingPatrolsFeed(true);
- fetchAndLoadPatrolData();
return () => {
const priorRequestCancelToken = patrolFetchRef?.current?.cancelToken;
@@ -40,7 +37,7 @@ const useFetchPatrolsFeed = () => {
priorRequestCancelToken.cancel();
}
};
- }, [fetchAndLoadPatrolData, patrolFilterParams]);
+ }, [dispatch, patrolFilterParams]);
return { loadingPatrolsFeed };
};
diff --git a/src/SideBar/useFetchPatrolsFeed/index.test.js b/src/SideBar/useFetchPatrolsFeed/index.test.js
index 55abda39c..948b3eb34 100644
--- a/src/SideBar/useFetchPatrolsFeed/index.test.js
+++ b/src/SideBar/useFetchPatrolsFeed/index.test.js
@@ -51,7 +51,7 @@ describe('useFetchPatrolsFeed', () => {
await waitFor(() => {
expect(actions).toHaveLength(2);
- expect(actions[0].type).toBe('FETCH_PATROLS_SUCCESS');
+ expect(actions[0].type).toBe('FETCH_PATROLS_FEED_SUCCESS');
expect(actions[1].type).toBe('UPDATE_PATROL_STORE');
});
});
diff --git a/src/__test-helpers/fixtures/patrols.js b/src/__test-helpers/fixtures/patrols.js
index 1b8eafb88..b369e4d0a 100644
--- a/src/__test-helpers/fixtures/patrols.js
+++ b/src/__test-helpers/fixtures/patrols.js
@@ -1672,9 +1672,7 @@ export const patrolDefaultStoreData = {
},
patrolStore: {},
subjectStore: {},
- patrols: {
- results: [],
- },
+ patrolsFeed: [],
patrolTypes: [{
display: 'Dog Patrol',
icon_id: 'dog-patrol-icon',
diff --git a/src/ducks/patrols.js b/src/ducks/patrols.js
index c6dd5e5e4..8a6fc1396 100644
--- a/src/ducks/patrols.js
+++ b/src/ducks/patrols.js
@@ -1,19 +1,16 @@
-import axios, { CancelToken, isCancel } from 'axios';
+import axios, { CancelToken } from 'axios';
import merge from 'lodash/merge';
import { API_URL } from '../constants';
-
-import { calcPatrolFilterForRequest/* ,
- validatePatrolAgainstCurrentPatrolFilter */ } from '../utils/patrol-filter';
+import { calcPatrolFilterForRequest } from '../utils/patrol-filter';
+import globallyResettableReducer from '../reducers/global-resettable';
export const PATROLS_API_URL = `${API_URL}activity/patrols/`;
-const FETCH_PATROLS_SUCCESS = 'FETCH_PATROLS_SUCCESS';
const UPDATE_PATROL_STORE = 'UPDATE_PATROL_STORE';
const FETCH_PATROLS_ERROR = 'FETCH_PATROLS_ERROR';
const CREATE_PATROL_SUCCESS = 'CREATE_PATROL_SUCCESS';
-// const CREATE_PATROL_ERROR = 'CREATE_PATROL_ERROR';
const UPDATE_PATROL_ERROR = 'UPDATE_PATROL_ERROR';
@@ -23,7 +20,6 @@ const UPLOAD_PATROL_FILES_START = 'UPLOAD_PATROL_FILES_START';
const UPLOAD_PATROL_FILES_SUCCESS = 'UPLOAD_PATROL_FILES_SUCCESS';
const UPLOAD_PATROL_FILES_ERROR = 'UPLOAD_PATROL_FILES_ERROR';
-
const CLEAR_PATROL_DATA = 'CLEAR_PATROL_DATA';
const REMOVE_PATROL_BY_ID = 'REMOVE_PATROL_BY_ID';
@@ -74,14 +70,6 @@ export const updatePatrolStore = (patrols) => ({
payload: patrols,
});
-export const fetchPatrolsSuccess = (patrols) => (dispatch) => {
- dispatch({
- type: FETCH_PATROLS_SUCCESS,
- payload: patrols,
- });
- dispatch(updatePatrolStore(patrols));
-};
-
export const socketDeletePatrol = (payload) => (dispatch) => {
const { patrol_id, matches_current_filter } = payload;
if (matches_current_filter) {
@@ -106,33 +94,6 @@ export const fetchPatrol = id => dispatch => axios.get(`${PATROLS_API_URL}${id}`
throw error;
});
-export const fetchPatrols = () => (dispatch) => {
- let cancelToken = CancelToken.source();
-
- const patrolFilterParamString = calcPatrolFilterForRequest({ params: { page_size: 200 } });
-
- const request = axios.get(`${PATROLS_API_URL}?${patrolFilterParamString}`, {
- cancelToken: cancelToken.token,
- })
- .then(({ data: { data: patrols } }) => {
- dispatch(fetchPatrolsSuccess(patrols));
- return patrols;
-
- })
- .catch((error) => {
- console.warn('error fetching patrols', error);
- dispatch({
- type: FETCH_PATROLS_ERROR,
- payload: error,
- });
- if (!isCancel(error)) {
- return new Error(error);
- }
- });
-
- return { request, cancelToken };
-};
-
export const createPatrol = (patrol) => (dispatch) => {
return axios.post(PATROLS_API_URL, patrol)
@@ -313,3 +274,49 @@ export const patrolTracksReducer = (state = INITIAL_PATROL_TRACKS_STATE, { type,
return state;
};
+
+// Actions
+const FETCH_PATROLS_FEED_SUCCESS = 'FETCH_PATROLS_FEED_SUCCESS';
+
+// Action creators
+export const fetchPatrolsFeed = () => (dispatch) => {
+ const cancelToken = CancelToken.source();
+
+ const request = axios.get(
+ `${PATROLS_API_URL}?${calcPatrolFilterForRequest({ params: { page_size: 200 } })}`,
+ { cancelToken: cancelToken.token }
+ )
+ .then((response) => {
+ dispatch({ payload: response.data.data.results.map((patrol) => patrol.id), type: FETCH_PATROLS_FEED_SUCCESS });
+ dispatch(updatePatrolStore(response.data.data));
+ })
+ .catch((error) => {
+ dispatch({ payload: error, type: FETCH_PATROLS_ERROR });
+
+ console.warn('error fetching patrols', error);
+ });
+
+ return { cancelToken, request };
+};
+
+// Reducer
+export const INITIAL_PATROLS_FEED_STATE = [];
+
+export const patrolsFeedReducer = globallyResettableReducer((state, action) => {
+ switch (action.type) {
+ case FETCH_PATROLS_FEED_SUCCESS:
+ return action.payload;
+
+ case CREATE_PATROL_REALTIME:
+ if (!state.includes(action.payload.id)) {
+ return [action.payload.id, ...state];
+ }
+ return state;
+
+ case REMOVE_PATROL_BY_ID:
+ return state.filter((patrolId) => patrolId !== action.payload);
+
+ default:
+ return state;
+ }
+}, INITIAL_PATROLS_FEED_STATE);
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 1f1155e9c..cff2918dc 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -9,7 +9,7 @@ import eventStoreReducer, { mapEventsReducer, eventFeedReducer, incidentFeedRedu
import eventCategoriesReducer from '../ducks/event-categories';
import eventTypesReducer from '../ducks/event-types';
import observationsReducer from '../ducks/observations';
-import { patrolStoreReducer, patrolTracksReducer } from '../ducks/patrols';
+import { patrolsFeedReducer, patrolStoreReducer, patrolTracksReducer } from '../ducks/patrols';
import patrolTypesReducer from '../ducks/patrol-types';
import patrolFilterReducer, { persistenceConfig as patrolFilterPersistenceConfig } from '../ducks/patrol-filter';
import mapsReducer, { homeMapReducer } from '../ducks/maps';
@@ -68,6 +68,7 @@ const rootReducer = combineReducers({
data: combineReducers({
baseLayers: baseLayersReducer,
eventStore: eventStoreReducer,
+ patrolsFeed: patrolsFeedReducer,
patrolStore: patrolStoreReducer,
feedEvents: eventFeedReducer,
feedIncidents: incidentFeedReducer,
diff --git a/src/selectors/patrols/index.js b/src/selectors/patrols/index.js
index 00d998006..e45830b8f 100644
--- a/src/selectors/patrols/index.js
+++ b/src/selectors/patrols/index.js
@@ -66,6 +66,7 @@ const buildPatrolData = (patrol, timeSliderState, tracks) => {
return patrolData;
};
+const selectPatrolsFeed = (state) => state.data.patrolsFeed;
const selectPatrolLeaderSchema = (state) => state.data.patrolLeaderSchema;
const selectPatrolStore = (state) => state.data.patrolStore;
const selectPatrolTrackState = (state) => state.view.patrolTrackState;
@@ -111,6 +112,12 @@ export const selectPatrolLeadersWithLastPosition = createSelector(
}) : null
);
+export const selectPatrolsFeedMappedFromStore = createSelector(
+ [selectPatrolsFeed, selectPatrolStore],
+ // Map each patrol id from the feed to its patrol object and filter just the defined ones.
+ (patrolsFeed, patrolStore) => patrolsFeed.map((patrolId) => patrolStore[patrolId]).filter((patrol) => !!patrol),
+);
+
export const selectPatrolsWithTracks = createSelector(
[selectPatrolStore, selectVisibleAndPinnedPatrolTracks],
(patrolStore, visibleAndPinnedPatrolTracks) => visibleAndPinnedPatrolTracks