diff --git a/packages/apps/esm-login-app/src/choose-location/choose-location.component.test.tsx b/packages/apps/esm-login-app/src/choose-location/choose-location.component.test.tsx deleted file mode 100644 index c027b33ba..000000000 --- a/packages/apps/esm-login-app/src/choose-location/choose-location.component.test.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import "@testing-library/jest-dom"; -import { waitFor } from "@testing-library/react"; -import { navigate, useConfig } from "@openmrs/esm-framework"; -import { queryLocations } from "./choose-location.resource"; -import { mockConfig } from "../../__mocks__/config.mock"; -import ChooseLocation from "./choose-location.component"; -import renderWithRouter from "../test-helpers/render-with-router"; - -const navigateMock = navigate as jest.Mock; -const mockedQueryLocations = queryLocations as jest.Mock; -const mockedUseConfig = useConfig as jest.Mock; - -jest.mock("../CurrentUserContext", () => ({ - useCurrentUser() { - return { - display: "Demo", - }; - }, -})); - -jest.mock("./choose-location.resource.ts", () => ({ - queryLocations: jest.fn().mockResolvedValue([ - { - resource: { - id: "abc", - name: "foo", - }, - }, - ]), -})); - -describe(``, () => { - beforeEach(() => { - mockedUseConfig.mockReturnValue(mockConfig); - }); - - afterEach(() => { - navigateMock.mockClear(); - }); - - it(`should redirect back to referring page on successful login when there is only one location`, async () => { - const locationMock = { - state: { - referrer: "/home/patient-search", - }, - }; - - renderWithRouter(ChooseLocation, { - location: locationMock, - isLoginEnabled: true, - }); - - await waitFor(() => - expect(navigate).toHaveBeenCalledWith({ - to: "${openmrsSpaBase}" + locationMock.state.referrer, - }) - ); - }); - - it(`should set location and skip location select page if there is exactly one location`, async () => { - renderWithRouter(ChooseLocation, { isLoginEnabled: true }); - - await waitFor(() => - expect(navigate).toHaveBeenCalledWith({ to: "${openmrsSpaBase}/home" }) - ); - }); - - it(`should set location and skip location select page if there is no location`, async () => { - mockedQueryLocations.mockResolvedValueOnce([]); - - renderWithRouter(ChooseLocation, { isLoginEnabled: true }); - - await waitFor(() => - expect(navigate).toHaveBeenCalledWith({ to: "${openmrsSpaBase}/home" }) - ); - }); - - it(`should show the location picker when multiple locations exist`, async () => { - mockedQueryLocations.mockResolvedValueOnce([ - { - resource: { - id: "abc", - name: "foo", - }, - }, - { - resource: { - id: "def", - name: "ghi", - }, - }, - ]); - - renderWithRouter(ChooseLocation, { isLoginEnabled: true }); - - await waitFor(() => expect(navigate).not.toHaveBeenCalled()); - }); - - it(`should not show the location picker when disabled`, async () => { - mockedUseConfig.mockReturnValue({ - ...mockConfig, - chooseLocation: { - enabled: false, - }, - }); - - mockedQueryLocations.mockResolvedValueOnce([ - { - resource: { - id: "abc", - name: "foo", - }, - }, - { - resource: { - id: "def", - name: "ghi", - }, - }, - ]); - - renderWithRouter(ChooseLocation, { isLoginEnabled: true }); - - await waitFor(() => - expect(navigate).toHaveBeenCalledWith({ to: "${openmrsSpaBase}/home" }) - ); - }); - - it(`should redirect to custom path if configured`, async () => { - mockedUseConfig.mockReturnValue({ - ...mockConfig, - links: { - loginSuccess: "${openmrsSpaBase}/foo", - }, - }); - - renderWithRouter(ChooseLocation, { isLoginEnabled: true }); - - await waitFor(() => - expect(navigate).toHaveBeenCalledWith({ to: "${openmrsSpaBase}/foo" }) - ); - }); - - it(`should redirect back to returnUrl when provided`, async () => { - const locationMock = { - search: "?returnToUrl=/openmrs/spa/home", - }; - - renderWithRouter(ChooseLocation, { - location: locationMock, - isLoginEnabled: true, - }); - - await waitFor(() => - expect(navigate).toHaveBeenCalledWith({ - to: "/openmrs/spa/home", - }) - ); - }); -}); diff --git a/packages/apps/esm-login-app/src/choose-location/choose-location.component.tsx b/packages/apps/esm-login-app/src/choose-location/choose-location.component.tsx index cee90afcd..bd666a805 100644 --- a/packages/apps/esm-login-app/src/choose-location/choose-location.component.tsx +++ b/packages/apps/esm-login-app/src/choose-location/choose-location.component.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import LoadingIcon from "../loading/loading.component"; import LocationPicker from "../location-picker/location-picker.component"; import { RouteComponentProps } from "react-router-dom"; @@ -7,9 +7,8 @@ import { useConfig, setSessionLocation, } from "@openmrs/esm-framework"; -import { queryLocations } from "./choose-location.resource"; +import { useLoginLocations } from "./choose-location.resource"; import { useCurrentUser } from "../CurrentUserContext"; -import { LocationEntry } from "../types"; import type { StaticContext } from "react-router"; export interface LoginReferrer { @@ -29,9 +28,9 @@ export const ChooseLocation: React.FC = ({ const referrer = location?.state?.referrer; const config = useConfig(); const user = useCurrentUser(); - const [loginLocations, setLoginLocations] = - useState>(null); - const [isLoading, setIsLoading] = useState(true); + const { locationData, isLoading } = useLoginLocations( + config.chooseLocation.useLoginLocationTag + ); const changeLocation = useCallback( (locationUuid?: string) => { @@ -54,32 +53,28 @@ export const ChooseLocation: React.FC = ({ ); useEffect(() => { - if (isLoginEnabled) { - const ac = new AbortController(); - queryLocations("", ac, config.chooseLocation.useLoginLocationTag).then( - (locations) => setLoginLocations(locations) - ); - return () => ac.abort(); - } - }, [isLoginEnabled]); - - useEffect(() => { - if (loginLocations) { - if (!config.chooseLocation.enabled || loginLocations.length < 2) { - changeLocation(loginLocations[0]?.resource.id); - } else { - setIsLoading(false); + if (!isLoading) { + if (!config.chooseLocation.enabled || locationData.length === 1) { + changeLocation(locationData[0]?.resource.id); + } + if (!isLoading && !locationData.length) { + changeLocation(); } } - }, [loginLocations, user, changeLocation, config.chooseLocation.enabled]); + }, [ + locationData, + user, + changeLocation, + config.chooseLocation.enabled, + isLoading, + ]); if (!isLoading || !isLoginEnabled) { return ( ); diff --git a/packages/apps/esm-login-app/src/choose-location/choose-location.resource.ts b/packages/apps/esm-login-app/src/choose-location/choose-location.resource.ts index a25a73269..0084b1dc3 100644 --- a/packages/apps/esm-login-app/src/choose-location/choose-location.resource.ts +++ b/packages/apps/esm-login-app/src/choose-location/choose-location.resource.ts @@ -1,40 +1,85 @@ +import { useEffect, useMemo } from "react"; import { openmrsFetch, - openmrsObservableFetch, fhirBaseUrl, + FetchResponse, + showNotification, } from "@openmrs/esm-framework"; -import { map } from "rxjs/operators"; -import { LocationResponse } from "../types"; +import { LocationEntry, LocationResponse } from "../types"; +import useSwrInfinite from "swr/infinite"; -export function getLoginLocations(): Observable { - return openmrsObservableFetch( - `/ws/rest/v1/location?tag=Login%20Location&v=custom:(uuid,display)` - ).pipe(map(({ data }) => data["results"])); -} +const fhirLocationUrl = `${fhirBaseUrl}/Location?_summary=data`; -export function searchLocationsFhir( - location: string, - abortController: AbortController, - useLoginLocationTag: boolean -) { - const baseUrl = `${fhirBaseUrl}/Location?name=${location}`; - const url = useLoginLocationTag - ? baseUrl.concat("&_tag=login location") - : baseUrl; - return openmrsFetch(url, { - method: "GET", - signal: abortController.signal, - }); -} +export function useLoginLocations( + useLoginLocationTag: boolean, + count: number = 0, + searchQuery: string = "" +): { + locationData: Array; + isLoading: boolean; + totalResults: number; + hasMore: boolean; + loadingNewData: boolean; + setPage: ( + size: number | ((_size: number) => number) + ) => Promise[]>; +} { + const getUrl = (page, prevPageData: FetchResponse) => { + if ( + prevPageData && + !prevPageData?.data?.link?.some((link) => link.relation === "next") + ) + return null; + let url = fhirLocationUrl; + if (count) { + url += `&_count=${count}`; + } + if (page) { + url += `&_getpagesoffset=${page * count}`; + } + if (useLoginLocationTag) { + url += "&_tag=login location"; + } + if (typeof searchQuery === "string" && searchQuery != "") { + url += `&name=${searchQuery}`; + } + return url; + }; + + const { data, isValidating, size, setSize, error } = useSwrInfinite< + FetchResponse, + Error + >(getUrl, openmrsFetch); + + if (error) { + console.error(error.message); + showNotification({ + title: error.name, + description: error.message, + kind: "error", + }); + } + + useEffect(() => { + setSize(1); + }, [searchQuery, setSize]); + + const returnValue = useMemo(() => { + return { + locationData: data + ? [].concat(...data?.map((resp) => resp?.data?.entry)) + : null, + isLoading: !data, + 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, + }; + }, [data, isValidating, setSize]); -export function queryLocations( - location: string, - abortController = new AbortController(), - useLoginLocationTag: boolean -) { - return searchLocationsFhir( - location, - abortController, - useLoginLocationTag - ).then((locs) => locs.data.entry); + return returnValue; } diff --git a/packages/apps/esm-login-app/src/config-schema.ts b/packages/apps/esm-login-app/src/config-schema.ts index f4618d771..1aa1b02b4 100644 --- a/packages/apps/esm-login-app/src/config-schema.ts +++ b/packages/apps/esm-login-app/src/config-schema.ts @@ -36,6 +36,12 @@ export const configSchema = { _default: 8, _description: "The number of locations displayed on location picker", }, + locationsPerRequest: { + _type: Type.Number, + _default: 50, + _description: + "The number of results to be fetched in each cycle of the infinite scroll", + }, useLoginLocationTag: { _type: Type.Boolean, _default: true, diff --git a/packages/apps/esm-login-app/src/location-picker/location-picker.component.scss b/packages/apps/esm-login-app/src/location-picker/location-picker.component.scss index cc0fadefd..4b4ad4854 100644 --- a/packages/apps/esm-login-app/src/location-picker/location-picker.component.scss +++ b/packages/apps/esm-login-app/src/location-picker/location-picker.component.scss @@ -9,7 +9,7 @@ width: 100vw; } -.location-card { +.locationCard { display: flex; flex-direction: column; width: 23rem; @@ -37,8 +37,8 @@ .searchResults { @extend .bodyShort02; margin: 1rem 0rem; - height: 26.125rem; max-height: 100%; + overflow-y: auto; } .resultsCount { @@ -52,10 +52,6 @@ margin-top: 0.5rem; } -:global(.omrs-breakpoint-gt-tablet) .locationResultsContainer { - height: 23rem; -} - .locationRadioButton { display: flex; justify-content: flex-start; @@ -75,3 +71,38 @@ .confirmButton button { width: 20rem; } + +.radioButtonSkeleton { + margin-right: 0 !important; + margin-bottom: $spacing-05; +} + +.radioButtonSkeleton span { + width: 100% !important; +} + +.pagination { + background-color: $ui-01; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $spacing-05; + border: 2px solid $ui-03; +} + +.pagination > button:first-child, +pagination > button:first-child:hover { + border-right: 2px solid $ui-03; +} + +.pagination > button, +.pagination > button:hover { + border-left: 2px solid $ui-03; +} + +.loadingIcon { + display: flex; + justify-content: center; + align-items: center; + margin-top: $spacing-05; +} diff --git a/packages/apps/esm-login-app/src/location-picker/location-picker.component.test.tsx b/packages/apps/esm-login-app/src/location-picker/location-picker.component.test.tsx deleted file mode 100644 index 18059a404..000000000 --- a/packages/apps/esm-login-app/src/location-picker/location-picker.component.test.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import "@testing-library/jest-dom"; -import React from "react"; -import LocationPicker from "./location-picker.component"; -import { act } from "react-dom/test-utils"; -import { fireEvent, render, waitFor, screen } from "@testing-library/react"; -import { useConfig } from "@openmrs/esm-framework"; -import { mockConfig } from "../../__mocks__/config.mock"; - -const loginLocations = { - data: { - entry: [ - { resource: { id: "111", name: "Earth" } }, - { resource: { id: "222", name: "Mars" } }, - ], - }, -}; - -const mockedUseConfig = useConfig as jest.Mock; - -jest.mock("lodash-es/debounce", () => jest.fn((fn) => fn)); - -describe(``, () => { - let searchInput, - marsInput, - submitButton, - locationEntries, - onChangeLocation, - searchLocations; - - beforeEach(async () => { - Object.defineProperty(window, "localStorage", { - value: { - getItem: jest.fn(() => "111"), - setItem: jest.fn(), - }, - writable: true, - }); - - mockedUseConfig.mockReturnValue(mockConfig); - - // reset mocks - locationEntries = loginLocations.data.entry; - onChangeLocation = jest.fn(() => {}); - searchLocations = jest.fn(() => Promise.resolve([])); - - // prepare components - render( - - ); - - searchInput = screen.getByRole("searchbox"); - submitButton = screen.getByText("Confirm", { selector: "button" }); - }); - - it("trigger search on typing", async () => { - searchLocations = jest.fn(() => Promise.resolve(loginLocations)); - - render( - - ); - - fireEvent.change(searchInput, { target: { value: "mars" } }); - - await waitFor(() => { - expect(screen.getByLabelText("Mars")).not.toBeNull(); - }); - }); - - it(`disables/enables the submit button when input is invalid/valid`, async () => { - act(() => { - fireEvent.change(searchInput, { target: { value: "Mars" } }); - }); - - await waitFor(() => { - expect(screen.queryByText("Mars")).not.toBeNull(); - marsInput = screen.getByLabelText("Mars"); - }); - - act(() => { - fireEvent.click(marsInput); - }); - - await waitFor(() => { - expect(submitButton).not.toHaveAttribute("disabled"); - }); - }); - - it(`makes an API request when you submit the form`, async () => { - expect(onChangeLocation).not.toHaveBeenCalled(); - - act(() => { - fireEvent.change(searchInput, { target: { value: "Mars" } }); - }); - - await waitFor(() => { - expect(screen.queryByText("Mars")).not.toBeNull(); - marsInput = screen.getByLabelText("Mars"); - }); - - fireEvent.click(marsInput); - fireEvent.click(submitButton); - - await waitFor(() => expect(onChangeLocation).toHaveBeenCalled()); - }); - - it(`send the user to the home page on submit`, async () => { - expect(onChangeLocation).not.toHaveBeenCalled(); - - act(() => { - fireEvent.change(searchInput, { target: { value: "Mars" } }); - }); - - await waitFor(() => { - expect(screen.queryByText("Mars")).not.toBeNull(); - marsInput = screen.getByLabelText("Mars"); - }); - - fireEvent.click(marsInput); - fireEvent.click(submitButton); - - await waitFor(() => { - expect(onChangeLocation).toHaveBeenCalled(); - }); - }); - - it(`send the user to the redirect page on submit`, async () => { - expect(onChangeLocation).not.toHaveBeenCalled(); - - act(() => { - fireEvent.change(searchInput, { target: { value: "Mars" } }); - }); - - await waitFor(() => { - expect(screen.queryByText("Mars")).not.toBeNull(); - marsInput = screen.getByLabelText("Mars"); - }); - - submitButton = screen.getByText("Confirm", { selector: "button" }); - - fireEvent.click(marsInput); - fireEvent.click(submitButton); - - await waitFor(() => { - expect(onChangeLocation).toHaveBeenCalled(); - }); - }); - - it("search term input should have autofocus on render", async () => { - expect(searchInput).toEqual(document.activeElement); - }); - - it("should deselect active location when user searches for a location", async () => { - const locationRadioButton: HTMLElement = await screen.getByRole("radio", { - name: /Earth/, - }); - fireEvent.click(locationRadioButton); - expect(locationRadioButton).toHaveProperty("checked", true); - fireEvent.change(searchInput, { target: { value: "Mars" } }); - expect(locationRadioButton).toHaveProperty("checked", false); - }); - - it("shows error message when no matching locations can be found", async () => { - fireEvent.change(searchInput, { target: { value: "doof" } }); - - await waitFor(() => { - expect( - screen.getByText("Sorry, no matching location was found") - ).not.toBeNull(); - }); - expect(submitButton).toHaveAttribute("disabled"); - }); - - it("should get user Default location on render and auto select the location", async () => { - expect( - window.localStorage.getItem("userDefaultLoginLocationKeyDemo") - ).toEqual("111"); - const locationRadioButton: HTMLElement = await screen.getByRole("radio", { - name: /Earth/, - }); - expect(locationRadioButton).toHaveProperty("checked", true); - }); - - it("should set user Default location when location is changed", async () => { - const locationRadioButton: HTMLElement = await screen.findByLabelText( - /Earth/ - ); - fireEvent.click(locationRadioButton); - expect(window.localStorage.setItem).toHaveBeenCalled(); - expect(window.localStorage.setItem).toHaveBeenCalledWith( - "userDefaultLoginLocationKey", - "111" - ); - }); - - it("should display the correct pageSize", async () => { - expect(screen.getByText(/Showing 2 of 2 locations/i)).toBeInTheDocument(); - - const loginLocations: any = { - data: { - entry: [ - { resource: { id: "111", name: "Earth" } }, - { resource: { id: "222", name: "Mars" } }, - { resource: { id: "333", name: "Mercury" } }, - ], - }, - }; - - render( - - ); - - expect(screen.getByText(/Showing 3 of 3 locations/i)).toBeInTheDocument(); - }); -}); 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 23f2c4042..a6174f393 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 @@ -1,30 +1,23 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import debounce from "lodash-es/debounce"; -import isEmpty from "lodash-es/isEmpty"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { Button, Search, RadioButton, RadioButtonGroup, + Loading, + RadioButtonSkeleton, } from "carbon-components-react"; import { LocationEntry } from "../types"; -import { createErrorHandler, useConfig } from "@openmrs/esm-framework"; +import { useConfig } from "@openmrs/esm-framework"; import styles from "./location-picker.component.scss"; +import { useLoginLocations } from "../choose-location/choose-location.resource"; -interface LocationDataState { - activeLocation: string; - locationResult: Array; -} interface LocationPickerProps { currentUser: string; loginLocations: Array; onChangeLocation(locationUuid: string): void; - searchLocations( - query: string, - ac: AbortController, - useLoginLocationTag: boolean - ): Promise>; hideWelcomeMessage?: boolean; currentLocationUuid?: string; isLoginEnabled: boolean; @@ -34,7 +27,6 @@ const LocationPicker: React.FC = ({ currentUser, loginLocations, onChangeLocation, - searchLocations, hideWelcomeMessage, currentLocationUuid, isLoginEnabled, @@ -42,108 +34,58 @@ const LocationPicker: React.FC = ({ const config = useConfig(); const { chooseLocation } = config; const { t } = useTranslation(); + const [pageSize, setPageSize] = useState(chooseLocation.numberToShow); const userDefaultLoginLocation: string = "userDefaultLoginLocationKey"; const getDefaultUserLoginLocation = (): string => { const userLocation = window.localStorage.getItem( `${userDefaultLoginLocation}${currentUser}` ); - const isValidLocation = loginLocations.some( + const isValidLocation = loginLocations?.some( (location) => location.resource.id === userLocation ); return isValidLocation ? userLocation : ""; }; - const [locationData, setLocationData] = useState({ - activeLocation: getDefaultUserLoginLocation() ?? "", - locationResult: loginLocations, - }); + const [activeLocation, setActiveLocation] = useState( + getDefaultUserLoginLocation() ?? "" + ); const [searchTerm, setSearchTerm] = useState(""); + + const { + locationData, + isLoading, + hasMore, + totalResults, + loadingNewData, + setPage, + } = useLoginLocations( + chooseLocation.useLoginLocationTag, + chooseLocation.locationsPerRequest, + searchTerm + ); + const [isSubmitting, setIsSubmitting] = useState(false); - const [pageSize, setPageSize] = useState(chooseLocation.numberToShow); const inputRef = useRef(); - const searchTimeout = 300; useEffect(() => { if (isSubmitting) { - onChangeLocation(locationData.activeLocation); + onChangeLocation(activeLocation); setIsSubmitting(false); } - }, [isSubmitting, locationData, onChangeLocation]); + }, [isSubmitting, activeLocation, onChangeLocation]); useEffect(() => { - const ac = new AbortController(); - - if (loginLocations.length > 100) { - if (searchTerm) { - searchLocations( - searchTerm, - ac, - chooseLocation.useLoginLocationTag - ).then((locationResult) => { - changeLocationData({ - locationResult, - }); - }, createErrorHandler()); - } - } else if (searchTerm) { - filterList(searchTerm); - } else if (loginLocations !== locationData.locationResult) { - changeLocationData({ locationResult: loginLocations }); - } - - return () => ac.abort(); - }, [searchTerm, loginLocations]); - - const search = debounce((location: string) => { - clearSelectedLocation(); - setSearchTerm(location); - }, searchTimeout); - - const filterList = (searchTerm: string) => { - if (searchTerm) { - const updatedList = loginLocations.filter((item) => { - return ( - item.resource.name.toLowerCase().search(searchTerm.toLowerCase()) !== - -1 - ); - }); - - changeLocationData({ locationResult: updatedList }); - } - }; - - const changeLocationData = (data: Partial) => { - if (data) { - setLocationData((prevState) => ({ - ...prevState, - ...data, - })); - } - }; - - const handleSubmit = (evt: React.FormEvent) => { - evt.preventDefault(); - setIsSubmitting(true); - }; - - useEffect(() => { - if (locationData.activeLocation) { + if (activeLocation) { window.localStorage.setItem( `${userDefaultLoginLocation}${currentUser}`, - locationData.activeLocation + activeLocation ); } - }, [locationData.activeLocation, currentUser]); + }, [activeLocation, currentUser]); useEffect(() => { if (currentLocationUuid && hideWelcomeMessage) { - setLocationData((prevState) => ({ - ...prevState, - ...{ - activeLocation: currentLocationUuid, - locationResult: prevState.locationResult, - }, - })); + setActiveLocation(currentLocationUuid); } }, [currentLocationUuid, hideWelcomeMessage]); @@ -155,30 +97,53 @@ const LocationPicker: React.FC = ({ } }, [isSubmitting]); - const clearSelectedLocation = (): void => { - setLocationData((prevState) => ({ - activeLocation: "", - locationResult: prevState.locationResult, - })); + useEffect(() => { + if (!isLoading && totalResults && chooseLocation.numberToShow) { + setPageSize(Math.min(chooseLocation.numberToShow, totalResults)); + } + }, [isLoading, totalResults, chooseLocation.numberToShow]); + + const search = debounce((location: string) => { + setActiveLocation(""); + setSearchTerm(location); + setPage(1); + }, searchTimeout); + + const handleSubmit = (evt: React.FormEvent) => { + evt.preventDefault(); + setIsSubmitting(true); }; - useEffect(() => { - locationData.locationResult.length < pageSize && - setPageSize(locationData.locationResult.length); - chooseLocation.numberToShow > locationData.locationResult.length - ? setPageSize(locationData.locationResult.length) - : setPageSize(chooseLocation.numberToShow); - }, [locationData.locationResult.length]); + // Infinte scrolling + const observer = useRef(null); + const loadingIconRef = useCallback( + (node) => { + 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] + ); return ( -
+
-
+
-

+

{t("welcome", "Welcome")} {currentUser}

-

+

{t( "selectYourLocation", "Select your location from the list below. Use the search bar to search for your location." @@ -187,67 +152,85 @@ const LocationPicker: React.FC = ({

search(ev.target.value)} name="searchForLocation" /> -
-

- {searchTerm - ? `${locationData.locationResult.length} ${ - locationData.locationResult.length === 1 - ? t("match", "match") - : t("matches", "matches") - } ${t("found", "found")}` - : `${t("showing", "Showing")} ${pageSize} ${t("of", "of")} ${ - locationData.locationResult.length - } ${t("locations", "locations")}`} -

-
- {!isEmpty(locationData.locationResult) && ( - { - changeLocationData({ activeLocation: ev.toString() }); - }} - > - {locationData.locationResult - .slice(0, pageSize) - .map((entry) => ( - - ))} - - )} - {locationData.locationResult.length === 0 && ( -

- - Sorry, no matching location was found - +

+ {!isLoading ? ( + <> +

+ {searchTerm + ? `${locationData?.length ?? 0} ${ + locationData?.length === 1 + ? t("match", "match") + : t("matches", "matches") + } ${t("found", "found")}` + : `${t("showing", "Showing")} ${pageSize} ${t( + "of", + "of" + )} ${totalResults} ${t("locations", "locations")}`}

- )} -
-

+

+ {locationData?.length > 0 && ( + { + setActiveLocation(ev.toString()); + }} + > + {locationData.map((entry) => ( + + ))} + + )} + {locationData?.length === 0 && ( +

+ {t( + "locationNotFound", + "Sorry, no matching location was found" + )} +

+ )} +
+ + {hasMore && ( +
+ +
+ )} + + ) : ( +
+
-
+ )}
-
+
diff --git a/packages/apps/esm-login-app/translations/en.json b/packages/apps/esm-login-app/translations/en.json index 755bbc92c..57dbb9fb4 100644 --- a/packages/apps/esm-login-app/translations/en.json +++ b/packages/apps/esm-login-app/translations/en.json @@ -6,6 +6,7 @@ "error": "Error", "found": "found", "invalidCredentials": "Invalid username or password", + "loading": "Loading", "locationNotFound": "Sorry, no matching location was found.", "locations": "locations", "login": "Log in",