From e548826d55ac203d7b4dfd6a3acd32ba3ced1568 Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Thu, 14 Dec 2023 18:52:37 +0530 Subject: [PATCH] (fix) Fixed the default login location coming in the every term search --- .../location-picker.component.tsx | 70 +++---- .../location-picker.resource.ts | 194 +++++++++++++++++- .../apps/esm-login-app/src/login.resource.ts | 125 +---------- 3 files changed, 216 insertions(+), 173 deletions(-) diff --git a/packages/apps/esm-login-app/src/location-picker/location-picker.component.tsx b/packages/apps/esm-login-app/src/location-picker/location-picker.component.tsx index 0ad73b33a..f9f505335 100644 --- a/packages/apps/esm-login-app/src/location-picker/location-picker.component.tsx +++ b/packages/apps/esm-login-app/src/location-picker/location-picker.component.tsx @@ -10,32 +10,36 @@ import { RadioButtonGroup, RadioButtonSkeleton, } from '@carbon/react'; -import { navigate, setSessionLocation, useConfig, useConnectivity, useSession } from '@openmrs/esm-framework'; +import { + navigate, + setSessionLocation, + useConfig, + useConnectivity, + useDebounce, + useSession, +} from '@openmrs/esm-framework'; import type { LoginReferrer } from '../login/login.component'; -import { useLoginLocations } from '../login.resource'; import styles from './location-picker.scss'; -import { useDefaultLocation } from './location-picker.resource'; +import { useDefaultLocation, useInfiniteScrolling, useLoginLocations } from './location-picker.resource'; import { ConfigSchema } from '../config-schema'; -interface LocationPickerProps { - hideWelcomeMessage?: boolean; - currentLocationUuid?: string; -} +interface LocationPickerProps {} -const LocationPicker: React.FC = ({ hideWelcomeMessage, currentLocationUuid }) => { +const LocationPicker: React.FC = () => { const { t } = useTranslation(); const config = useConfig(); const { chooseLocation } = config; const isLoginEnabled = useConnectivity(); + const [searchTerm, setSearchTerm] = useState(null); + const debouncedSearchQuery = useDebounce(searchTerm); const [searchParams] = useSearchParams(); + const isUpdateFlow = useMemo(() => searchParams.get('update') === 'true', [searchParams]); const { userDefaultLocationUuid, updateDefaultLocation, savePreference, setSavePreference, defaultLocationFhir } = - useDefaultLocation(isUpdateFlow); - - const [searchTerm, setSearchTerm] = useState(null); + useDefaultLocation(isUpdateFlow, debouncedSearchQuery); const { user, sessionLocation } = useSession(); - const { currentUser, userProperties } = useMemo( + const { currentUser } = useMemo( () => ({ currentUser: user?.display, userUuid: user?.uuid, @@ -50,12 +54,13 @@ const LocationPicker: React.FC = ({ hideWelcomeMessage, cur hasMore, loadingNewData, setPage, - } = useLoginLocations(chooseLocation.useLoginLocationTag, chooseLocation.locationsPerRequest, searchTerm); + } = useLoginLocations(chooseLocation.useLoginLocationTag, chooseLocation.locationsPerRequest, debouncedSearchQuery); const locations = useMemo(() => { if (!defaultLocationFhir?.length || !fetchedLocations) { return fetchedLocations; } + return [ ...(defaultLocationFhir ?? []), ...fetchedLocations?.filter(({ resource }) => resource.id !== defaultLocationFhir?.[0].resource.id), @@ -63,9 +68,6 @@ const LocationPicker: React.FC = ({ hideWelcomeMessage, cur }, [defaultLocationFhir, fetchedLocations]); const [activeLocation, setActiveLocation] = useState(() => { - if (currentLocationUuid && hideWelcomeMessage) { - return currentLocationUuid; - } return sessionLocation?.uuid ?? userDefaultLocationUuid; }); @@ -124,9 +126,8 @@ const LocationPicker: React.FC = ({ hideWelcomeMessage, cur } }, [changeLocation, isSubmitting, userDefaultLocationUuid, isUpdateFlow]); - const search = (location: string) => { - setActiveLocation(''); - setSearchTerm(location); + const handleSearch = (event) => { + setSearchTerm(event.target.value); }; const handleSubmit = useCallback( @@ -140,27 +141,15 @@ const LocationPicker: React.FC = ({ hideWelcomeMessage, cur [activeLocation, changeLocation, savePreference], ); - // Infinite scroll - const observer = useRef(null); - const loadingIconRef = useCallback( - (node: HTMLDivElement) => { - if (loadingNewData) return; - if (observer.current) observer.current.disconnect(); - observer.current = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore) { - setPage((page) => page + 1); - } - }, - { - threshold: 1, - }, - ); - if (node) observer.current.observe(node); - }, - [loadingNewData, hasMore, setPage], - ); + const handleFetchNextSet = useCallback(() => { + setPage((page) => page + 1); + }, [setPage]); + const { observer, loadingIconRef } = useInfiniteScrolling({ + inLoadingState: loadingNewData, + onIntersection: handleFetchNextSet, + shouldLoadMore: hasMore, + }); const reloadIndex = hasMore ? Math.floor(locations.length * 0.5) : -1; return ( @@ -183,9 +172,10 @@ const LocationPicker: React.FC = ({ hideWelcomeMessage, cur labelText={t('searchForLocation', 'Search for a location')} id="search-1" placeholder={t('searchForLocation', 'Search for a location')} - onChange={(event) => search(event.target.value)} + onChange={handleSearch} name="searchForLocation" size="lg" + value={searchTerm} />
{isLoading ? ( diff --git a/packages/apps/esm-login-app/src/location-picker/location-picker.resource.ts b/packages/apps/esm-login-app/src/location-picker/location-picker.resource.ts index c4e1f6ae2..e22b1397e 100644 --- a/packages/apps/esm-login-app/src/location-picker/location-picker.resource.ts +++ b/packages/apps/esm-login-app/src/location-picker/location-picker.resource.ts @@ -1,12 +1,153 @@ -import { setUserProperties, showToast, useSession } from '@openmrs/esm-framework'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useValidateLocationUuid } from '../login.resource'; +import useSwrInfinite from 'swr/infinite'; +import useSwrImmutable from 'swr/immutable'; +import { + FetchResponse, + fhirBaseUrl, + openmrsFetch, + setUserProperties, + showNotification, + showToast, + useSession, +} from '@openmrs/esm-framework'; +import { LocationEntry, LocationResponse } from '../types'; -export function useDefaultLocation(isUpdateFlow: boolean) { +interface LoginLocationData { + locations: Array; + isLoading: boolean; + totalResults: number; + hasMore: boolean; + loadingNewData: boolean; + setPage: (size: number | ((_size: number) => number)) => Promise[]>; +} + +export function useLoginLocations( + useLoginLocationTag: boolean, + count: number = 0, + searchQuery: string = '', +): LoginLocationData { + const { t } = useTranslation(); + function constructUrl(page: number, prevPageData: FetchResponse) { + if (prevPageData) { + const nextLink = prevPageData.data?.link?.find((link) => link.relation === 'next'); + + if (!nextLink) { + return null; + } + + const nextUrl = new URL(nextLink.url); + // default for production + if (nextUrl.origin === window.location.origin) { + return nextLink.url; + } + + // in development, the request should be funnelled through the local proxy + return new URL( + `${nextUrl.pathname}${nextUrl.search ? `?${nextUrl.search}` : ''}`, + window.location.origin, + ).toString(); + } + + let url = `${fhirBaseUrl}/Location?`; + let urlSearchParameters = new URLSearchParams(); + urlSearchParameters.append('_summary', 'data'); + + if (count) { + urlSearchParameters.append('_count', '' + count); + } + + if (page) { + urlSearchParameters.append('_getpagesoffset', '' + page * count); + } + + if (useLoginLocationTag) { + urlSearchParameters.append('_tag', 'Login Location'); + } + + if (typeof searchQuery === 'string' && searchQuery != '') { + urlSearchParameters.append('name:contains', searchQuery); + } + + return url + urlSearchParameters.toString(); + } + + const { data, isLoading, isValidating, setSize, error } = useSwrInfinite, Error>( + constructUrl, + openmrsFetch, + ); + + if (error) { + showNotification({ + title: t('errorLoadingLoginLocations', 'Error loading login locations'), + kind: 'error', + critical: true, + description: error?.message, + }); + } + + const memoizedLocations = useMemo(() => { + return { + locations: data?.length ? data?.flatMap((entries) => entries?.data?.entry ?? []) : null, + isLoading, + totalResults: data?.[0]?.data?.total ?? null, + hasMore: data?.length ? data?.[data.length - 1]?.data?.link?.some((link) => link.relation === 'next') : false, + loadingNewData: isValidating, + setPage: setSize, + }; + }, [isLoading, data, isValidating, setSize]); + + return memoizedLocations; +} + +/** + * The hook is created to validate a locationUuid and also allow searching with a search term + * for the same location so that the searching by name is validated from the backend itself, + * which is similar to the hook `useLoginLocations`. + * The result from this hook and `useLoginLocations` are merged and hence searching is + * required as well in this hook + */ +export function useValidateLocationUuid(locationUuid: string, searchTerm?: string) { + const [isLocationValid, setIsLocationValid] = useState(false); + let urlSearchParameters = new URLSearchParams(); + urlSearchParameters.append('_id', locationUuid); + if (searchTerm) { + urlSearchParameters.append('name:contains', searchTerm); + } + const url = locationUuid ? `/ws/fhir2/R4/Location?${urlSearchParameters.toString()}` : null; + const { data, error, isLoading } = useSwrImmutable>(url, openmrsFetch, { + shouldRetryOnError(err) { + if (err?.response?.status) { + return err.response.status >= 500; + } + return false; + }, + }); + + useEffect(() => { + // We can only validate a locationUuid if there is no search term filtering the result any more. + if (!searchTerm) { + setIsLocationValid(data?.ok && data?.data?.total > 0); + } + }, [data?.data?.total, data?.ok, searchTerm]); + + const results = useMemo( + () => ({ + isLocationValid, + location: data?.data?.entry ?? [], + error, + isLoading, + }), + [isLocationValid, data?.data?.entry, error, isLoading], + ); + return results; +} + +export function useDefaultLocation(isUpdateFlow: boolean, searchTerm: string) { const { t } = useTranslation(); const [savePreference, setSavePreference] = useState(false); const { user } = useSession(); + const { userUuid, userProperties } = useMemo( () => ({ userUuid: user?.uuid, @@ -15,11 +156,12 @@ export function useDefaultLocation(isUpdateFlow: boolean) { [user], ); - const userDefaultLocationUuid = useMemo(() => { - return userProperties?.defaultLocation; - }, [userProperties?.defaultLocation]); + const userDefaultLocationUuid = useMemo(() => userProperties?.defaultLocation, [userProperties?.defaultLocation]); - const { isLocationValid, defaultLocation: defaultLocationFhir } = useValidateLocationUuid(userDefaultLocationUuid); + const { isLocationValid, location: defaultFhirLocation } = useValidateLocationUuid( + userDefaultLocationUuid, + searchTerm, + ); useEffect(() => { if (userDefaultLocationUuid) { @@ -76,10 +218,44 @@ export function useDefaultLocation(isUpdateFlow: boolean) { ); return { - defaultLocationFhir, + defaultFhirLocation, userDefaultLocationUuid: isLocationValid ? userDefaultLocationUuid : null, updateDefaultLocation, savePreference, setSavePreference, }; } + +export function useInfiniteScrolling({ + inLoadingState, + shouldLoadMore, + onIntersection, +}: { + inLoadingState: boolean; + shouldLoadMore: boolean; + onIntersection: () => void; +}) { + const observer = useRef(null); + const loadingIconRef = useCallback( + (node: HTMLDivElement) => { + if (inLoadingState) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && shouldLoadMore) { + onIntersection(); + } + }, + { + threshold: 1, + }, + ); + if (node) observer.current.observe(node); + }, + [inLoadingState, shouldLoadMore, onIntersection], + ); + return { + observer, + loadingIconRef, + }; +} diff --git a/packages/apps/esm-login-app/src/login.resource.ts b/packages/apps/esm-login-app/src/login.resource.ts index 65106e7bb..faff0e797 100644 --- a/packages/apps/esm-login-app/src/login.resource.ts +++ b/packages/apps/esm-login-app/src/login.resource.ts @@ -1,17 +1,4 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import useSwrInfinite from 'swr/infinite'; -import useSwrImmutable from 'swr/immutable'; -import { - FetchResponse, - fhirBaseUrl, - openmrsFetch, - refetchCurrentUser, - Session, - showNotification, - useDebounce, -} from '@openmrs/esm-framework'; -import { LocationEntry, LocationResponse } from './types'; +import { openmrsFetch, refetchCurrentUser, Session } from '@openmrs/esm-framework'; export async function performLogin(username: string, password: string): Promise<{ data: Session }> { const abortController = new AbortController(); @@ -28,113 +15,3 @@ export async function performLogin(username: string, password: string): Promise< return res; }); } - -interface LoginLocationData { - locations: Array; - isLoading: boolean; - totalResults: number; - hasMore: boolean; - loadingNewData: boolean; - setPage: (size: number | ((_size: number) => number)) => Promise[]>; -} - -export function useLoginLocations( - useLoginLocationTag: boolean, - count: number = 0, - searchQuery: string = '', -): LoginLocationData { - const { t } = useTranslation(); - const debouncedSearchQuery = useDebounce(searchQuery); - function constructUrl(page: number, prevPageData: FetchResponse) { - if (prevPageData) { - const nextLink = prevPageData.data?.link?.find((link) => link.relation === 'next'); - - if (!nextLink) { - return null; - } - - const nextUrl = new URL(nextLink.url); - // default for production - if (nextUrl.origin === window.location.origin) { - return nextLink.url; - } - - // in development, the request should be funnelled through the local proxy - return new URL( - `${nextUrl.pathname}${nextUrl.search ? `?${nextUrl.search}` : ''}`, - window.location.origin, - ).toString(); - } - - let url = `${fhirBaseUrl}/Location?`; - let urlSearchParameters = new URLSearchParams(); - urlSearchParameters.append('_summary', 'data'); - - if (count) { - urlSearchParameters.append('_count', '' + count); - } - - if (page) { - urlSearchParameters.append('_getpagesoffset', '' + page * count); - } - - if (useLoginLocationTag) { - urlSearchParameters.append('_tag', 'Login Location'); - } - - if (typeof debouncedSearchQuery === 'string' && debouncedSearchQuery != '') { - urlSearchParameters.append('name:contains', debouncedSearchQuery); - } - - return url + urlSearchParameters.toString(); - } - - const { data, isLoading, isValidating, setSize, error } = useSwrInfinite, Error>( - constructUrl, - openmrsFetch, - ); - - if (error) { - showNotification({ - title: t('errorLoadingLoginLocations', 'Error loading login locations'), - kind: 'error', - critical: true, - description: error?.message, - }); - } - - const memoizedLocations = useMemo(() => { - return { - locations: data?.length ? data?.flatMap((entries) => entries?.data?.entry ?? []) : null, - isLoading, - totalResults: data?.[0]?.data?.total ?? null, - hasMore: data?.length ? data?.[data.length - 1]?.data?.link?.some((link) => link.relation === 'next') : false, - loadingNewData: isValidating, - setPage: setSize, - }; - }, [isLoading, data, isValidating, setSize]); - - return memoizedLocations; -} - -export function useValidateLocationUuid(locationUuid: string) { - const url = locationUuid ? `/ws/fhir2/R4/Location?_id=${locationUuid}` : null; - const { data, error, isLoading } = useSwrImmutable>(url, openmrsFetch, { - shouldRetryOnError(err) { - if (err?.response?.status) { - return err.response.status >= 500; - } - return false; - }, - }); - const results = useMemo( - () => ({ - isLocationValid: data?.ok && data?.data?.total > 0, - defaultLocation: data?.data?.entry ?? [], - error, - isLoading, - }), - [data, isLoading, error], - ); - return results; -}