diff --git a/app/pages/clinicworkspace/ConnectionIssues/ConnectionIssues.js b/app/pages/clinicworkspace/ConnectionIssues/ConnectionIssues.js
index 0c0efcf03d..89aff21d77 100644
--- a/app/pages/clinicworkspace/ConnectionIssues/ConnectionIssues.js
+++ b/app/pages/clinicworkspace/ConnectionIssues/ConnectionIssues.js
@@ -1,38 +1,38 @@
-import React, { useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { Trans } from 'react-i18next';
+import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+import { Trans, useTranslation } from 'react-i18next';
import { colors as vizColors } from '@tidepool/viz';
import Table from '../../../components/elements/Table';
import { Flex, Text } from 'theme-ui';
+import AppliedFiltersList from './filters/AppliedFiltersList';
import FilterByCategory from './filters/FilterByCategory';
+import FilterByTags from './filters/FilterByTags';
+import FilterBySites from './filters/FilterBySites';
import PaginationController from './PaginationController';
+import EmptyContentNode from './EmptyNodeContent';
import { resetConnectionIssuesState } from './connectionIssuesSlice';
-import { useGetConnectionIssuesPatientsQuery } from './connectionIssuesApi';
import useTableColumns from './useTableColumns';
+import useConnectionIssuesPatients from './useConnectionIssuesPatients';
-const LIMIT = 12;
+const tableContainerProps = { sx: { containerType: 'inline-size' } };
const ConnectionIssues = () => {
+ const { t } = useTranslation();
const dispatch = useDispatch();
- const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
- const category = useSelector(state => state.blip.connectionIssues.category);
- const offset = useSelector(state => state.blip.connectionIssues.offset);
+ const { data } = useConnectionIssuesPatients();
const columns = useTableColumns();
- const { data } = useGetConnectionIssuesPatientsQuery(
- { clinicId: selectedClinicId, offset, category, limit: LIMIT },
- { skip: !selectedClinicId }
- );
-
// reset state on dismount
useEffect(() => {
return () => dispatch(resetConnectionIssuesState());
}, []);
+ const emptyContentNode = useMemo(() => , []);
+
if (!data) return null;
const patients = data?.data || [];
@@ -49,17 +49,25 @@ const ConnectionIssues = () => {
+
+ {t('Filter By')}
+
+
+
+
+
diff --git a/app/pages/clinicworkspace/ConnectionIssues/EmptyNodeContent.js b/app/pages/clinicworkspace/ConnectionIssues/EmptyNodeContent.js
new file mode 100644
index 0000000000..3f652cc15c
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/EmptyNodeContent.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import colorPalette from '../../../themes/colorPalette';
+import { Flex, Text } from 'theme-ui';
+import { resetConnectionIssuesFilters } from './connectionIssuesFiltersSlice';
+import { setOffset } from './connectionIssuesSlice';
+import ClearFilterButtons, { PATIENT_QUERY_STATE } from '../components/ClearFilterButtons';
+import noop from 'lodash/noop';
+import without from 'lodash/without';
+
+const EmptyContentNode = () => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+
+ const { patientTags, clinicSites } = useSelector(state => state.blip.connectionIssuesFilters);
+
+ const activeFiltersCount = without([
+ patientTags?.length,
+ clinicSites?.length,
+ ], null, 0, undefined).length;
+
+ const hasActiveFilters = activeFiltersCount > 0;
+
+ const handleResetFilters = () => {
+ dispatch(resetConnectionIssuesFilters());
+ dispatch(setOffset(0));
+ };
+
+ const emptyContentCopy = hasActiveFilters
+ ? t('There are no patients with the current filter(s)')
+ : t('There are no results to show');
+
+ const patientQueryState = hasActiveFilters ? PATIENT_QUERY_STATE.FILTER_ONLY : PATIENT_QUERY_STATE.NONE;
+
+ return (
+
+
+ {emptyContentCopy}
+
+
+ { hasActiveFilters &&
+
+ }
+
+ );
+};
+
+export default EmptyContentNode;
diff --git a/app/pages/clinicworkspace/ConnectionIssues/PaginationController.js b/app/pages/clinicworkspace/ConnectionIssues/PaginationController.js
index 18255d8169..0fff8b8fff 100644
--- a/app/pages/clinicworkspace/ConnectionIssues/PaginationController.js
+++ b/app/pages/clinicworkspace/ConnectionIssues/PaginationController.js
@@ -6,7 +6,7 @@ import { Grid, Flex } from 'theme-ui';
import { colors as vizColors } from '@tidepool/viz';
import { setOffset } from './connectionIssuesSlice';
-import { LIMIT } from './connectionIssuesApi';
+import { LIMIT } from './useConnectionIssuesPatients';
const PaginationController = ({ total }) => {
const dispatch = useDispatch();
diff --git a/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesApi.js b/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesApi.js
index 1b76ea9aad..2e3965ef8d 100644
--- a/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesApi.js
+++ b/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesApi.js
@@ -25,25 +25,49 @@ const getConnectionIssuesParam = (category) => {
}
};
-export const LIMIT = 12;
+export const buildGetConnectionIssuesPatientsParams = (offset, limit, category, tags = [], sites = []) => {
+ const deviceIssues = getConnectionIssuesParam(category);
+ const omitHiddenDevicesIssues = category !== CATEGORY.HIDDEN;
+
+ const formattedTags = tags?.length > 0 ? tags.join(',') : undefined;
+ const formattedSites = sites?.length > 0 ? sites.join(',') : undefined;
+
+ return {
+ offset,
+ limit,
+ deviceIssues,
+ omitHiddenDevicesIssues,
+ tags: formattedTags,
+ sites: formattedSites,
+ };
+};
+
+export const tagTypes = {
+ CONNECTION_ISSUES_PATIENTS: 'CONNECTION_ISSUES_PATIENTS',
+};
+
+const { CONNECTION_ISSUES_PATIENTS } = tagTypes;
+
+RTKQueryApi.enhanceEndpoints({
+ addTagTypes: [CONNECTION_ISSUES_PATIENTS],
+});
const connectionIssuesApi = RTKQueryApi.injectEndpoints({
endpoints: (builder) => ({
getConnectionIssuesPatients: builder.query({
- query: ({ clinicId, offset, category, limit }) => {
- const deviceIssues = getConnectionIssuesParam(category);
- const omitHiddenDevicesIssues = category !== CATEGORY.HIDDEN;
+ query: ({ clinicId, offset, category, limit, tags, sites }) => {
+ const params = buildGetConnectionIssuesPatientsParams(offset, limit, category, tags, sites);
return {
url: `/clinics/${clinicId}/patients`,
- params: {
- offset,
- limit,
- deviceIssues,
- omitHiddenDevicesIssues,
- },
+ params,
};
},
+ transformResponse: (response, _meta, arg) => ({
+ ...response,
+ category: arg.category,
+ }),
+ providesTags: [CONNECTION_ISSUES_PATIENTS],
}),
}),
});
diff --git a/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesFiltersSlice.js b/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesFiltersSlice.js
new file mode 100644
index 0000000000..a59cce52ba
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/connectionIssuesFiltersSlice.js
@@ -0,0 +1,36 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { SELECT_CLINIC_SUCCESS } from '../../../redux/constants/actionTypes';
+
+const getInitialState = () => ({
+ patientTags: [],
+ clinicSites: [],
+});
+
+const connectionIssuesFiltersSlice = createSlice({
+ name: 'connectionIssuesFilters',
+ initialState: getInitialState(),
+ reducers: {
+ setConnectionIssuesFilters: (_state, action) => action.payload || getInitialState(),
+ setPatientTagsFilter: (state, action) => {
+ state.patientTags = action.payload;
+ },
+ setClinicSitesFilter: (state, action) => {
+ state.clinicSites = action.payload;
+ },
+ resetConnectionIssuesFilters: () => getInitialState(),
+ },
+ extraReducers: (builder) => {
+ builder.addCase(SELECT_CLINIC_SUCCESS, (_state, action) => {
+ return action.payload?.connectionIssuesFilters || getInitialState();
+ });
+ },
+});
+
+export const {
+ setConnectionIssuesFilters,
+ setClinicSitesFilter,
+ setPatientTagsFilter,
+ resetConnectionIssuesFilters,
+} = connectionIssuesFiltersSlice.actions;
+
+export default connectionIssuesFiltersSlice.reducer;
diff --git a/app/pages/clinicworkspace/ConnectionIssues/filters/AppliedFiltersList.js b/app/pages/clinicworkspace/ConnectionIssues/filters/AppliedFiltersList.js
new file mode 100644
index 0000000000..854359bf02
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/filters/AppliedFiltersList.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
+import without from 'lodash/without';
+import noop from 'lodash/noop';
+
+import ActiveFiltersTray from '../../components/filters/ActiveFiltersTray';
+import ClearFilterButtons, { PATIENT_QUERY_STATE } from '../../components/ClearFilterButtons';
+import { Box } from 'theme-ui';
+import { setClinicSitesFilter, setPatientTagsFilter } from '../connectionIssuesFiltersSlice';
+import { setOffset } from '../connectionIssuesSlice';
+
+export const getPatientQueryState = (patientTags, clinicSites) => {
+ const hasFiltersActive = clinicSites?.length > 0 || patientTags?.length > 0;
+
+ if (hasFiltersActive) return PATIENT_QUERY_STATE.FILTER_ONLY;
+
+ return PATIENT_QUERY_STATE.NONE;
+};
+
+const AppliedFiltersList = ({ patientCount = 0 }) => {
+ const dispatch = useDispatch();
+
+ const { clinicSites, patientTags } = useSelector(state => state.blip.connectionIssuesFilters);
+
+ const activeFilters = { clinicSites, patientTags };
+
+ const handleResetFilters = () => {
+ dispatch(setPatientTagsFilter([]));
+ dispatch(setClinicSitesFilter([]));
+ dispatch(setOffset(0));
+ };
+
+ const handleRemoveFilter = (filterKey, value) => {
+ switch (filterKey) {
+ case 'patientTags': {
+ const updatedTags = without(patientTags, value);
+ dispatch(setPatientTagsFilter(updatedTags));
+ dispatch(setOffset(0));
+ break;
+ }
+
+ case 'clinicSites': {
+ const updatedSites = without(clinicSites, value);
+ dispatch(setClinicSitesFilter(updatedSites));
+ dispatch(setOffset(0));
+ break;
+ }
+ }
+ };
+
+ const patientQueryState = getPatientQueryState(patientTags, clinicSites);
+
+ return (
+
+
+
+ }
+ />
+ );
+};
+
+AppliedFiltersList.propTypes = {
+ patientCount: PropTypes.number,
+};
+
+export default AppliedFiltersList;
diff --git a/app/pages/clinicworkspace/ConnectionIssues/filters/FilterBySites.js b/app/pages/clinicworkspace/ConnectionIssues/filters/FilterBySites.js
new file mode 100644
index 0000000000..4506bc192e
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/filters/FilterBySites.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setOffset } from '../connectionIssuesSlice';
+import { setClinicSitesFilter } from '../connectionIssuesFiltersSlice';
+import SiteFilterDropdown from '../../components/filters/SiteFilterDropdown';
+
+const FilterBySites = () => {
+ const dispatch = useDispatch();
+ const { clinicSites } = useSelector(state => state.blip.connectionIssuesFilters);
+
+ const handleChange = (clinicSites) => {
+ dispatch(setClinicSitesFilter(clinicSites));
+ dispatch(setOffset(0));
+ };
+
+ return ;
+};
+
+export default FilterBySites;
diff --git a/app/pages/clinicworkspace/ConnectionIssues/filters/FilterByTags.js b/app/pages/clinicworkspace/ConnectionIssues/filters/FilterByTags.js
new file mode 100644
index 0000000000..849e576e6c
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/filters/FilterByTags.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setOffset } from '../connectionIssuesSlice';
+import { setPatientTagsFilter } from '../connectionIssuesFiltersSlice';
+import TagFilterDropdown from '../../components/filters/TagFilterDropdown';
+
+const FilterByTags = () => {
+ const dispatch = useDispatch();
+ const { patientTags } = useSelector(state => state.blip.connectionIssuesFilters);
+
+ const handleChange = (tags) => {
+ dispatch(setPatientTagsFilter(tags));
+ dispatch(setOffset(0));
+ };
+
+ return ;
+};
+
+export default FilterByTags;
diff --git a/app/pages/clinicworkspace/ConnectionIssues/useConnectionIssuesPatients.js b/app/pages/clinicworkspace/ConnectionIssues/useConnectionIssuesPatients.js
new file mode 100644
index 0000000000..3e3a74edb3
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/useConnectionIssuesPatients.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { useGetConnectionIssuesPatientsQuery } from './connectionIssuesApi';
+
+export const LIMIT = 12;
+
+const useConnectionIssuesPatients = () => {
+ const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
+ const category = useSelector(state => state.blip.connectionIssues.category);
+ const offset = useSelector(state => state.blip.connectionIssues.offset);
+ const patientTags = useSelector(state => state.blip.connectionIssuesFilters.patientTags);
+ const clinicSites = useSelector(state => state.blip.connectionIssuesFilters.clinicSites);
+
+ return useGetConnectionIssuesPatientsQuery(
+ { clinicId: selectedClinicId, offset, category, limit: LIMIT, tags: patientTags, sites: clinicSites },
+ { skip: !selectedClinicId }
+ );
+};
+
+export default useConnectionIssuesPatients;
diff --git a/app/pages/clinicworkspace/ConnectionIssues/usePruneInvalidFilters.js b/app/pages/clinicworkspace/ConnectionIssues/usePruneInvalidFilters.js
new file mode 100644
index 0000000000..a1dc839d40
--- /dev/null
+++ b/app/pages/clinicworkspace/ConnectionIssues/usePruneInvalidFilters.js
@@ -0,0 +1,43 @@
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setPatientTagsFilter, setClinicSitesFilter } from './connectionIssuesFiltersSlice';
+import { SPECIAL_FILTER_STATES } from '../useClinicPatientsFilters';
+import keyBy from 'lodash/keyBy';
+import isEqual from 'lodash/isEqual';
+
+const usePruneInvalidFilters = () => {
+ const dispatch = useDispatch();
+ const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
+ const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
+ const { patientTags, clinicSites } = useSelector(state => state.blip.connectionIssuesFilters);
+
+ const clinicId = clinic?.id;
+
+ useEffect(() => {
+ if (!patientTags?.length || !clinicId) return;
+
+ if (isEqual(patientTags, SPECIAL_FILTER_STATES.ZERO_TAGS)) return;
+
+ const availableTags = keyBy(clinic.patientTags || [], 'id');
+ const prunedTags = patientTags.filter(tagId => !!availableTags[tagId]);
+
+ if (prunedTags.length < patientTags.length) {
+ dispatch(setPatientTagsFilter(prunedTags));
+ }
+ }, [clinicId]);
+
+ useEffect(() => {
+ if (!clinicSites?.length || !clinicId) return;
+
+ if (isEqual(clinicSites, SPECIAL_FILTER_STATES.ZERO_SITES)) return;
+
+ const availableSites = keyBy(clinic.sites || [], 'id');
+ const prunedSites = clinicSites.filter(siteId => !!availableSites[siteId]);
+
+ if (prunedSites.length < clinicSites.length) {
+ dispatch(setClinicSitesFilter(prunedSites));
+ }
+ }, [clinicId]);
+};
+
+export default usePruneInvalidFilters;
diff --git a/app/redux/actions/async.js b/app/redux/actions/async.js
index 30de11200c..0015819e04 100644
--- a/app/redux/actions/async.js
+++ b/app/redux/actions/async.js
@@ -18,7 +18,7 @@ import utils from '../../core/utils';
import { clinicUIDetails } from '../../core/clinicUtils.js';
import { getDismissedAltRangeBannerKey, isRangeWithNonStandardTarget } from '../../providers/AppBanner/appBannerHelpers.js';
import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils.js';
-import { getTideDashboardFiltersKey, loadLocalState } from '../store/localStorage';
+import { getTideDashboardFiltersKey, getConnectionIssuesFiltersKey, loadLocalState } from '../store/localStorage';
// Exported as a mutable reference to allow location to be swapped in tests
export const _win = { location: window.location };
@@ -3158,7 +3158,11 @@ export function selectClinic(api, clinicId) {
const tideDashboardFiltersKey = getTideDashboardFiltersKey(loggedInUserId, clinicId);
const tideDashboardFilters = loggedInUserId && clinicId ? loadLocalState(tideDashboardFiltersKey) : undefined;
- dispatch(sync.selectClinicSuccess(clinicId, tideDashboardFilters));
+
+ const connectionIssuesFiltersKey = getConnectionIssuesFiltersKey(loggedInUserId, clinicId);
+ const connectionIssuesFilters = loggedInUserId && clinicId ? loadLocalState(connectionIssuesFiltersKey) : undefined;
+
+ dispatch(sync.selectClinicSuccess(clinicId, tideDashboardFilters, connectionIssuesFilters));
const clinic = clinics[clinicId];
diff --git a/app/redux/actions/sync.js b/app/redux/actions/sync.js
index 9577a9134f..4b00660144 100644
--- a/app/redux/actions/sync.js
+++ b/app/redux/actions/sync.js
@@ -2114,12 +2114,13 @@ export function getClinicsForClinicianFailure(error, apiError) {
};
}
-export function selectClinicSuccess(clinicId, tideDashboardFilters) {
+export function selectClinicSuccess(clinicId, tideDashboardFilters, connectionIssuesFilters) {
return {
type: ActionTypes.SELECT_CLINIC_SUCCESS,
payload: {
clinicId,
tideDashboardFilters,
+ connectionIssuesFilters,
},
};
}
diff --git a/app/redux/reducers/index.js b/app/redux/reducers/index.js
index fc0b4bd26d..95f0d63109 100644
--- a/app/redux/reducers/index.js
+++ b/app/redux/reducers/index.js
@@ -23,6 +23,7 @@ import patientListFilters from './patientListFilters';
import tideDashboard from '../../pages/clinicworkspace/TideDashboardV2/tideDashboardSlice';
import tideDashboardFilters from '../../pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice';
import connectionIssues from '../../pages/clinicworkspace/ConnectionIssues/connectionIssuesSlice';
+import connectionIssuesFilters from '../../pages/clinicworkspace/ConnectionIssues/connectionIssuesFiltersSlice';
import {
bgPrefs,
@@ -115,4 +116,5 @@ export default combineReducers({
tideDashboard,
tideDashboardFilters,
connectionIssues,
+ connectionIssuesFilters,
});
diff --git a/app/redux/store/configureStore.dev.js b/app/redux/store/configureStore.dev.js
index d537c9fd72..f3620e6382 100644
--- a/app/redux/store/configureStore.dev.js
+++ b/app/redux/store/configureStore.dev.js
@@ -31,7 +31,7 @@ import { stringify, parse } from 'qs';
import blipState from '../reducers/initialState';
import reducers from '../reducers';
-import { getTideDashboardFiltersKey, loadLocalState, saveLocalState } from './localStorage';
+import { getTideDashboardFiltersKey, getConnectionIssuesFiltersKey, loadLocalState, saveLocalState } from './localStorage';
import createErrorLogger from '../utils/logErrorMiddleware';
import trackingMiddleware from '../utils/trackingMiddleware';
@@ -117,8 +117,10 @@ function _createStore(api) {
if (loggedInUserId && selectedClinicId) {
const tideDashboardFiltersKey = getTideDashboardFiltersKey(loggedInUserId, selectedClinicId);
+ const connectionIssuesFiltersKey = getConnectionIssuesFiltersKey(loggedInUserId, selectedClinicId);
saveLocalState(store.getState().blip?.tideDashboardFilters, tideDashboardFiltersKey);
+ saveLocalState(store.getState().blip?.connectionIssuesFilters, connectionIssuesFiltersKey);
}
}, 1000));
diff --git a/app/redux/store/configureStore.prod.js b/app/redux/store/configureStore.prod.js
index 57b5eb44f5..ca2068bf29 100644
--- a/app/redux/store/configureStore.prod.js
+++ b/app/redux/store/configureStore.prod.js
@@ -26,7 +26,7 @@ import throttle from 'lodash/throttle';
import blipState from '../reducers/initialState';
import reducers from '../reducers';
-import { getTideDashboardFiltersKey, loadLocalState, saveLocalState } from './localStorage';
+import { getTideDashboardFiltersKey, getConnectionIssuesFiltersKey, loadLocalState, saveLocalState } from './localStorage';
import createErrorLogger from '../utils/logErrorMiddleware';
import trackingMiddleware from '../utils/trackingMiddleware';
@@ -73,8 +73,10 @@ function _createStore(api) {
if (loggedInUserId && selectedClinicId) {
const tideDashboardFiltersKey = getTideDashboardFiltersKey(loggedInUserId, selectedClinicId);
+ const connectionIssuesFiltersKey = getConnectionIssuesFiltersKey(loggedInUserId, selectedClinicId);
saveLocalState(store.getState().blip?.tideDashboardFilters, tideDashboardFiltersKey);
+ saveLocalState(store.getState().blip?.connectionIssuesFilters, connectionIssuesFiltersKey);
}
}, 1000));
diff --git a/app/redux/store/localStorage.js b/app/redux/store/localStorage.js
index ecc790139c..e09e9325fc 100644
--- a/app/redux/store/localStorage.js
+++ b/app/redux/store/localStorage.js
@@ -23,3 +23,7 @@ export const saveLocalState = (state, key = 'blipState') => {
export const getTideDashboardFiltersKey = (userId, clinicId) => {
return `tideDashboardFilters/${userId}/${clinicId}`;
};
+
+export const getConnectionIssuesFiltersKey = (userId, clinicId) => {
+ return `connectionIssuesFilters/${userId}/${clinicId}`;
+};