From 30545b34c7e163dd8a924547a73d40bd5ba16bea Mon Sep 17 00:00:00 2001 From: utas-raymondng Date: Wed, 17 Jul 2024 15:07:11 +1000 Subject: [PATCH] Add sorting --- .../common/buttons/MapListToggleButton.tsx | 56 ++++---- src/components/common/buttons/SortButton.tsx | 130 +++++++++++++++++- .../filters/ResultPanelSimpleFilter.tsx | 23 ++-- .../common/store/componentParamReducer.tsx | 24 ++++ src/components/common/store/searchReducer.tsx | 7 + src/pages/search-page/SearchPage.tsx | 51 +++++-- .../search-page/subpages/ResultSection.tsx | 8 +- 7 files changed, 239 insertions(+), 60 deletions(-) diff --git a/src/components/common/buttons/MapListToggleButton.tsx b/src/components/common/buttons/MapListToggleButton.tsx index 8161c850..2840d3e2 100644 --- a/src/components/common/buttons/MapListToggleButton.tsx +++ b/src/components/common/buttons/MapListToggleButton.tsx @@ -1,6 +1,6 @@ import { IconButton, ListItemIcon, MenuItem } from "@mui/material"; import ArrowDropDownSharpIcon from "@mui/icons-material/ArrowDropDownSharp"; -import React, { useState } from "react"; +import React, { FC, useCallback, useState } from "react"; import Menu from "@mui/material/Menu"; import ActionButtonPaper from "./ActionButtonPaper"; import GridAndMapIcon from "../../icon/GridAndMapIcon"; @@ -14,53 +14,57 @@ enum SearchResultLayoutEnum { VISIBLE = "VISIBLE", } -interface MapListToggleButtonProps { +export interface MapListToggleButtonProps { onChangeLayout: (layout: SearchResultLayoutEnum) => void; } -const MapListToggleButton = ({ onChangeLayout }: MapListToggleButtonProps) => { +const determineShowingIcon = (resultLayout: SearchResultLayoutEnum) => { + switch (resultLayout) { + case SearchResultLayoutEnum.LIST: + return ; + + case SearchResultLayoutEnum.GRID: + return ; + + default: + return ; + } +}; + +const MapListToggleButton: FC = ({ + onChangeLayout, +}) => { const [anchorEl, setAnchorEl] = useState(null); const [resultLayout, setResultLayout] = useState( SearchResultLayoutEnum.GRID ); - const open = Boolean(anchorEl); - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; + const handleClick = useCallback( + (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, + [setAnchorEl] + ); - const handleClose = () => { + const handleClose = useCallback(() => { setAnchorEl(null); - }; - - const determineShowingIcon = () => { - switch (resultLayout) { - case SearchResultLayoutEnum.LIST: - return ; - - case SearchResultLayoutEnum.GRID: - return ; - - default: - return ; - } - }; + }, [setAnchorEl]); return ( - {determineShowingIcon()} + {determineShowingIcon(resultLayout)} void; +} + +const determineShowingIcon = (resultLayout: SortResultEnum) => { + switch (resultLayout) { + case SortResultEnum.RELEVANT: + return ; + + case SortResultEnum.TITLE: + return ; + + case SortResultEnum.POPULARITY: + return ; + + case SortResultEnum.MODIFIED: + return ; + + default: + return ; + } +}; + +const SortButton: FC = ({ onChangeSorting }) => { + const [anchorEl, setAnchorEl] = useState(null); + const [resultLayout, setResultLayout] = useState( + SortResultEnum.RELEVANT + ); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, + [setAnchorEl] + ); + + const handleClose = useCallback(() => { + setAnchorEl(null); + }, [setAnchorEl]); -const SortButton = () => { return ( - - + + {determineShowingIcon(resultLayout)} + + { + // No need to set layout because we want to + // remember the last layout + handleClose(); + onChangeSorting(SortResultEnum.RELEVANT); + }} + > + + + + Relevancy + + { + setResultLayout(SortResultEnum.TITLE); + handleClose(); + onChangeSorting(SortResultEnum.TITLE); + }} + > + + + + Title + + { + setResultLayout(SortResultEnum.POPULARITY); + handleClose(); + onChangeSorting(SortResultEnum.POPULARITY); + }} + > + + + + Popularity + + { + setResultLayout(SortResultEnum.MODIFIED); + handleClose(); + onChangeSorting(SortResultEnum.MODIFIED); + }} + > + + + + Modified + + ); }; +export { SortResultEnum }; export default SortButton; diff --git a/src/components/common/filters/ResultPanelSimpleFilter.tsx b/src/components/common/filters/ResultPanelSimpleFilter.tsx index e75c36a9..a26be372 100644 --- a/src/components/common/filters/ResultPanelSimpleFilter.tsx +++ b/src/components/common/filters/ResultPanelSimpleFilter.tsx @@ -12,14 +12,10 @@ import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import MapListToggleButton, { - SearchResultLayoutEnum, + MapListToggleButtonProps, } from "../buttons/MapListToggleButton"; -import SortButton from "../buttons/SortButton"; -import React from "react"; - -// interface ResultPanelSimpleFilterProps { -// filterClicked?: (e: React.MouseEvent) => void; -// } +import SortButton, { SortButtonProps } from "../buttons/SortButton"; +import { FC } from "react"; const SlimSelect = styled(InputBase)(() => ({ "& .MuiInputBase-input": { @@ -27,13 +23,14 @@ const SlimSelect = styled(InputBase)(() => ({ }, })); -interface ResultPanelSimpleFilterProps { - onChangeLayout: (layout: SearchResultLayoutEnum) => void; -} +interface ResultPanelSimpleFilterProps + extends MapListToggleButtonProps, + SortButtonProps {} -const ResultPanelSimpleFilter = ({ +const ResultPanelSimpleFilter: FC = ({ onChangeLayout, -}: ResultPanelSimpleFilterProps) => { + onChangeSorting, +}) => { return ( @@ -62,7 +59,7 @@ const ResultPanelSimpleFilter = ({ - + diff --git a/src/components/common/store/componentParamReducer.tsx b/src/components/common/store/componentParamReducer.tsx index 25344a9b..2b2c481c 100644 --- a/src/components/common/store/componentParamReducer.tsx +++ b/src/components/common/store/componentParamReducer.tsx @@ -4,6 +4,7 @@ */ import { bboxPolygon } from "@turf/turf"; import { Feature, Polygon, GeoJsonProperties } from "geojson"; +import { sortBy } from "lodash"; const UPDATE_PARAMETER_STATES = "UPDATE_PARAMETER_STATES"; const UPDATE_DATETIME_FILTER_VARIABLE = "UPDATE_DATETIME_FILTER_VARIABLE"; @@ -12,6 +13,7 @@ const UPDATE_IMOS_ONLY_DATASET_FILTER_VARIABLE = "UPDATE_IMOS_ONLY_DATASET_FILTER_VARIABLE"; const UPDATE_POLYGON_FILTER_VARIABLE = "UPDATE_POLYGON_FILTER_VARIABLE"; const UPDATE_CATEGORY_FILTER_VARIABLE = "UPDATE_CATEGORY_FILTER_VARIABLE"; +const UPDATE_SORT_BY_VARIABLE = "UPDATE_SORT_BY_VARIABLE"; interface DataTimeFilterRange { // Cannot use Date in Redux as it is non-serializable @@ -27,6 +29,7 @@ export interface ParameterState { // Use in search box searchText?: string; categories?: Array; + sortby?: string; } // Function use to test an input value is of type Category const isTypeCategory = (value: any): value is Category => @@ -96,6 +99,21 @@ const updateCategories = (input: Array): ActionType => { }; }; +const updateSortBy = ( + input: Array<{ field: string; order: "ASC" | "DESC" }> +): ActionType => { + return { + type: UPDATE_SORT_BY_VARIABLE, + payload: { + sortby: input + .map((item) => + item.order === "ASC" ? `+${item.field}` : `-${item.field}` + ) + .join(","), + }, + }; +}; + // Initial State const createInitialParameterState = (): ParameterState => { return { @@ -139,6 +157,11 @@ const paramReducer = ( ...state, categories: action.payload.categories, }; + case UPDATE_SORT_BY_VARIABLE: + return { + ...state, + sortby: action.payload.sortby, + }; case UPDATE_PARAMETER_STATES: return { ...state, @@ -264,4 +287,5 @@ export { updateFilterPolygon, updateCategories, updateParameterStates, + updateSortBy, }; diff --git a/src/components/common/store/searchReducer.tsx b/src/components/common/store/searchReducer.tsx index 13d14d72..42ba722f 100644 --- a/src/components/common/store/searchReducer.tsx +++ b/src/components/common/store/searchReducer.tsx @@ -151,12 +151,14 @@ export type SearchParameters = { text?: string; filter?: string; properties?: string; + sortby?: string; }; type OGCSearchParameters = { q?: string; filter?: string; properties?: string; + sortby?: string; }; export interface CollectionsQueryType { @@ -208,6 +210,10 @@ const searchResult = async (param: SearchParameters, thunkApi: any) => { p.filter = param.filter; } + if (param.sortby !== undefined && param.sortby.length !== 0) { + p.sortby = param.sortby; + } + const response = await axios.get( "/api/v1/ogc/collections", { @@ -363,6 +369,7 @@ const createSearchParamFrom = (i: ParameterState): SearchParameters => { const p: SearchParameters = {}; p.text = i.searchText; p.filter = undefined; + p.sortby = i.sortby; if (i.isImosOnlyDataset) { p.filter = appendFilter( diff --git a/src/pages/search-page/SearchPage.tsx b/src/pages/search-page/SearchPage.tsx index 1ac521bd..1eca3dcc 100644 --- a/src/pages/search-page/SearchPage.tsx +++ b/src/pages/search-page/SearchPage.tsx @@ -18,6 +18,7 @@ import { unFlattenToParameterState, updateFilterPolygon, updateParameterStates, + updateSortBy, } from "../../components/common/store/componentParamReducer"; import store, { AppDispatch, @@ -47,6 +48,7 @@ import { color, margin } from "../../styles/constants"; import ComplexTextSearch from "../../components/search/ComplexTextSearch"; import { SearchResultLayoutEnum } from "../../components/common/buttons/MapListToggleButton"; import { bboxPolygon } from "@turf/turf"; +import { SortResultEnum } from "../../components/common/buttons/SortButton"; const SearchPage = () => { const location = useLocation(); @@ -167,16 +169,6 @@ const SearchPage = () => { }, [dispatch, doSearch] ); - // const onRemoveLayer = useCallback( - // ( - // event: React.MouseEvent, - // collection: OGCCollection | undefined - // ) => { - // // Remove the layer if found - // setLayers((v) => v.filter((i) => i.id !== collection?.id)); - // }, - // [setLayers] - // ); // If this flag is set, that means it is call from within react // and the search status already refresh and useSelector contains // the correct values, else it is user paste the url directly @@ -210,11 +202,39 @@ const SearchPage = () => { // to this page. useEffect(() => handleNavigation(), [handleNavigation]); - const handleNavigateToDetailPage = (uuid: string) => { - const searchParams = new URLSearchParams(); - searchParams.append("uuid", uuid); - navigate(pageDefault.details + "?" + searchParams.toString()); - }; + const handleNavigateToDetailPage = useCallback( + (uuid: string) => { + const searchParams = new URLSearchParams(); + searchParams.append("uuid", uuid); + navigate(pageDefault.details + "?" + searchParams.toString()); + }, + [navigate] + ); + + const onChangeSorting = useCallback( + (v: SortResultEnum) => { + switch (v) { + case SortResultEnum.RELEVANT: + dispatch(updateSortBy([{ field: "score", order: "DESC" }])); + break; + + case SortResultEnum.TITLE: + dispatch(updateSortBy([{ field: "title", order: "ASC" }])); + break; + + case SortResultEnum.POPULARITY: + //TODO: need ogcapi change + break; + + case SortResultEnum.MODIFIED: + //TODO: need ogcapi change + break; + } + + doSearch(); + }, + [dispatch, doSearch] + ); return ( @@ -246,6 +266,7 @@ const SearchPage = () => { onRemoveLayer={undefined} onVisibilityChanged={onVisibilityChanged} onClickCard={handleNavigateToDetailPage} + onChangeSorting={onChangeSorting} datasetSelected={datasetsSelected} /> void; onVisibilityChanged?: (v: SearchResultLayoutEnum) => void; + onChangeSorting: (v: SortResultEnum) => void; onClickCard?: (uuid: string) => void; datasetSelected?: OGCCollection[]; } @@ -25,6 +27,7 @@ const ResultSection: React.FC = ({ contents, onRemoveLayer, onVisibilityChanged, + onChangeSorting, onClickCard, datasetSelected, }) => { @@ -63,7 +66,10 @@ const ResultSection: React.FC = ({ }} data-testid="search-page-result-list" > - +