diff --git a/geonode_mapstore_client/client/js/actions/gnsearch.js b/geonode_mapstore_client/client/js/actions/gnsearch.js index d4092af029..fa93bc7ea8 100644 --- a/geonode_mapstore_client/client/js/actions/gnsearch.js +++ b/geonode_mapstore_client/client/js/actions/gnsearch.js @@ -21,6 +21,7 @@ export const GET_FACET_ITEMS = 'GEONODE:GET_FACET_ITEMS'; export const SET_FACET_ITEMS = 'GEONODE:SET_FACET_ITEMS'; export const GET_FACET_FILTERS = 'GEONODE:GET_FACET_FILTERS'; export const SET_FILTERS = "SET_FILTERS"; +export const SHOW_FILTER_FORM = "GEONODE:SHOW_FILTER_FORM"; /** * Actions for GeoNode resource featured items @@ -140,6 +141,13 @@ export function setFilters(filters) { }; } +export function showFilterForm(show) { + return { + type: SHOW_FILTER_FORM, + show + }; +} + export default { SEARCH_RESOURCES, searchResources, @@ -151,5 +159,7 @@ export default { requestResource, setFeaturedResources, SET_SEARCH_CONFIG, - setSearchConfig + setSearchConfig, + SHOW_FILTER_FORM, + showFilterForm }; diff --git a/geonode_mapstore_client/client/js/components/FiltersMenu/FiltersMenu.jsx b/geonode_mapstore_client/client/js/components/FiltersMenu/FiltersMenu.jsx index 459988e72b..9d86c4544a 100644 --- a/geonode_mapstore_client/client/js/components/FiltersMenu/FiltersMenu.jsx +++ b/geonode_mapstore_client/client/js/components/FiltersMenu/FiltersMenu.jsx @@ -32,7 +32,8 @@ const FiltersMenu = forwardRef(({ loading, hideCardLayoutButton, cardLayoutStyle, - setCardLayoutStyle + setCardLayoutStyle, + disableFilters }, ref) => { const { isMobile } = getConfigProp('geoNodeSettings'); @@ -50,22 +51,24 @@ const FiltersMenu = forwardRef(({
- {totalFilters > 0 ? } - > - {isMobile ? : } - : } - {' '} + {!disableFilters && <> + {totalFilters > 0 ? } + > + {isMobile ? : } + : } + {' '} + } {loading ? : } diff --git a/geonode_mapstore_client/client/js/plugins/ResourcesGrid.jsx b/geonode_mapstore_client/client/js/plugins/ResourcesGrid.jsx index fd6a96a9fe..9abfbb7db5 100644 --- a/geonode_mapstore_client/client/js/plugins/ResourcesGrid.jsx +++ b/geonode_mapstore_client/client/js/plugins/ResourcesGrid.jsx @@ -30,8 +30,8 @@ import { import { withResizeDetector } from 'react-resize-detector'; import { userSelector } from '@mapstore/framework/selectors/security'; import ConnectedCardGrid from '@js/plugins/resourcesgrid/ConnectedCardGrid'; -import { getTotalResources, getFacetsItems } from '@js/selectors/search'; -import { searchResources, setSearchConfig, getFacetItems, setFilters as setFiltersAction } from '@js/actions/gnsearch'; +import { getTotalResources, getFacetsItems, getShowFilterForm } from '@js/selectors/search'; +import { searchResources, setSearchConfig, getFacetItems, setFilters as setFiltersAction, showFilterForm as showFilterFormAction } from '@js/actions/gnsearch'; import gnsearch from '@js/reducers/gnsearch'; import gnresource from '@js/reducers/gnresource'; @@ -43,12 +43,11 @@ import resourceServiceEpics from '@js/epics/resourceservice'; import favoriteEpics from '@js/epics/favorite'; import DetailsPanel from '@js/components/DetailsPanel'; import { processingDownload } from '@js/selectors/resourceservice'; -import { resourceHasPermission, getCataloguePath } from '@js/utils/ResourceUtils'; +import { resourceHasPermission } from '@js/utils/ResourceUtils'; import {downloadResource, setFavoriteResource} from '@js/actions/gnresource'; import FiltersForm from '@js/components/FiltersForm'; import usePluginItems from '@js/hooks/usePluginItems'; import { ProcessTypes } from '@js/utils/ResourceServiceUtils'; -import { replace } from 'connected-react-router'; import FaIcon from '@js/components/FaIcon'; import Button from '@js/components/Button'; import useLocalStorage from '@js/hooks/useLocalStorage'; @@ -117,40 +116,102 @@ function PaginationCustom({ ); } -const removeMenuHighlight = () => { - // Remove previous higlighted menu - const menuHiglighted = document.querySelector('#gn-topbar .highlight-menu'); - menuHiglighted?.classList.remove('highlight-menu'); -}; const getCatalogPage = (pathname) => { - const {params: {page} = {}} = matchPath(pathname, { path: "/:page", exact: true }) ?? {}; + const { params: {page} = {} } = matchPath(pathname, { path: "/:page", exact: true }) ?? {}; return page; }; const withPageConfig = (Component) => { return (props) => { - useEffect(() => { - // highlight topbar menu item based on catalog page - const page = getCatalogPage(props.location.pathname); - - if (page) { - removeMenuHighlight(); - - const topbarMenu = document.querySelector(`#gn-topbar #${page}`); - topbarMenu?.classList.add('highlight-menu'); - } else { - removeMenuHighlight(); - } - }, [props.location.pathname]); - const mergePropsWithPageConfigs = () => { const pageName = getCatalogPage(props.location.pathname, props); return {...props, ...props?.[`${pageName}Page`]}; }; - return ; }; }; +const useResourceGridLayout = ({ + headerNodeSelector, + navbarNodeSelector, + footerNodeSelector, + containerSelector, + showFilterForm, + showDetail, + width, + height, + panel +}) => { + const [stickyTop, setStickyTop] = useState(0); + const [stickyBottom, setStickyBottom] = useState(0); + useEffect(() => { + if (!panel) { + const header = headerNodeSelector ? document.querySelector(headerNodeSelector) : null; + const navbar = navbarNodeSelector ? document.querySelector(navbarNodeSelector) : null; + const footer = footerNodeSelector ? document.querySelector(footerNodeSelector) : null; + const { height: headerHeight = 0 } = header?.getBoundingClientRect() || {}; + const { height: navbarHeight = 0 } = navbar?.getBoundingClientRect() || {}; + const { height: footerHeight = 0 } = footer?.getBoundingClientRect() || {}; + setStickyTop(headerHeight + navbarHeight); + setStickyBottom(footerHeight); + } + }, [width, height, panel]); + const detailNode = useRef(); + const filterFormNode = useRef(); + const { width: filterFormNodeWidth = 0 } = filterFormNode?.current?.getBoundingClientRect() || {}; + const { width: detailNodeWidth = 0 } = detailNode?.current?.getBoundingClientRect() || {}; + const filterFormWidth = showFilterForm ? filterFormNodeWidth : 0; + const detailWidth = showDetail ? detailNodeWidth : 0; + const panelsWidth = filterFormWidth + detailWidth; + const container = containerSelector ? document.querySelector(containerSelector) : null; + const { height: containerHeight } = container?.getBoundingClientRect() || {}; + useEffect(() => { + if (container && !panel) { + container.style.width = `calc(100% - ${panelsWidth}px)`; + container.style.marginLeft = `${filterFormWidth}px`; + } + }, [container, panelsWidth, filterFormWidth, panel]); + const filterMenuNode = useRef(); + const footerPaginationNode = useRef(); + const [top, setTop] = useState(); + const [bottom, setBottom] = useState(); + const fullPageScroll = !container && !panel; + useEffect(() => { + let onScroll; + // reset top and bottom + setTop(); + setBottom(); + if (fullPageScroll) { + onScroll = () => { + const { top: filterMenuTop = 0 } = filterMenuNode?.current?.getBoundingClientRect() || {}; + const footerPaginationRect = footerPaginationNode?.current?.getBoundingClientRect() || {}; + setTop(filterMenuTop); + setBottom(window.innerHeight - footerPaginationRect.y - footerPaginationRect.height); + }; + onScroll(); + document.addEventListener('scroll', onScroll); + } + return () => { + if (onScroll) { + document.removeEventListener('scroll', onScroll); + } + }; + }, [width, height, fullPageScroll]); + return { + container, + detailNode, + filterFormNode, + filterMenuNode, + footerPaginationNode, + stickyTop, + stickyBottom, + containerHeight, + panelsWidth, + filterFormWidth, + top: top === undefined ? stickyTop : top, + bottom: bottom === undefined ? stickyBottom : bottom + }; +}; + /** * @module ResourcesGrid */ @@ -174,7 +235,6 @@ const withPageConfig = (Component) => { * @prop {boolean} pagination Provides a config to allow for pagination * @prop {boolean} disableDetailPanel Provides a config to allow resource details to be viewed when selected. * @prop {boolean} disableFilters Provides a config to enable/disable filtering of resources - * @prop {string} filterPagePath sets path for filters page when filter button is clicked * @prop {array} resourceCardActionsOrder order in which `cfg.items` will be rendered * @prop {boolean} enableGeoNodeCardsMenuItems Provides a config to allow for card menu items to be enabled/disabled. * @prop {boolean} panel when enabled, the component render the list of resources, filters and details preview inside a panel @@ -467,16 +527,14 @@ function ResourcesGrid({ footerNodeSelector = '.gn-footer', containerSelector = '', scrollContainerSelector = '', - pagination, + pagination = true, disableDetailPanel, disableFilters, - filterPagePath = '/catalogue/#/search/filter', resourceCardActionsOrder = [ ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE, 'downloadResource' ], - onReplaceLocation, error, enableGeoNodeCardsMenuItems, detailsTabs = [], @@ -484,9 +542,13 @@ function ResourcesGrid({ facets, filters, setFilters, - ...props + showFilterForm: showFilterFormProp, + setShowFilterForm }, context) { + const showDetail = !isEmpty(resource); + const showFilterForm = showFilterFormProp && !showDetail; + const [_cardLayoutStyleState, setCardLayoutStyle] = useLocalStorage('layoutCardsStyle', defaultCardLayoutStyle); const cardLayoutStyleState = cardLayoutStyle || _cardLayoutStyleState; // Force style when `cardLayoutStyle` is configured @@ -534,46 +596,12 @@ function ResourcesGrid({ excludeQueryKeys: [] }); - const [_showFilterForm, setShowFilterForm] = useState(false); - const showDetail = !isEmpty(resource); - const showFilterForm = _showFilterForm && !showDetail; - const handleShowFilterForm = (show) => { - if (show && disableFilters) { - simulateAClick(getCataloguePath(filterPagePath)); - } else { - if (!isEmpty(resource)) { - const href = closeDetailPanelHref(); - simulateAClick(href); - } - setShowFilterForm(show); + if (!isEmpty(resource)) { + const href = closeDetailPanelHref(); + simulateAClick(href); } - }; - - const isCatalogPage = (pathname) => { - const isConfigPresent = !!props?.[`${pathname.replace('/', '')}Page`]; - - // to be a catalog page it should have configuration - return getCatalogPage(pathname) && isConfigPresent; - }; - - const getMatchPath = () => { - const pathname = location.pathname; - const matchedPath = [ - '/search', - '/search/filter', - '/detail/:pk', - '/detail/:resourceType/:pk', - '/:page' - ].find((path) => matchPath(pathname, { path, exact: true })); - return matchedPath; - }; - - const getUpdatedPathName = (pathname) => { - if (isEmpty(pathname)) { - return isCatalogPage(location.pathname) ? location.pathname : '/'; - } - return pathname; + setShowFilterForm(show); }; function handleUpdate(newParams, pathname) { @@ -581,7 +609,7 @@ function ResourcesGrid({ onSearch({ ...omit(query, ['page']), ...newParams - }, getUpdatedPathName(pathname)); + }, pathname ?? location.pathname); } function handleClear() { @@ -604,13 +632,8 @@ function ResourcesGrid({ }, [cardLayoutStyle]); useEffect(() => { - let pathname = location.pathname; - const initialize = (pathname === '/' - || !isEmpty(getMatchPath()) - || isCatalogPage(pathname)) && init; - - if (initialize) { - pathname = getUpdatedPathName(); + if (init) { + const pathname = location.pathname; onInit({ defaultQuery, pageSize, @@ -631,70 +654,33 @@ function ResourcesGrid({ } }, [init, isPaginated, location.pathname]); - const [top, setTop] = useState(0); - const [bottom, setBottom] = useState(0); - useEffect(() => { - if (!panel) { - const header = headerNodeSelector ? document.querySelector(headerNodeSelector) : null; - const navbar = navbarNodeSelector ? document.querySelector(navbarNodeSelector) : null; - const footer = footerNodeSelector ? document.querySelector(footerNodeSelector) : null; - const { height: headerHeight = 0 } = header?.getBoundingClientRect() || {}; - const { height: navbarHeight = 0 } = navbar?.getBoundingClientRect() || {}; - const { height: footerHeight = 0 } = footer?.getBoundingClientRect() || {}; - setTop(headerHeight + navbarHeight); - setBottom(footerHeight); - } - }, [width, height, panel]); - const { query } = url.parse(location.search, true); const queryFilters = getQueryFilters(query); - const detailNode = useRef(); - const filterFormNode = useRef(); - const { width: filterFormNodeWidth = 0 } = filterFormNode?.current?.getBoundingClientRect() || {}; - const { width: detailNodeWidth = 0 } = detailNode?.current?.getBoundingClientRect() || {}; - const filterFormWidth = showFilterForm ? filterFormNodeWidth : 0; - const detailWidth = showDetail ? detailNodeWidth : 0; - const panelsWidth = filterFormWidth + detailWidth; - const container = containerSelector ? document.querySelector(containerSelector) : null; - const { height: containerHeight } = container?.getBoundingClientRect() || {}; - useEffect(() => { - if (container && !panel) { - container.style.width = `calc(100% - ${panelsWidth}px)`; - container.style.marginLeft = `${filterFormWidth}px`; - } - }, [container, panelsWidth, filterFormWidth, panel]); - useEffect(() => { - if (!panel) { - const pathname = location.pathname; - const matchedPath = getMatchPath(); - if (matchedPath) { - const options = matchPath(pathname, { path: matchedPath, exact: true }); - !isCatalogPage(location.pathname) && onReplaceLocation('' + (location.search || '')); - switch (options.path) { - case '/search': - case '/detail/:pk': { - break; - } - case '/search/filter': { - handleShowFilterForm(true); - break; - } - case '/detail/:resourceType/:pk': { - const { query: locationQuery } = url.parse(location.search, true); - const search = url.format({ query: { - ...locationQuery, - d: `${options?.params?.pk};${options?.params?.resourceType}` - }}); - simulateAClick('#' + (search || '')); - break; - } - default: - break; - } - } - } - }, [location.pathname, panel]); + const { + container, + detailNode, + filterFormNode, + filterMenuNode, + footerPaginationNode, + stickyTop, + stickyBottom, + containerHeight, + panelsWidth, + filterFormWidth, + top, + bottom + } = useResourceGridLayout({ + headerNodeSelector, + navbarNodeSelector, + footerNodeSelector, + containerSelector, + showFilterForm, + showDetail, + width, + height, + panel + }); const filterForm = !disableFilters && (
} footer={
{error @@ -869,8 +859,9 @@ const ResourcesGridPlugin = connect( state => getMonitoredState(state, getConfigProp('monitorState')), state => state?.gnsearch?.error, getFacetsItems, - state => state?.gnsearch?.filters - ], (params, user, totalResources, loading, location, resource, monitoredState, error, facets, filters) => ({ + state => state?.gnsearch?.filters, + getShowFilterForm + ], (params, user, totalResources, loading, location, resource, monitoredState, error, facets, filters, showFilterForm) => ({ params, user, totalResources, @@ -880,14 +871,15 @@ const ResourcesGridPlugin = connect( monitoredState, error, facets, - filters + filters, + showFilterForm })), { onSearch: searchResources, onInit: setSearchConfig, - onReplaceLocation: replace, onGetFacets: getFacetItems, - setFilters: setFiltersAction + setFilters: setFiltersAction, + setShowFilterForm: showFilterFormAction } )(withResizeDetector(withPageConfig(ResourcesGrid))); diff --git a/geonode_mapstore_client/client/js/reducers/gnsearch.js b/geonode_mapstore_client/client/js/reducers/gnsearch.js index 112fbf968a..db818c4196 100644 --- a/geonode_mapstore_client/client/js/reducers/gnsearch.js +++ b/geonode_mapstore_client/client/js/reducers/gnsearch.js @@ -18,7 +18,8 @@ import { INCREASE_TOTAL_COUNT, SET_SEARCH_CONFIG, SET_FACET_ITEMS, - SET_FILTERS + SET_FILTERS, + SHOW_FILTER_FORM } from '@js/actions/gnsearch'; import { UPDATE_SINGLE_RESOURCE } from '@js/actions/gnresource'; @@ -132,6 +133,11 @@ function gnsearch(state = defaultState, action) { ...state, filters: {...state.filters, ...action.filters} }; + case SHOW_FILTER_FORM: + return { + ...state, + showFilterForm: !!action.show + }; default: return state; } diff --git a/geonode_mapstore_client/client/js/selectors/search.js b/geonode_mapstore_client/client/js/selectors/search.js index db3aab14fe..b35428be9c 100644 --- a/geonode_mapstore_client/client/js/selectors/search.js +++ b/geonode_mapstore_client/client/js/selectors/search.js @@ -42,3 +42,4 @@ export const getTotalResources = (state) => { }; export const getFacetsItems = state => state?.gnsearch?.facetItems; +export const getShowFilterForm = state => state?.gnsearch?.showFilterForm; diff --git a/geonode_mapstore_client/client/js/utils/AppRoutesUtils.js b/geonode_mapstore_client/client/js/utils/AppRoutesUtils.js index 5a0c47caa4..5d9d8b6dd2 100644 --- a/geonode_mapstore_client/client/js/utils/AppRoutesUtils.js +++ b/geonode_mapstore_client/client/js/utils/AppRoutesUtils.js @@ -186,10 +186,6 @@ export const CATALOGUE_ROUTES = [ name: 'catalogue', path: [ '/', - '/search/', - '/search/filter', - '/detail/:pk', - '/detail/:ctype/:pk', '/:page' ], component: appRouteComponentTypes.CATALOGUE diff --git a/geonode_mapstore_client/client/js/utils/__tests__/AppRoutesUtils-test.js b/geonode_mapstore_client/client/js/utils/__tests__/AppRoutesUtils-test.js index c1359ca585..50e71ea165 100644 --- a/geonode_mapstore_client/client/js/utils/__tests__/AppRoutesUtils-test.js +++ b/geonode_mapstore_client/client/js/utils/__tests__/AppRoutesUtils-test.js @@ -103,10 +103,6 @@ describe('Test App Routes Utils', () => { expect(mapViewerRoute.shouldNotRequestResources).toEqual(true); expect(catalogueRoute.path).toEqual([ '/', - '/search/', - '/search/filter', - '/detail/:pk', - '/detail/:ctype/:pk', '/:page' ]); expect(catalogueRoute.name).toEqual('catalogue'); diff --git a/geonode_mapstore_client/client/themes/geonode/less/_base.less b/geonode_mapstore_client/client/themes/geonode/less/_base.less index ded5f3dfaa..3251adb128 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_base.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_base.less @@ -167,9 +167,6 @@ body { z-index: 0; overflow: hidden; } - .gn-card-grid { - padding-top: 0; - } .gn-filters-menu { top: 0 !important; margin-bottom: 0; diff --git a/geonode_mapstore_client/client/themes/geonode/less/_card-grid.less b/geonode_mapstore_client/client/themes/geonode/less/_card-grid.less index 36eec773f8..8e0cba91bb 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_card-grid.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_card-grid.less @@ -50,7 +50,6 @@ // ************** .gn-card-grid { - padding-top: 20px; .gn-card-grid-container { position: relative; max-width: @gn-page-max-width; @@ -74,6 +73,7 @@ max-width: @gn-page-max-width; margin: auto; width: 100%; + align-content: flex-start; li { padding: 0; margin: 0; diff --git a/geonode_mapstore_client/client/themes/geonode/less/_home-container.less b/geonode_mapstore_client/client/themes/geonode/less/_home-container.less index 6a5560b75f..7663c6e196 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_home-container.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_home-container.less @@ -77,4 +77,5 @@ #gn-home-resources-grid { position: relative; + padding-bottom: 2rem; } diff --git a/geonode_mapstore_client/client/themes/geonode/less/_menu.less b/geonode_mapstore_client/client/themes/geonode/less/_menu.less index 1168b9b26c..9778fb3855 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_menu.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_menu.less @@ -259,6 +259,9 @@ nav.hide-navigation#gn-topbar { position: sticky; top: 0px; z-index: 10; + .gn-menu-container { + padding-top: 0.35rem; + } } .gn-action-navbar-title { diff --git a/geonode_mapstore_client/client/themes/geonode/less/_resources-grid.less b/geonode_mapstore_client/client/themes/geonode/less/_resources-grid.less index 1bdad57f97..8a7da31334 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_resources-grid.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_resources-grid.less @@ -131,6 +131,11 @@ max-width: 100%; } } + &:not(.gn-panel) { + .gn-card-list { + min-height: 100vh; + } + } .gn-card-grid-container { .gn-menu { .gn-menu-container { diff --git a/geonode_mapstore_client/templates/geonode-mapstore-client/snippets/brand_navbar.html b/geonode_mapstore_client/templates/geonode-mapstore-client/snippets/brand_navbar.html index 34e2cfb13b..dabfd90e47 100644 --- a/geonode_mapstore_client/templates/geonode-mapstore-client/snippets/brand_navbar.html +++ b/geonode_mapstore_client/templates/geonode-mapstore-client/snippets/brand_navbar.html @@ -81,6 +81,29 @@ } window.addEventListener('DOMContentLoaded', manageUrlChange); window.addEventListener('hashchange', manageUrlChange, false); + + window.addEventListener('mapstore:ready', function(event) { + const msAPI = event.detail; + msAPI.onAction('@@router/LOCATION_CHANGE', (action) => { + const hashPathname = action?.payload?.location?.pathname || ''; + const parts = hashPathname.split('/'); + const page = parts[parts.length - 1]; + // Remove previous highlighted menu + const menuHighlighted = document.querySelector('#gn-topbar .highlight-menu'); + if (menuHighlighted) { + menuHighlighted.classList.remove('highlight-menu'); + } + // add new highlight + // exclude number + if (page && isNaN(parseFloat(page))) { + const topBarMenu = document.querySelector(`#gn-topbar #${page}`); + if (topBarMenu) { + topBarMenu.classList.add('highlight-menu'); + } + } + }); + }); + })(); {% endblock extra_script %} \ No newline at end of file diff --git a/geonode_mapstore_client/templates/index.html b/geonode_mapstore_client/templates/index.html index 18ba072ca0..6542675f41 100644 --- a/geonode_mapstore_client/templates/index.html +++ b/geonode_mapstore_client/templates/index.html @@ -57,7 +57,7 @@ {% endblock %} - + {% block ms_scripts %} {% endblock %}