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