diff --git a/src/Common/hooks/useFilters.tsx b/src/Common/hooks/useFilters.tsx index 4229ae8ab02..1c62a10d9d0 100644 --- a/src/Common/hooks/useFilters.tsx +++ b/src/Common/hooks/useFilters.tsx @@ -5,14 +5,14 @@ import GenericFilterBadge from "../../CAREUI/display/FilterBadge"; import PaginationComponent from "../../Components/Common/Pagination"; import useConfig from "./useConfig"; import { classNames } from "../../Utils/utils"; +import FiltersCache from "../../Utils/FiltersCache"; export type FilterState = Record; -export type FilterParamKeys = string | string[]; interface FilterBadgeProps { name: string; value?: string; - paramKey: FilterParamKeys; + paramKey: string | string[]; } /** @@ -32,18 +32,18 @@ export default function useFilters({ const [showFilters, setShowFilters] = useState(false); const [qParams, _setQueryParams] = useQueryParams(); + const updateCache = (query: QueryParam) => { + const blacklist = FILTERS_CACHE_BLACKLIST.concat(cacheBlacklist); + FiltersCache.set(query, blacklist); + }; + const setQueryParams = ( query: QueryParam, options?: setQueryParamsOptions ) => { - const updatedQParams = { ...query }; - - for (const param of cacheBlacklist) { - delete updatedQParams[param]; - } - + query = FiltersCache.utils.clean(query); _setQueryParams(query, options); - updateFiltersCache(updatedQParams); + updateCache(query); }; const updateQuery = (filter: FilterState) => { @@ -61,15 +61,22 @@ export default function useFilters({ const removeFilter = (param: string) => removeFilters([param]); useEffect(() => { - const cache = getFiltersCache(); const qParamKeys = Object.keys(qParams); - const canSkip = Object.keys(cache).every( - (key) => qParamKeys.includes(key) && qParams[key] === cache[key] - ); - if (canSkip) return; - if (Object.keys(cache).length) { - setQueryParams(cache); + + // If we navigate to a path that has query params set on mount, + // skip restoring the cache, instead update the cache with new filters. + if (qParamKeys.length) { + updateCache(qParams); + return; } + + const cache = FiltersCache.get(); + if (!cache) { + return; + } + + // Restore cache + setQueryParams(cache); }, []); const FilterBadge = ({ name, value, paramKey }: FilterBadgeProps) => { @@ -99,7 +106,7 @@ export default function useFilters({ }; const badgeUtils = { - badge(name: string, paramKey: FilterParamKeys) { + badge(name: string, paramKey: FilterBadgeProps["paramKey"]) { return { name, paramKey }; }, ordering(name = "Sort by", paramKey = "ordering") { @@ -109,7 +116,7 @@ export default function useFilters({ value: qParams[paramKey] && t("SortOptions." + qParams[paramKey]), }; }, - value(name: string, paramKey: FilterParamKeys, value: string) { + value(name: string, paramKey: FilterBadgeProps["paramKey"], value: string) { return { name, value, paramKey }; }, phoneNumber(name = "Phone Number", paramKey = "phone_number") { @@ -278,15 +285,3 @@ const removeFromQuery = (query: Record, params: string[]) => { }; const FILTERS_CACHE_BLACKLIST = ["page", "limit", "offset"]; - -const getFiltersCacheKey = () => `filters--${window.location.pathname}`; -const getFiltersCache = () => { - return JSON.parse(localStorage.getItem(getFiltersCacheKey()) || "{}"); -}; -const updateFiltersCache = (cache: Record) => { - const result = { ...cache }; - for (const param of FILTERS_CACHE_BLACKLIST) { - delete result[param]; - } - localStorage.setItem(getFiltersCacheKey(), JSON.stringify(result)); -}; diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index d1cd94dbab5..8dcae9bafc0 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -11,8 +11,8 @@ import useConfig from "../../Common/hooks/useConfig"; import CircularProgress from "../Common/components/CircularProgress"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; -import { invalidateFiltersCache } from "../../Utils/utils"; import { useAuthContext } from "../../Common/hooks/useAuthUser"; +import FiltersCache from "../../Utils/FiltersCache"; export const Login = (props: { forgot?: boolean }) => { const { signIn } = useAuthContext(); @@ -93,7 +93,7 @@ export const Login = (props: { forgot?: boolean }) => { const handleSubmit = async (e: any) => { e.preventDefault(); setLoading(true); - invalidateFiltersCache(); + FiltersCache.invaldiateAll(); const validated = validateData(); if (!validated) { setLoading(false); diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index 17f67a7def1..b207189d27d 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -16,7 +16,7 @@ interface FacilitySelectProps { showAll?: boolean; showNOptions?: number; freeText?: boolean; - selected: FacilityModel | FacilityModel[] | null; + selected?: FacilityModel | FacilityModel[] | null; setSelected: (selected: FacilityModel | FacilityModel[] | null) => void; } diff --git a/src/Components/Facility/FacilityFilter/DistrictSelect.tsx b/src/Components/Facility/FacilityFilter/DistrictSelect.tsx index 088048ba6dd..ef77d50221e 100644 --- a/src/Components/Facility/FacilityFilter/DistrictSelect.tsx +++ b/src/Components/Facility/FacilityFilter/DistrictSelect.tsx @@ -8,7 +8,7 @@ interface DistrictSelectProps { errors: string; className?: string; multiple?: boolean; - selected: string; + selected?: string; setSelected: (selected: string) => void; } diff --git a/src/Components/Patient/PatientFilter.tsx b/src/Components/Patient/PatientFilter.tsx index 0e1cdfdd083..6392c5c4d45 100644 --- a/src/Components/Patient/PatientFilter.tsx +++ b/src/Components/Patient/PatientFilter.tsx @@ -1,5 +1,4 @@ import dayjs from "dayjs"; -import { useCallback, useEffect } from "react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import { @@ -12,12 +11,6 @@ import { } from "../../Common/constants"; import useConfig from "../../Common/hooks/useConfig"; import useMergeState from "../../Common/hooks/useMergeState"; -import { - getAllLocalBody, - getAnyFacility, - getDistrict, -} from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { dateQueryString } from "../../Utils/utils"; import { DateRange } from "../Common/DateRangeInputV2"; import { FacilitySelect } from "../Common/FacilitySelect"; @@ -35,6 +28,9 @@ import { import MultiSelectMenuV2 from "../Form/MultiSelectMenuV2"; import SelectMenuV2 from "../Form/SelectMenuV2"; import DiagnosesFilter, { FILTER_BY_DIAGNOSES_KEYS } from "./DiagnosesFilter"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); @@ -105,37 +101,24 @@ export default function PatientFilter(props: any) { diagnoses_unconfirmed: filter.diagnoses_unconfirmed || null, diagnoses_differential: filter.diagnoses_differential || null, }); - const dispatch: any = useDispatch(); - - useEffect(() => { - async function fetchData() { - if (filter.facility) { - const { data: facilityData } = await dispatch( - getAnyFacility(filter.facility, "facility") - ); - setFilterState({ facility_ref: facilityData }); - } - if (filter.district) { - const { data: districtData } = await dispatch( - getDistrict(filter.district, "district") - ); - setFilterState({ district_ref: districtData }); - } + useQuery(routes.getAnyFacility, { + pathParams: { id: filter.facility }, + prefetch: !!filter.facility, + onResponse: ({ data }) => setFilterState({ facility_ref: data }), + }); - if (filter.lsgBody) { - const { data: lsgRes } = await dispatch(getAllLocalBody({})); - const lsgBodyData = lsgRes.results; + useQuery(routes.getDistrict, { + pathParams: { id: filter.district }, + prefetch: !!filter.district, + onResponse: ({ data }) => setFilterState({ district_ref: data }), + }); - setFilterState({ - lsgBody_ref: lsgBodyData.filter( - (obj: any) => obj.id.toString() === filter.lsgBody.toString() - )[0], - }); - } - } - fetchData(); - }, [dispatch]); + useQuery(routes.getLocalBody, { + pathParams: { id: filter.lsgBody }, + prefetch: !!filter.lsgBody, + onResponse: ({ data }) => setFilterState({ lsgBody_ref: data }), + }); const VACCINATED_FILTER = [ { id: "0", text: "Unvaccinated" }, @@ -161,21 +144,19 @@ export default function PatientFilter(props: any) { { id: "false", text: "No" }, ]; - const setFacility = (selected: any, name: string) => { - const filterData: any = { ...filterState }; - filterData[`${name}_ref`] = selected; - filterData[name] = (selected || {}).id; - - setFilterState(filterData); + const setFilterWithRef = (name: string, selected?: any) => { + setFilterState({ + [`${name}_ref`]: selected, + [name]: selected?.id, + }); }; - const lsgSearch = useCallback( - async (search: string) => { - const res = await dispatch(getAllLocalBody({ local_body_name: search })); - return res?.data?.results; - }, - [dispatch] - ); + const lsgSearch = async (search: string) => { + const { data } = await request(routes.getAllLocalBody, { + query: { local_body_name: search }, + }); + return data?.results; + }; const applyFilter = () => { const { @@ -585,7 +566,7 @@ export default function PatientFilter(props: any) { name="facility" showAll={false} selected={filterState.facility_ref} - setSelected={(obj) => setFacility(obj, "facility")} + setSelected={(obj) => setFilterWithRef("facility", obj)} /> {filterState.facility && ( @@ -629,13 +610,7 @@ export default function PatientFilter(props: any) { name="lsg_body" selected={filterState.lsgBody_ref} fetchData={lsgSearch} - onChange={(selected) => - setFilterState({ - ...filterState, - lsgBody_ref: selected, - lsgBody: selected.id, - }) - } + onChange={(obj) => setFilterWithRef("lsgBody", obj)} optionLabel={(option) => option.name} compareBy="id" /> @@ -648,7 +623,7 @@ export default function PatientFilter(props: any) { multiple={false} name="district" selected={filterState.district_ref} - setSelected={(obj: any) => setFacility(obj, "district")} + setSelected={(obj) => setFilterWithRef("district", obj)} errors={""} /> diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 8350ca8bd29..8e3ae382547 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -132,9 +132,6 @@ export const getWardByLocalBody = (pathParam: object) => { export const getLocalBody = (pathParam: object) => { return fireRequest("getLocalBody", [], {}, pathParam); }; -export const getAllLocalBody = (params: object) => { - return fireRequest("getAllLocalBody", [], params); -}; // Sample Test export const getSampleTestList = (params: object, pathParam: object) => { diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index de30ed7b567..6425ff8eab0 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -761,6 +761,8 @@ const routes = { }, getAllLocalBody: { path: "/api/v1/local_body/", + method: "GET", + TRes: Type>(), }, getLocalbodyByName: { path: "/api/v1/local_body/", diff --git a/src/Utils/FiltersCache.tsx b/src/Utils/FiltersCache.tsx new file mode 100644 index 00000000000..e69fb14da58 --- /dev/null +++ b/src/Utils/FiltersCache.tsx @@ -0,0 +1,77 @@ +type Filters = Record; + +/** + * @returns The filters cache key associated to the current window URL + */ +const getKey = () => { + return `filters--${window.location.pathname}`; +}; + +/** + * Returns a sanitized filter object that ignores filters with no value or + * filters that are part of the blacklist. + * + * @param filters Input filters to be sanitized + * @param blacklist Optional array of filter keys that are to be ignored. + */ +const clean = (filters: Filters, blacklist?: string[]) => { + const reducer = (cleaned: Filters, key: string) => { + const valueAllowed = (filters[key] ?? "") != ""; + if (valueAllowed && !blacklist?.includes(key)) { + cleaned[key] = filters[key]; + } + return cleaned; + }; + + return Object.keys(filters).reduce(reducer, {}); +}; + +/** + * Retrieves the cached filters + */ +const get = (key?: string) => { + const content = localStorage.getItem(key ?? getKey()); + return content ? (JSON.parse(content) as Filters) : null; +}; + +/** + * Sets the filters cache with the specified filters. + */ +const set = (filters: Filters, blacklist?: string[], key?: string) => { + key ??= getKey(); + filters = clean(filters, blacklist); + + if (Object.keys(filters).length) { + localStorage.setItem(key, JSON.stringify(filters)); + } else { + invalidate(key); + } +}; + +/** + * Removes the filters cache for the specified key or current URL. + */ +const invalidate = (key?: string) => { + localStorage.removeItem(key ?? getKey()); +}; + +/** + * Removes all filters cache in the platform. + */ +const invaldiateAll = () => { + for (const key in localStorage) { + if (key.startsWith("filters--")) { + invalidate(key); + } + } +}; + +export default { + get, + set, + invalidate, + invaldiateAll, + utils: { + clean, + }, +}; diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 2966790fb10..b890672a6c1 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -449,14 +449,6 @@ export const showUserDelete = (authUser: UserModel, targetUser: UserModel) => { return false; }; -export const invalidateFiltersCache = () => { - for (const key in localStorage) { - if (key.startsWith("filters--")) { - localStorage.removeItem(key); - } - } -}; - export const compareBy = (key: keyof T) => { return (a: T, b: T) => { return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0;