diff --git a/playwright/pages/detail_page.py b/playwright/pages/detail_page.py index c10a1b4f..599fe789 100644 --- a/playwright/pages/detail_page.py +++ b/playwright/pages/detail_page.py @@ -18,7 +18,7 @@ def __init__(self, page: Page): def load(self, uuid: str) -> None: """Load the detail page for the given uuid""" url = f'{settings.baseURL}/details?uuid={uuid}' - self.page.goto(url, timeout=90 * 1000) + self.page.goto(url, wait_until='domcontentloaded') def get_tab_section(self, title: str) -> Locator: """Returns tab section title element""" diff --git a/playwright/pages/landing_page.py b/playwright/pages/landing_page.py index defa8b9b..e4b7966a 100644 --- a/playwright/pages/landing_page.py +++ b/playwright/pages/landing_page.py @@ -11,4 +11,4 @@ def __init__(self, page: Page): self.search = SearchComponent(page) def load(self) -> None: - self.page.goto(settings.baseURL, timeout=90 * 1000) + self.page.goto(settings.baseURL, wait_until='domcontentloaded') diff --git a/playwright/tests/search_page/test_map.py b/playwright/tests/search_page/test_map.py index f104221e..5dc6c339 100644 --- a/playwright/tests/search_page/test_map.py +++ b/playwright/tests/search_page/test_map.py @@ -128,6 +128,8 @@ def test_map_base_layers( layer_id = layer_factory.get_layer_id(layer_type) assert search_page.map.is_map_layer_visible(layer_id) is True + if not search_page.get_text(layer_text).is_visible(): + search_page.map.basemap_show_hide_menu.click() search_page.click_text(layer_text) assert search_page.map.is_map_layer_visible(layer_id) is False diff --git a/src/components/common/store/__test__/componentParamReducer.test.tsx b/src/components/common/store/__test__/componentParamReducer.test.tsx index 2b95b007..0daf9c16 100644 --- a/src/components/common/store/__test__/componentParamReducer.test.tsx +++ b/src/components/common/store/__test__/componentParamReducer.test.tsx @@ -6,6 +6,7 @@ import { } from "../componentParamReducer"; import { bboxPolygon } from "@turf/turf"; +import { MapDefaultConfig } from "../../../map/mapbox/constants"; describe("Component Reducer Function Test", () => { it("Verify formatToUrlParam", () => { @@ -97,6 +98,7 @@ describe("Component Reducer Function Test", () => { isImosOnlyDataset: false, dateTimeFilterRange: {}, searchText: "", + zoom: MapDefaultConfig.ZOOM, }; const answer1: string = formatToUrlParam(sample1); @@ -111,6 +113,7 @@ describe("Component Reducer Function Test", () => { end: 45697, }, searchText: "This is test", + zoom: MapDefaultConfig.ZOOM, parameterVocabs: [ { label: "cat1", @@ -132,6 +135,7 @@ describe("Component Reducer Function Test", () => { end: 45697, }, searchText: "This is test", + zoom: 3, parameterVocabs: [ { label: "cat1", diff --git a/src/components/common/store/componentParamReducer.tsx b/src/components/common/store/componentParamReducer.tsx index 145f311b..b4a1fdfe 100644 --- a/src/components/common/store/componentParamReducer.tsx +++ b/src/components/common/store/componentParamReducer.tsx @@ -17,6 +17,7 @@ const UPDATE_PARAMETER_VOCAB_FILTER_VARIABLE = "UPDATE_PARAMETER_VOCAB_FILTER_VARIABLE"; const UPDATE_UPDATE_FREQ_VARIABLE = "UPDATE_UPDATE_FREQ_VARIABLE"; const UPDATE_SORT_BY_VARIABLE = "UPDATE_SORT_BY_VARIABLE"; +const UPDATE_ZOOM_VARIABLE = "UPDATE_ZOOM_VARIABLE"; const { WEST_LON, EAST_LON, NORTH_LAT, SOUTH_LAT } = MapDefaultConfig.BBOX_ENDPOINTS; @@ -35,6 +36,7 @@ export interface ParameterState { parameterVocabs?: Array; updateFreq?: DatasetFrequency | undefined; sortby?: string; + zoom?: number; } // Function use to test an input value is of type Vocab const isVocabType = (value: any): value is Vocab => @@ -128,6 +130,15 @@ const updateSortBy = ( }; }; +const updateZoom = (input: number | undefined): ActionType => { + return { + type: UPDATE_ZOOM_VARIABLE, + payload: { + zoom: input, + } as ParameterState, + }; +}; + // Initial State const createInitialParameterState = ( withDefaultPolygon: boolean = true @@ -136,6 +147,7 @@ const createInitialParameterState = ( isImosOnlyDataset: false, dateTimeFilterRange: {}, searchText: "", + zoom: MapDefaultConfig.ZOOM, }; if (withDefaultPolygon) { @@ -208,6 +220,11 @@ const paramReducer = ( ...state, sortby: action.payload.sortby, }; + case UPDATE_ZOOM_VARIABLE: + return { + ...state, + zoom: action.payload.zoom, + }; case UPDATE_PARAMETER_STATES: return { ...state, @@ -346,4 +363,5 @@ export { updateParameterStates, updateSortBy, updateUpdateFreq, + updateZoom, }; diff --git a/src/components/loading/SnackbarLoader.tsx b/src/components/loading/SnackbarLoader.tsx index 2c56e0f1..053cd5ea 100644 --- a/src/components/loading/SnackbarLoader.tsx +++ b/src/components/loading/SnackbarLoader.tsx @@ -15,7 +15,6 @@ const SnackbarLoader: React.FC = ({ { resizeObserver.unobserve(map.getContainer()); map.off("zoomend", debounceOnZoomEvent); @@ -161,12 +166,22 @@ const ReactMap = ({ useEffect(() => { // The map center is use to set the initial center point, however we may need // to set to other place, for example if the user pass the url to someone - bbox && - map && - map.setCenter( - new LngLatBounds(bbox as [number, number, number, number]).getCenter() - ); - }, [bbox, map]); + if (bbox && map) { + // Turn off event to avoid looping + map.off("zoomend", debounceOnZoomEvent); + map.off("moveend", debounceOnMoveEvent); + // DO NOT use fitBounds(), it will cause the zoom and padding adjust so + // you end up map area drift. + map.jumpTo({ + center: bbox.getCenter(), + zoom: zoom, + }); + map.on("idle", () => { + map.on("zoomend", debounceOnZoomEvent); + map.on("moveend", debounceOnMoveEvent); + }); + } + }, [bbox, zoom, map, debounceOnZoomEvent, debounceOnMoveEvent]); return ( map && ( diff --git a/src/components/map/mapbox/constants.ts b/src/components/map/mapbox/constants.ts index 7483d0e5..ed5d6051 100644 --- a/src/components/map/mapbox/constants.ts +++ b/src/components/map/mapbox/constants.ts @@ -1,6 +1,7 @@ export const MapDefaultConfig = { // Magic number, try and error by experience DEBOUNCE_BEFORE_EVENT_FIRE: 700, + ZOOM: 4, MIN_ZOOM: 1, MAX_ZOOM: 12, PROJECTION: "equirectangular", diff --git a/src/hooks/useRedirectSearch.tsx b/src/hooks/useRedirectSearch.tsx index 5dbc6c4f..fb66a815 100644 --- a/src/hooks/useRedirectSearch.tsx +++ b/src/hooks/useRedirectSearch.tsx @@ -9,13 +9,17 @@ import store, { getComponentState } from "../components/common/store/store"; const useRedirectSearch = () => { const navigate = useNavigate(); - const redirectSearch = (referer: string) => { + const redirectSearch = ( + referer: string, + fromNavigate: boolean = true, + requireSearch: boolean = true + ) => { const componentParam: ParameterState = getComponentState(store.getState()); navigate(pageDefault.search + "?" + formatToUrlParam(componentParam), { state: { - fromNavigate: true, - requireSearch: true, - referer, + fromNavigate: fromNavigate, + requireSearch: requireSearch, + referer: referer, }, }); }; diff --git a/src/pages/detail-page/subpages/HeaderSection.tsx b/src/pages/detail-page/subpages/HeaderSection.tsx index 55fc1c9b..f3487480 100644 --- a/src/pages/detail-page/subpages/HeaderSection.tsx +++ b/src/pages/detail-page/subpages/HeaderSection.tsx @@ -30,6 +30,7 @@ import { } from "../../../components/common/store/componentParamReducer"; import OrganizationLogo from "../../../components/logo/OrganizationLogo"; import { pageDefault } from "../../../components/common/constants"; +import useRedirectSearch from "../../../hooks/useRedirectSearch"; interface ButtonWithIcon { label: string; @@ -54,21 +55,14 @@ const buttons: Record = { }; const HeaderSection = () => { - const navigate = useNavigate(); const { collection } = useDetailPageContext(); const title = collection?.title; + const redirectSearch = useRedirectSearch(); // TODO: on click user goes back to search page where has results based on previous search params const onGoBack = useCallback(() => { - const componentParam: ParameterState = getComponentState(store.getState()); - navigate(pageDefault.search + "?" + formatToUrlParam(componentParam), { - state: { - fromNavigate: true, - requireSearch: true, - referer: "HeaderSection", - }, - }); - }, [navigate]); + redirectSearch("HeaderSection", true, false); + }, [redirectSearch]); // TODO: implement the goNext and goPrevious function // This will require the entire search results (their ids and indexes) based on search params diff --git a/src/pages/detail-page/subpages/tab-panels/__test__/LinksPanel.test.tsx b/src/pages/detail-page/subpages/tab-panels/__test__/LinksPanel.test.tsx index 87778d40..83e6288a 100644 --- a/src/pages/detail-page/subpages/tab-panels/__test__/LinksPanel.test.tsx +++ b/src/pages/detail-page/subpages/tab-panels/__test__/LinksPanel.test.tsx @@ -51,19 +51,16 @@ describe("LinksPanel", async () => { ); }); - test("should render LinksPanel", async () => { - await waitFor(() => { - expect(screen.queryAllByText("Data access using R")).to.exist; - expect( - screen.queryAllByText("Marine Weather Observations for Davies Reef") - ).to.exist; - expect(screen.queryAllByText("Data access via AODN Portal")).to.exist; - expect(screen.queryAllByText("Data access via Programming API")).to.exist; - }); + test("should render LinksPanel", () => { + expect(screen.queryAllByText("Data access using R")).to.exist; + expect(screen.queryAllByText("Marine Weather Observations for Davies Reef")) + .to.exist; + expect(screen.queryAllByText("Data access via AODN Portal")).to.exist; + expect(screen.queryAllByText("Data access via Programming API")).to.exist; }); - test("should show COPY LINK button when on hover", async () => { - await waitFor(() => { + test("should show COPY LINK button when on hover", () => { + waitFor(() => screen.findByText("Data access using R")).then(() => { const link = screen.queryByText("Data access using R"); expect(link).to.exist; userEvent.hover(link!); diff --git a/src/pages/search-page/SearchPage.tsx b/src/pages/search-page/SearchPage.tsx index 2ad6ba27..a204d7cd 100644 --- a/src/pages/search-page/SearchPage.tsx +++ b/src/pages/search-page/SearchPage.tsx @@ -16,6 +16,7 @@ import { updateFilterPolygon, updateParameterStates, updateSortBy, + updateZoom, } from "../../components/common/store/componentParamReducer"; import store, { getComponentState } from "../../components/common/store/store"; @@ -31,7 +32,7 @@ import store, { getComponentState } from "../../components/common/store/store"; // import MapboxDrawControl from "../components/map/maplibre/controls/MapboxDrawControl"; // import VectorTileLayers from "../components/map/maplibre/layers/VectorTileLayers"; // Map section, you can switch to other map library, this is for mapbox -import { LngLatBoundsLike, MapboxEvent as MapEvent } from "mapbox-gl"; +import { LngLatBounds, MapboxEvent as MapEvent } from "mapbox-gl"; import ResultSection from "./subpages/ResultSection"; import MapSection from "./subpages/MapSection"; import { color, padding } from "../../styles/constants"; @@ -45,6 +46,8 @@ import { import { useAppDispatch } from "../../components/common/store/hooks"; import { pageDefault } from "../../components/common/constants"; +const REFERER = "SEARCH_PAGE"; + const isLoading = (count: number): boolean => { if (count > 0) { return true; @@ -71,7 +74,8 @@ const SearchPage = () => { ); const [selectedUuids, setSelectedUuids] = useState>([]); const [datasetsSelected, setDatasetsSelected] = useState(); - const [bbox, setBbox] = useState(undefined); + const [bbox, setBbox] = useState(undefined); + const [zoom, setZoom] = useState(undefined); const [loadingThreadCount, setLoadingThreadCount] = useState(0); const startOneLoadingThread = useCallback(() => { @@ -140,6 +144,27 @@ const SearchPage = () => { [getCollectionsData] ); + const doMapSearch = useCallback(() => { + const componentParam: ParameterState = getComponentState(store.getState()); + + // Use a different parameter so that it return id and bbox only and do not store the values, + // we cannot add page because we want to show all record on map + const paramNonPaged = createSearchParamFrom(componentParam); + return dispatch( + // add param "sortby: id" for fetchResultNoStore to ensure data source for map is always sorted + // and ordered by uuid to avoid affecting cluster calculation + fetchResultNoStore({ + ...paramNonPaged, + properties: "id,centroid", + sortby: "id", + }) + ) + .unwrap() + .then((collections) => { + setLayers(collections.collections); + }); + }, [dispatch]); + const doSearch = useCallback( (needNavigate: boolean = true) => { startOneLoadingThread(); @@ -153,43 +178,33 @@ const SearchPage = () => { pagesize: DEFAULT_SEARCH_PAGE, }); - dispatch(fetchResultWithStore(paramPaged)).then(() => { - // Use a different parameter so that it return id and bbox only and do not store the values, - // we cannot add page because we want to show all record on map - const paramNonPaged = createSearchParamFrom(componentParam); - dispatch( - // add param "sortby: id" for fetchResultNoStore to ensure data source for map is always sorted - // and ordered by uuid to avoid affecting cluster calculation - fetchResultNoStore({ - ...paramNonPaged, - properties: "id,centroid", - sortby: "id", - }) - ) - .unwrap() - .then((collections) => { - setLayers(collections.collections); - }) - .then(() => { - if (needNavigate) { - navigate( - pageDefault.search + "?" + formatToUrlParam(componentParam), - { - state: { - fromNavigate: true, - requireSearch: false, - referer: "SearchPage", - }, - } - ); - } - }) - .finally(() => { - endOneLoadingThread(); - }); - }); + dispatch(fetchResultWithStore(paramPaged)); + doMapSearch() + .then(() => { + if (needNavigate) { + navigate( + pageDefault.search + "?" + formatToUrlParam(componentParam), + { + state: { + fromNavigate: true, + requireSearch: false, + referer: REFERER, + }, + } + ); + } + }) + .finally(() => { + endOneLoadingThread(); + }); }, - [startOneLoadingThread, dispatch, navigate, endOneLoadingThread] + [ + startOneLoadingThread, + endOneLoadingThread, + doMapSearch, + dispatch, + navigate, + ] ); // The result will be changed based on the zoomed area, that is only // dataset where spatial extends fall into the zoomed area will be selected. @@ -213,7 +228,10 @@ const SearchPage = () => { componentParam.polygon && !booleanEqual(componentParam.polygon, polygon) ) { + setBbox(bounds); + setZoom(event.target.getZoom()); dispatch(updateFilterPolygon(polygon)); + dispatch(updateZoom(event.target.getZoom())); doSearch(); } } @@ -233,7 +251,12 @@ const SearchPage = () => { dispatch(updateParameterStates(paramState)); // URL request, we need to adjust the map to the same area as mentioned // in the url - setBbox(paramState.polygon?.bbox as LngLatBoundsLike); + setBbox( + new LngLatBounds( + paramState.polygon?.bbox as [number, number, number, number] + ) + ); + setZoom(paramState.zoom); doSearch(); } @@ -243,8 +266,35 @@ const SearchPage = () => { // but do not navigate again. doSearch(false); } + // If it is navigate from this component, and no need to search, that + // mean we already call doSearch() + doMapSearch(), however if you + // come from other page, the result list is good because we remember it + // but the map need init again and therefore need to do a doMapSearch() + else if (location.state?.referer !== REFERER) { + const componentParam: ParameterState = getComponentState( + store.getState() + ); + setBbox( + new LngLatBounds( + componentParam.polygon?.bbox as [number, number, number, number] + ) + ); + setZoom(componentParam.zoom); + + startOneLoadingThread(); + doMapSearch().finally(() => { + endOneLoadingThread(); + }); + } } - }, [location, dispatch, doSearch]); + }, [ + location, + dispatch, + doSearch, + doMapSearch, + startOneLoadingThread, + endOneLoadingThread, + ]); const onChangeSorting = useCallback( (v: SortResultEnum) => { @@ -308,6 +358,7 @@ const SearchPage = () => { justifyContent: "space-between", alignItems: "stretch", width: "100%", + height: "90vh", padding: padding.small, bgcolor: color.blue.light, }} @@ -326,11 +377,9 @@ const SearchPage = () => { )} ; selectedUuids: string[]; onMapZoomOrMove: ( @@ -45,6 +46,7 @@ interface MapSectionProps { const MapSection: React.FC = ({ bbox, + zoom, onMapZoomOrMove, onToggleClicked, onDatasetSelected, @@ -91,11 +93,15 @@ const MapSection: React.FC = ({ ); return ( - + diff --git a/src/pages/search-page/subpages/ResultSection.tsx b/src/pages/search-page/subpages/ResultSection.tsx index 77c740d7..e5339c13 100644 --- a/src/pages/search-page/subpages/ResultSection.tsx +++ b/src/pages/search-page/subpages/ResultSection.tsx @@ -61,7 +61,7 @@ const ResultSection: FC = ({