diff --git a/geonode_mapstore_client/client/js/actions/gnsearch.js b/geonode_mapstore_client/client/js/actions/gnsearch.js index 1d59545335..6d8e056d90 100644 --- a/geonode_mapstore_client/client/js/actions/gnsearch.js +++ b/geonode_mapstore_client/client/js/actions/gnsearch.js @@ -19,6 +19,8 @@ export const INCREASE_TOTAL_COUNT = 'GEONODE_INCREASE_TOTAL_COUNT'; export const SET_SEARCH_CONFIG = 'GEONODE_SET_SEARCH_CONFIG'; 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"; /** * Actions for GeoNode resource featured items @@ -112,9 +114,10 @@ export function setSearchConfig(config) { } -export function getFacetItems() { +export function getFacetItems(query) { return { - type: GET_FACET_ITEMS + type: GET_FACET_ITEMS, + query }; } export function setFacetItems(facetItems) { @@ -123,6 +126,18 @@ export function setFacetItems(facetItems) { facetItems }; } +export function getFacetFilters(facets) { + return { + type: GET_FACET_FILTERS, + facets + }; +} + +export function setFilters(filters) { + return {type: SET_FILTERS, + filters + }; +} export default { SEARCH_RESOURCES, diff --git a/geonode_mapstore_client/client/js/api/geonode/v2/index.js b/geonode_mapstore_client/client/js/api/geonode/v2/index.js index 6610906147..61b7762cab 100644 --- a/geonode_mapstore_client/client/js/api/geonode/v2/index.js +++ b/geonode_mapstore_client/client/js/api/geonode/v2/index.js @@ -20,9 +20,12 @@ import isArray from 'lodash/isArray'; import isString from 'lodash/isString'; import isObject from 'lodash/isObject'; import castArray from 'lodash/castArray'; +import omit from 'lodash/omit'; import get from 'lodash/get'; +import pick from 'lodash/pick'; +import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; import { getUserInfo } from '@js/api/geonode/user'; -import { setFilterById } from '@js/utils/SearchUtils'; import { ResourceTypes, availableResourceTypes, setAvailableResourceTypes } from '@js/utils/ResourceUtils'; import { getConfigProp } from '@mapstore/framework/utils/ConfigUtils'; import { mergeConfigsPatch } from '@mapstore/patcher'; @@ -59,20 +62,12 @@ const MAPS = 'maps'; const GEOAPPS = 'geoapps'; const USERS = 'users'; const RESOURCE_TYPES = 'resource_types'; -const OWNERS = 'owners'; -const REGIONS = 'regions'; -const CATEGORIES = 'categories'; -const KEYWORDS = 'keywords'; const GROUPS = 'groups'; const UPLOADS = 'uploads'; const STATUS = 'status'; const EXECUTIONREQUEST = 'exectionRequest'; const FACETS = 'facets'; -function addCountToLabel(name, count) { - return `${name} (${count || 0})`; -} - export const setEndpoints = (data) => { endpoints = { ...endpoints, ...data }; }; @@ -118,7 +113,14 @@ function mergeCustomQuery(params, customQuery) { } return params; } - +export const getQueryParams = (params, customFilters) => { + const customQuery = customFilters + .filter(({ id }) => castArray(params?.f ?? []).indexOf(id) !== -1) + .reduce((acc, filter) => mergeCustomQuery(acc, filter.query || {}), {}) || {}; + return { + ...mergeCustomQuery(omit(params, "f"), customQuery) + }; +}; export const getResources = ({ q, pageSize = 20, @@ -128,13 +130,8 @@ export const getResources = ({ customFilters = [], ...params }) => { - - const customQuery = customFilters - .filter(({ id }) => castArray(f || []).indexOf(id) !== -1) - .reduce((acc, filter) => mergeCustomQuery(acc, filter.query || {}), {}) || {}; - const _mergeCustomQueryParams = mergeCustomQuery(params, customQuery); const _params = { - ..._mergeCustomQueryParams, + ...getQueryParams({...params, f}, customFilters), ...(q && { search: q, search_fields: ['title', 'abstract'] @@ -618,140 +615,6 @@ export const getFeaturedResources = (page = 1, page_size = 4) => { }).then(({data}) => data); }; -export const getCategories = ({ q, includes, page, pageSize, config, ...params }, filterKey = 'categories') => { - return axios.get(parseDevHostname(`${endpoints[CATEGORIES]}`), { - ...config, - params: { - page_size: pageSize || 9999, - page, - ...params, - ...(includes && {'filter{identifier.in}': includes}), - ...(q && { 'filter{identifier.icontains}': q }), - with_resources: "True" - } - }) - .then(({ data }) => { - const results = (data?.categories || []) - .map((result) => { - const selectOption = { - value: result.identifier, - label: addCountToLabel(result.gn_description || result.gn_description_en, result.count) - }; - const category = { - ...result, - selectOption - }; - setFilterById(filterKey + result.identifier, category); - return category; - }); - return { - results, - total: data.total, - isNextPageAvailable: !!data.links.next - }; - }); -}; - -export const getRegions = ({ q, includes, page, pageSize, config, ...params }, filterKey = 'regions') => { - return axios.get(parseDevHostname(`${endpoints[REGIONS]}`), { - ...config, - params: { - page_size: pageSize || 9999, - page, - ...params, - ...(includes && {'filter{name.in}': includes}), - ...(q && { 'filter{name.icontains}': q }), - with_resources: "True" - } - }) - .then(({ data }) => { - const results = (data?.regions || []) - .map((result) => { - const selectOption = { - value: result.name, - label: addCountToLabel(result.name, result.count) - }; - const region = { - ...result, - selectOption - }; - setFilterById(filterKey + result.name, region); - return region; - }); - return { - results, - total: data.total, - isNextPageAvailable: !!data.links.next - }; - }); -}; - -export const getOwners = ({ q, includes, page, pageSize, config, ...params }, filterKey = 'owners') => { - return axios.get(parseDevHostname(`${endpoints[OWNERS]}`), { - ...config, - params: { - page_size: pageSize || 9999, - page, - ...params, - ...(includes && {'filter{username.in}': includes}), - ...(q && { 'filter{username.icontains}': q }) - } - }) - .then(({ data }) => { - const results = (data?.owners || []) - .map((result) => { - const selectOption = { - value: result.username, - label: addCountToLabel(result.username, result.count) - }; - const owner = { - ...result, - selectOption - }; - setFilterById(filterKey + result.username, owner); - return owner; - }); - return { - results, - total: data.total, - isNextPageAvailable: !!data.links.next - }; - }); -}; - -export const getKeywords = ({ q, includes, page, pageSize, config, ...params }, filterKey = 'keywords') => { - return axios.get(parseDevHostname(`${endpoints[KEYWORDS]}`), { - ...config, - params: { - page_size: pageSize || 9999, - page, - ...params, - ...(includes && {'filter{slug.in}': includes}), - ...(q && { 'filter{slug.icontains}': q }) - } - }) - .then(({ data }) => { - const results = (data?.keywords || []) - .map((result) => { - const selectOption = { - value: result.slug, - label: addCountToLabel(result.slug, result.count) - }; - const keyword = { - ...result, - selectOption - }; - setFilterById(filterKey + result.slug, keyword); - return keyword; - }); - return { - results, - total: data.total, - isNextPageAvailable: !!data.links.next - }; - }); -}; - export const getCompactPermissionsByPk = (pk) => { return axios.get(parseDevHostname(`${endpoints[RESOURCES]}/${pk}/permissions`)) .then(({ data }) => data); @@ -902,37 +765,71 @@ export const getResourceByTypeAndByPk = (type, pk) => { } }; -export const getFacetItemsByFacetName = ({ name: facetName, style, filterKey }, { config, ...params }) => { - return axios.get(`${parseDevHostname(endpoints[FACETS])}/${facetName}`, { ...config, params }).then(({data}) => { - const {page: _page = 0, items = [], total, page_size: size} = data?.topics ?? {}; +export const getFacetItemsByFacetName = ({ name: facetName, style, filterKey, filters, setFilters}, { config, ...params }, customFilters) => { + const updatedParams = getQueryParams(params, customFilters); + return axios.get(`${parseDevHostname(endpoints[FACETS])}/${facetName}`, + { ...config, + params: updatedParams, + paramsSerializer + } + ).then(({data}) => { + const {page: _page = 0, items: _items = [], total, page_size: size} = data?.topics ?? {}; const page = Number(_page); const isNextPageAvailable = (Math.ceil(Number(total) / Number(size)) - (page + 1)) !== 0; - return { - page, - isNextPageAvailable, - items: items.map(({label, is_localized: isLocalized, key, count} = {})=> { - const item = { + + // Add filter values as item even when count is 0 + const filterKeys = Object.keys(updatedParams)?.map(key => `${key}${updatedParams[key]}`)?.filter(param => param?.includes(data?.filter)); + const filtersPresent = Object.values(pick(filters, filterKeys)); + + const items = isEmpty(_items) && !isEmpty(filtersPresent) + ? filtersPresent.map(item => ({ + ...(item.labelId ? {labelId: item.labelId} : {label: item.label}), + type: "filter", + count: 0, + filterKey: item.filterKey ?? filterKey, + filterValue: isNil(item.filterValue) ? String(item.key) : String(item.filterValue), + style + })) + : _items.map(({label, is_localized: isLocalized, key, count} = {})=> { + return { type: "filter", - ...(isLocalized ? { label } : { labelId: label }), + ...(!isNil(isLocalized) && !isLocalized ? { labelId: label } : { label }), // TODO remove when api send isLocalized for all facets response count, filterKey, filterValue: String(key), style }; - setFilterById(filterKey + key, item); - return item; - }) + }); + + // Update filters + setFilters(items.map((item) => ({[item.filterKey + item.filterValue]: item})).reduce((f, c) => ({...f, ...c}), {})); + + return { + page, + isNextPageAvailable, + items }; }); }; -export const getFacetItems = () => { +export const getFacetsByKey = (facet, filterParams) => { return axios - .get(parseDevHostname(endpoints[FACETS])) - .then(({ data } = {}) => + .get(parseDevHostname(endpoints[FACETS] + `/${facet}`), {params: {...filterParams}, paramsSerializer}) + .then(({ data } = {}) => data?.topics); +}; + +export const getFacetItems = (customFilters) => { + return axios + .get(parseDevHostname(endpoints[FACETS]), + { + params: { + include_config: true + } + } + ).then(({ data } = {}) => data?.facets?.map((facet) => ({ ...facet, - loadItems: getFacetItemsByFacetName + loadItems: (...args) => getFacetItemsByFacetName(...args, customFilters) })) || [] ).catch(() => []); }; @@ -962,10 +859,6 @@ export default { updateMap, getMapByPk, getMapsByPk, - getCategories, - getRegions, - getOwners, - getKeywords, getCompactPermissionsByPk, updateCompactPermissionsByPk, deleteResource, diff --git a/geonode_mapstore_client/client/js/components/Accordion/Accordion.jsx b/geonode_mapstore_client/client/js/components/Accordion/Accordion.jsx index 5123a33d2a..20fbae4785 100644 --- a/geonode_mapstore_client/client/js/components/Accordion/Accordion.jsx +++ b/geonode_mapstore_client/client/js/components/Accordion/Accordion.jsx @@ -45,7 +45,8 @@ const Accordion = ({ identifier, content, loadItems, - items + items, + query }) => { const isMounted = useIsMounted(); @@ -73,7 +74,7 @@ const Accordion = ({ .finally(()=> isMounted(() => setLoading(false))); } } - }, [isExpanded]); + }, [isExpanded, query]); return (
@@ -103,7 +104,8 @@ Accordion.propTypes = { identifier: PropTypes.string, content: PropTypes.func, loadItems: PropTypes.func, - items: PropTypes.array + items: PropTypes.array, + query: PropTypes.object }; Accordion.defaultProps = { diff --git a/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsInfo.jsx b/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsInfo.jsx index c6075ddf29..6697c16863 100644 --- a/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsInfo.jsx +++ b/geonode_mapstore_client/client/js/components/DetailsPanel/DetailsInfo.jsx @@ -86,7 +86,7 @@ function DetailsInfoFields({ fields, formatHref }) { {(values) => values.map((value, idx) => { return field.href ? {value} - : {value.value} + : {value.value}; })} ); diff --git a/geonode_mapstore_client/client/js/components/FiltersForm/FilterItems.jsx b/geonode_mapstore_client/client/js/components/FiltersForm/FilterItems.jsx index 482ce59819..4515f1892d 100644 --- a/geonode_mapstore_client/client/js/components/FiltersForm/FilterItems.jsx +++ b/geonode_mapstore_client/client/js/components/FiltersForm/FilterItems.jsx @@ -15,7 +15,6 @@ import ReactSelect from 'react-select'; import Accordion from "@js/components/Accordion"; import SelectInfiniteScroll from '@js/components/SelectInfiniteScroll'; -import { getFilterLabelById, getFilterById } from '@js/utils/SearchUtils'; import localizedProps from '@mapstore/framework/components/misc/enhancers/localizedProps'; import withDebounceOnCallback from '@mapstore/framework/components/misc/enhancers/withDebounceOnCallback'; import { getMessageById } from '@mapstore/framework/utils/LocaleUtils'; @@ -80,12 +79,12 @@ function ExtentFilterWithDebounce({ } function FilterItem({ id, - suggestionsRequestTypes, values, onChange, extentProps, timeDebounce, - field + field, + filters }, { messages }) { @@ -128,6 +127,7 @@ function FilterItem({ const getLabelValue = (item) => item.labelId ? `${getMessageById(messages, item.labelId)} (${item.count})` : `${item.label || ''} (${item.count})`; + const getFilterById = (value) => filters?.[filterKey + value]; return ( {field.labelId ? getMessageById(messages, field.labelId) : field.label} { - const selectedFilter = getFilterById(filterKey, value); + const selectedFilter = getFilterById(value); return { value, label: selectedFilter ? getLabelValue(selectedFilter) : value @@ -150,6 +150,7 @@ function FilterItem({ }} loadOptions={({ q, ...params }) => field.loadItems({ ...params, + ...values, // filter queries ...(q && { topic_contains: q }), page: params.page - 1 }) @@ -176,29 +177,22 @@ function FilterItem({ label, placeholderId, description, - options, - suggestionsRequestKey + options } = field; - const key = `${id}-${formId || suggestionsRequestKey}`; - const filterKey = suggestionsRequestKey - ? suggestionsRequestTypes[suggestionsRequestKey]?.filterKey - : `filter{${formId}.in}`; + const key = `${id}-${formId}`; + const filterKey = `filter{${formId}.in}`; - const currentValues = castArray(suggestionsRequestKey - ? values[suggestionsRequestTypes[suggestionsRequestKey]?.filterKey] || [] - : values[filterKey] || []); + const currentValues = values[filterKey] || []; - const optionsProp = suggestionsRequestKey - ? { loadOptions: suggestionsRequestTypes[suggestionsRequestKey]?.loadOptions } - : { options: options.map(option => ({ value: option, label: option })) }; - const Select = suggestionsRequestKey ? SelectInfiniteScroll : SelectSync; + const optionsProp = { options: options?.map(option => ({ value: option, label: option })) }; + const getFilterLabelById = (value) => filters?.[filterKey + value]?.selectOption?.label || filters?.[filterKey + value]?.label; return ( -