diff --git a/README.md b/README.md index 8e60b13..5f2e813 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Decidim - Octree Participatory democracy on a robust and open source solution

+

Decidim - Octree Participatory democracy on a robust and open source solution

Roadmap | Presentation | diff --git a/app/cells/decidim/geo/content_blocks/geo_maps_cell.rb b/app/cells/decidim/geo/content_blocks/geo_maps_cell.rb index c74066a..62cb075 100644 --- a/app/cells/decidim/geo/content_blocks/geo_maps_cell.rb +++ b/app/cells/decidim/geo/content_blocks/geo_maps_cell.rb @@ -42,7 +42,7 @@ def insert_map images: { not_geolocated: ActionController::Base.helpers.asset_pack_path("media/images/not-geolocated.svg") }, - filters: @options[:filters] || [], + filters: (@options[:filters] || []).concat([{ timeFilter: { time: "active" } }]), map_config: { lat: geo_config[:latitude], lng: geo_config[:longitude], @@ -99,8 +99,10 @@ def geo_i18n "decidim_geo.filters.type.only_assemblies": t("decidim.geo.filters.type.only_assemblies"), "decidim_geo.filters.type.only_proposals": t("decidim.geo.filters.type.only_proposals"), "decidim_geo.filters.type.only_meetings": t("decidim.geo.filters.type.only_meetings"), - "decidim_geo.filters.type.only_debates": t("decidim.geo.filters.type.only_debates") - + "decidim_geo.filters.type.only_debates": t("decidim.geo.filters.type.only_debates"), + "decidim_geo.filters.empty_message": t("decidim.geo.filters.empty.message"), + "decidim_geo.filters.empty_reset_button": t("decidim.geo.filters.empty.reset_button"), + "decidim_geo.actions.view": t("decidim.geo.actions.view") } end diff --git a/app/models/decidim/debates/debate.rb b/app/models/decidim/debates/debate.rb index 36e8c0f..e34d837 100644 --- a/app/models/decidim/debates/debate.rb +++ b/app/models/decidim/debates/debate.rb @@ -25,7 +25,7 @@ class Debate < Debates::ApplicationRecord include Decidim::Endorsable include Decidim::Randomable - if Decidim.version == "0.27.4" + if Decidim.version >= "0.27.4" include Decidim::DownloadYourData include Decidim::FilterableResource end @@ -59,7 +59,7 @@ class Debate < Debates::ApplicationRecord ) } - scope_search_multi :with_any_state, [:open, :closed] if Decidim.version == "0.27.4" + scope_search_multi :with_any_state, [:open, :closed] if Decidim.version >= "0.27.4" def self.log_presenter_class_for(_log) Decidim::Debates::AdminLog::DebatePresenter diff --git a/app/packs/src/decidim/geo/api/getFirstGeoDataSource.js b/app/packs/src/decidim/geo/api/getFirstGeoDataSource.js index 3015071..6d2bf22 100644 --- a/app/packs/src/decidim/geo/api/getFirstGeoDataSource.js +++ b/app/packs/src/decidim/geo/api/getFirstGeoDataSource.js @@ -1,21 +1,20 @@ -import {_getGeoDataSource, _getGeoDataSourceIds} from "./queries"; +import { _getGeoDataSource, _getGeoDataSourceIds } from "./queries"; -export const getFirstGeoDataSource = async (params = {}, fetchAll = true) => { - if (!params.variables) { - params.variables = {}; - } - const apiQuery = fetchAll ? _getGeoDataSource : _getGeoDataSourceIds; - let page; - try { - page = await apiQuery(params); - } catch (error) { - console.error(error); - throw error; - } - if (!page) return { nodes: [], hasMore: false, endCursor: "" }; - const { hasNextPage = false, endCursor = "" } = page?.pageInfo || {}; - return { nodes: page.nodes, hasMore: hasNextPage, after: endCursor }; - }; +export const getFirstGeoDataSource = async (params = {}, fetchComplete = true) => { + if (!params.variables) { + params.variables = {}; + } + const apiQuery = fetchComplete ? _getGeoDataSource : _getGeoDataSourceIds; + let page; + try { + page = await apiQuery(params); + } catch (error) { + console.error(error); + throw error; + } + if (!page) return { nodes: [], hasMore: false, endCursor: "" }; + const { hasNextPage = false, endCursor = "" } = page?.pageInfo || {}; + return { nodes: page.nodes, hasMore: hasNextPage, after: endCursor }; +}; - - export default getFirstGeoDataSource; \ No newline at end of file +export default getFirstGeoDataSource; diff --git a/app/packs/src/decidim/geo/api/getGeoDataSource.js b/app/packs/src/decidim/geo/api/getGeoDataSource.js index 4776317..796bb7c 100644 --- a/app/packs/src/decidim/geo/api/getGeoDataSource.js +++ b/app/packs/src/decidim/geo/api/getGeoDataSource.js @@ -1,4 +1,4 @@ -import {_getGeoDataSource, _getGeoDataSourceIds} from "./queries"; +import { _getGeoDataSource, _getGeoDataSourceIds } from "./queries"; const getGeoDataSource = async (params = {}, fetchAll = true) => { let results = []; @@ -33,4 +33,4 @@ const getGeoDataSource = async (params = {}, fetchAll = true) => { return { nodes: results }; }; - export default getGeoDataSource; \ No newline at end of file +export default getGeoDataSource; diff --git a/app/packs/src/decidim/geo/api/gql.js b/app/packs/src/decidim/geo/api/gql.js index a4d2435..636afc1 100644 --- a/app/packs/src/decidim/geo/api/gql.js +++ b/app/packs/src/decidim/geo/api/gql.js @@ -1,6 +1,6 @@ export const geoDatasource = ` query geoDatasourceQuery ($defaultLocale: String!, $locale: String!, $filters: [GeoDatasourceFilter!], $after: String) { - geoDatasource(filters: $filters, locale: $locale, after: $after, first: 500){ + geoDatasource(filters: $filters, locale: $locale, after: $after, first: 50){ pageInfo { hasPreviousPage startCursor @@ -45,7 +45,7 @@ export const geoDatasource = ` }`; export const geoDatasourceIds = ` query geoDatasourceQueryIds ($locale: String!, $filters: [GeoDatasourceFilter!], $after: String) { - geoDatasource(filters: $filters, locale: $locale, after: $after){ + geoDatasource(filters: $filters, locale: $locale, after: $after, first: 50){ pageInfo { hasPreviousPage startCursor diff --git a/app/packs/src/decidim/geo/api/index.js b/app/packs/src/decidim/geo/api/index.js index bdffd5d..e28ce05 100644 --- a/app/packs/src/decidim/geo/api/index.js +++ b/app/packs/src/decidim/geo/api/index.js @@ -1,6 +1,4 @@ +export { default as getGeoDataSource } from "./getGeoDataSource"; +export { default as getFirstGeoDataSource } from "./getFirstGeoDataSource"; -export {default as getGeoDataSource, } from './getGeoDataSource' -export {default as getFirstGeoDataSource, } from './getFirstGeoDataSource' - - -export {getGeoConfig, getGeoScopes} from './queries' +export { getGeoConfig, getGeoScopes } from "./queries"; diff --git a/app/packs/src/decidim/geo/api/makeQuery.js b/app/packs/src/decidim/geo/api/makeQuery.js index 4327d4e..76919e7 100644 --- a/app/packs/src/decidim/geo/api/makeQuery.js +++ b/app/packs/src/decidim/geo/api/makeQuery.js @@ -13,4 +13,4 @@ const makeQuery = return response.data[_responseKey]; }; - export default makeQuery \ No newline at end of file +export default makeQuery; diff --git a/app/packs/src/decidim/geo/api/queries.js b/app/packs/src/decidim/geo/api/queries.js index c3b362a..f99f857 100644 --- a/app/packs/src/decidim/geo/api/queries.js +++ b/app/packs/src/decidim/geo/api/queries.js @@ -3,4 +3,4 @@ export const _getGeoDataSource = makeQuery("geoDatasource"); export const _getGeoDataSourceIds = makeQuery("geoDatasourceIds", "geoDatasource"); export const getGeoConfig = makeQuery("geoConfig"); -export const getGeoScopes = makeQuery("geoScope"); \ No newline at end of file +export const getGeoScopes = makeQuery("geoScope"); diff --git a/app/packs/src/decidim/geo/models/GeoDatasourceNode.js b/app/packs/src/decidim/geo/models/GeoDatasourceNode.js index 5e2ca66..e6b8a86 100644 --- a/app/packs/src/decidim/geo/models/GeoDatasourceNode.js +++ b/app/packs/src/decidim/geo/models/GeoDatasourceNode.js @@ -27,6 +27,7 @@ export default class GeoDatasourceNode { pinPoint == `${this.data.id}` ) { this.marker.setStyle(this.selectedState); + this.marker.bringToFront(); } else { this.marker.setStyle(this.staledState); } diff --git a/app/packs/src/decidim/geo/models/GeoScope.js b/app/packs/src/decidim/geo/models/GeoScope.js index 1b2e9c4..f462141 100644 --- a/app/packs/src/decidim/geo/models/GeoScope.js +++ b/app/packs/src/decidim/geo/models/GeoScope.js @@ -20,7 +20,7 @@ export default class GeoScope { } isEmpty() { - if(this.data.geom === null) return true + if (this.data.geom === null) return true; const { points } = pointStore.getState(); const currentScopeId = this.id; return !points.find(({ scopeId }) => `${scopeId}` === `${currentScopeId}`); @@ -90,7 +90,7 @@ export default class GeoScope { ); } scopeClickHandler() { - this.select("layer") + this.select("layer"); } init() { const { map } = configStore.getState(); diff --git a/app/packs/src/decidim/geo/stores/dropdownFilterStore.js b/app/packs/src/decidim/geo/stores/dropdownFilterStore.js index 5b9e45d..a5662ba 100644 --- a/app/packs/src/decidim/geo/stores/dropdownFilterStore.js +++ b/app/packs/src/decidim/geo/stores/dropdownFilterStore.js @@ -46,6 +46,7 @@ const store = createStore( setFilter(name, value) { set((state) => ({ selectedFilters: { + ...state.defaultFilters, ...state.selectedFilters, [`${name}`]: value || state.defaultFilters[`${name}`] } diff --git a/app/packs/src/decidim/geo/stores/filterStore.js b/app/packs/src/decidim/geo/stores/filterStore.js index 66e42a1..74571a5 100644 --- a/app/packs/src/decidim/geo/stores/filterStore.js +++ b/app/packs/src/decidim/geo/stores/filterStore.js @@ -42,7 +42,7 @@ const store = createStore( const activeFilters = _.sortBy(filters, [sortingIteratee]); set((state) => { if (_.isEqual(state.activeFilters, activeFilters)) return {}; - return { activeFilters: activeFilters }; + return { activeFilters }; }); }, resetFilters: () => { @@ -84,11 +84,13 @@ const store = createStore( const timeFilterMatch = filters.find( ({ timeFilter = undefined }) => timeFilter ); + if (!timeFilterMatch) return defaultFilters.GeoTimeFilter; const timeFilter = timeFilterMatch.timeFilter.time; if (timeFilter === "past") return "only_past"; if (timeFilter === "active") return "only_active"; if (timeFilter === "future") return "only_future"; + if (timeFilter === "all") return "all"; return defaultFilters.GeoTimeFilter; case "GeoType": const typeFilterMatch = filters.find( diff --git a/app/packs/src/decidim/geo/stores/pointStore.js b/app/packs/src/decidim/geo/stores/pointStore.js index 64d43ab..3d2dfb4 100644 --- a/app/packs/src/decidim/geo/stores/pointStore.js +++ b/app/packs/src/decidim/geo/stores/pointStore.js @@ -27,8 +27,8 @@ const store = createStore( _lastFilter: "", _lastResponse: [], scopeForId: (scopeId) => { - const scope = get().scopes.find(({ data }) => `${data.id}` === `${scopeId}`); - if(!scope || scope.isEmpty()) return null; + const scope = get().scopes.find(({ data }) => `${data.id}` === `${scopeId}`); + if (!scope || scope.isEmpty()) return null; return scope; }, clearCache: () => { @@ -44,16 +44,19 @@ const store = createStore( fetchAll: async (filters = []) => { const { points: fetchedPoints } = store.getState(); if (fetchedPoints.length > 0) return; + const locale = configStore.getState().locale; const defaultLocale = configStore.getState().defaultLocale; set(({ isLoading }) => ({ isLoading: isLoading + 1 })); + const filterWithoutTime = filters.filter( + (f) => typeof f.timeFilter === "undefined" + ); const firstData = await getFirstGeoDataSource( { - variables: { filters, locale, defaultLocale } + variables: { filters: filterWithoutTime, locale, defaultLocale } }, true ); - set(() => ({ points: firstData.nodes.map(mapNodeToPoint).filter(Boolean) })); const scopes = await getGeoScopes({ @@ -77,7 +80,11 @@ const store = createStore( if (firstData.hasMore) { const data = await getGeoDataSource( { - variables: { filters: filters, locale: locale, after: firstData.after } + variables: { + filters: filterWithoutTime, + locale: locale, + after: firstData.after + } }, true ); @@ -122,7 +129,6 @@ const store = createStore( _lastResponse: filteredPoints, isLoading: isLoading - 1 })); - console.log(ids.nodes.length, "LAST RESPONSE", filteredPoints) return filteredPoints; } })) diff --git a/app/packs/src/decidim/geo/ui/Drawer.js b/app/packs/src/decidim/geo/ui/Drawer.js index a2fda79..1f81147 100644 --- a/app/packs/src/decidim/geo/ui/Drawer.js +++ b/app/packs/src/decidim/geo/ui/Drawer.js @@ -9,165 +9,169 @@ import { meetings as meetingDetails, fallback as fallbackDetails } from "./Drawe const createSkeletonItem = () => L.DomUtil.create("li", "decidimGeo__drawer__listCard"); export default class Drawer { - constructor(parent) { - // View - this.cardList = null; - this.loadingDOM = null; - this.parent = parent - } + constructor(parent) { + // View + this.cardList = null; + this.loadingDOM = null; + this.parent = parent; + } - isEmpty() { - return false; - } + isEmpty() { + return false; + } - loadingDom() { - if (this._loadingDOM) return this._loadingDOM; - this._loadingDOM = L.DomUtil.create( - "div", - createClasses("decidimGeo__drawer__listCardLoader") - ); - this._loadingDOM.appendChild(createSkeletonItem()); - this._loadingDOM.appendChild(createSkeletonItem()); - this._loadingDOM.appendChild(createSkeletonItem()); - return this._loadingDOM; - } + loadingDom() { + if (this._loadingDOM) return this._loadingDOM; + this._loadingDOM = L.DomUtil.create( + "div", + createClasses("decidimGeo__drawer__listCardLoader") + ); + this._loadingDOM.appendChild(createSkeletonItem()); + this._loadingDOM.appendChild(createSkeletonItem()); + this._loadingDOM.appendChild(createSkeletonItem()); + return this._loadingDOM; + } - repaintList(points) { - points.map(({ menuItem }) => { - this.cardList.appendChild(menuItem); - }); - } - repaintEmpty() { - const emptyContainer = L.DomUtil.create( - "li", - "decidimGeo__emptyDrawer__container", - this.cardList - ); - const emptyParagraph = L.DomUtil.create( - "p", - "decidimGeo__emptyDrawer__paragraph", - emptyContainer - ); - emptyParagraph.textContent = "No data with these filters"; - const emptyAction = L.DomUtil.create( - "button", - "decidimGeo__emptyDrawer__button", - emptyContainer - ); - emptyAction.textContent = "Reset filter"; - emptyAction.onclick = this.emptyActionHandler.bind(this) - } - emptyActionHandler() { - const { resetFilters } = filterStore.getState(); - const { resetFilters: resetDropdownFilter } = dropdownFilterStore.getState(); - const { selectScope } = geoStore.getState(); - const { space_ids } = configStore.getState(); - const { scopeForId } = pointStore.getState(); + repaintList(points) { + points.map(({ menuItem }) => { + this.cardList.appendChild(menuItem); + }); + } + repaintEmpty() { + const i18n = this.i18n(); + const emptyContainer = L.DomUtil.create( + "li", + "decidimGeo__emptyDrawer__container", + this.cardList + ); + const emptyParagraph = L.DomUtil.create( + "p", + "decidimGeo__emptyDrawer__paragraph", + emptyContainer + ); + emptyParagraph.textContent = this.i18n()["decidim_geo.filters.empty_message"]; + const emptyAction = L.DomUtil.create( + "button", + "decidimGeo__emptyDrawer__button", + emptyContainer + ); + emptyAction.textContent = this.i18n()["decidim_geo.filters.empty_reset_button"]; + emptyAction.onclick = this.emptyActionHandler.bind(this); + } - resetFilters(); - resetDropdownFilter(); - const scopes = space_ids.map((scope) => scopeForId(scope)).filter(Boolean); - if (scopes.length === 1) { - const scope = scopeForId(scopes[0]); - if (scope) { - selectScope(scope); - scope.repaint(); - } - } else { - selectScope(null); - } - this.repaint(); + i18n() { + return this.config().i18n; + } - } - repaintLoading() { - const loadingElement = this.loadingDom(); - this.cardList.appendChild(loadingElement); - } - repaintDetail({ data: node }) { - const listCard = L.DomUtil.create( - "li", - "decidimGeo__drawer__listCard", - this.cardList - ); - listCard.onclick = () => { - location.href = node.link; - };; + config() { + return configStore.getState(); + } + emptyActionHandler() { + const { resetFilters } = filterStore.getState(); + const { resetFilters: resetDropdownFilter } = dropdownFilterStore.getState(); + const { selectScope } = geoStore.getState(); + const { space_ids } = configStore.getState(); + const { scopeForId } = pointStore.getState(); - const info = L.DomUtil.create( - "div", - "decidimGeo__drawer__listCardInfo decidimGeo__drawer__listCardInfo--large", - listCard - ); - switch (node.type) { - case "Decidim::Meetings::Meeting": - meetingDetails(info, node); - break; - default: - fallbackDetails(info, node); - break; + resetFilters(); + resetDropdownFilter(); + const scopes = space_ids.map((scope) => scopeForId(scope)).filter(Boolean); + if (scopes.length === 1) { + const scope = scopeForId(scopes[0]); + if (scope) { + selectScope(scope); + scope.repaint(); } - const viewBtn = L.DomUtil.create("a", "decidimGeo__drawer__viewBtn", listCard); - viewBtn.textContent = "View"; - viewBtn.href = node.link; - return listCard; + } else { + selectScope(null); } - repaint() { - L.DomUtil.empty(this.cardList); - const { isLoading, getFilteredPoints } = pointStore.getState(); - const { selectedPoint } = geoStore.getState(); - const pointsInMap = getFilteredPoints(); + this.repaint(); + } + repaintLoading() { + const loadingElement = this.loadingDom(); + this.cardList.appendChild(loadingElement); + } + repaintDetail({ data: node }) { + const listCard = L.DomUtil.create( + "li", + "decidimGeo__drawer__listCard", + this.cardList + ); + listCard.onclick = () => { + location.href = node.link; + }; - if (isLoading) { - return this.repaintLoading(); - } - if (selectedPoint) { - return this.repaintDetail(selectedPoint); - } + const info = L.DomUtil.create( + "div", + "decidimGeo__drawer__listCardInfo decidimGeo__drawer__listCardInfo--large", + listCard + ); + switch (node.type) { + case "Decidim::Meetings::Meeting": + meetingDetails(info, node); + break; + default: + fallbackDetails(info, node); + break; + } + const viewBtn = L.DomUtil.create("a", "decidimGeo__drawer__viewBtn", listCard); + viewBtn.textContent = this.i18n()["decidim_geo.actions.view"]; + viewBtn.href = node.link; + return listCard; + } + repaint() { + L.DomUtil.empty(this.cardList); + const { isLoading, getFilteredPoints } = pointStore.getState(); + const { selectedPoint } = geoStore.getState(); + const pointsInMap = getFilteredPoints(); - if (pointsInMap.length === 0) { - this.repaintEmpty(); - } else { - this.repaintList(pointsInMap); - } + if (isLoading) { + return this.repaintLoading(); + } + if (selectedPoint) { + return this.repaintDetail(selectedPoint); + } - this.cardList.className = createClasses("decidimGeo__drawer__list", [ - isLoading && "loading", - this.isEmpty() && "empty" - ]); + if (pointsInMap.length === 0) { + this.repaintEmpty(); + } else { + this.repaintList(pointsInMap); } - onAdd() { - if (!this.parent ) - throw new Error("no this.parent "); - const { isLoading } = pointStore.getState(); - this.cardList = L.DomUtil.create( - "ul", - createClasses("decidimGeo__drawer__list", [ - isLoading && "loading", - this.isEmpty() && "empty" - ]), - this.parent - ); - L.DomEvent.disableClickPropagation(this.cardList); - L.DomEvent.disableScrollPropagation(this.cardList); + this.cardList.className = createClasses("decidimGeo__drawer__list", [ + isLoading && "loading", + this.isEmpty() && "empty" + ]); + } + onAdd() { + if (!this.parent) throw new Error("no this.parent "); + const { isLoading } = pointStore.getState(); + this.cardList = L.DomUtil.create( + "ul", + createClasses("decidimGeo__drawer__list", [ + isLoading && "loading", + this.isEmpty() && "empty" + ]), + this.parent + ); + L.DomEvent.disableClickPropagation(this.cardList); + L.DomEvent.disableScrollPropagation(this.cardList); - geoStore.subscribe( - (state) => [state.selectedPoint, state.selectedScope], - () => { - this.repaint(); - } - ); + geoStore.subscribe( + (state) => [state.selectedPoint, state.selectedScope], + () => { + this.repaint(); + } + ); - pointStore.subscribe( - (state) => [state.isLoading, state._lastResponse, state._lastFilter], - ([isLoading, lastResponse, lastFilter]) => { - if(isLoading) return; - console.log("lastResponse", lastResponse, lastFilter) - this.repaint(); - } - ); - this.repaint(); - return this.cardList; - } + pointStore.subscribe( + (state) => [state.isLoading, state._lastResponse, state._lastFilter], + ([isLoading, lastResponse, lastFilter]) => { + if (isLoading) return; + this.repaint(); + } + ); + this.repaint(); + return this.cardList; } - +} diff --git a/app/packs/src/decidim/geo/ui/FilterDropdown.js b/app/packs/src/decidim/geo/ui/FilterDropdown.js index 443ece3..843d56a 100644 --- a/app/packs/src/decidim/geo/ui/FilterDropdown.js +++ b/app/packs/src/decidim/geo/ui/FilterDropdown.js @@ -20,7 +20,10 @@ class FilterDropdown { ); this.title = L.DomUtil.create( "h6", - createClasses("decidimGeo__filterDropdown__title", [this.isOpen() && "active", this.isEmpty() && "empty"]), + createClasses("decidimGeo__filterDropdown__title", [ + this.isOpen() && "active", + this.isEmpty() && "empty" + ]), this.titleContainer ); this.countBadge = L.DomUtil.create( @@ -69,37 +72,33 @@ class FilterDropdown { ); filterStore.subscribe( (state) => [state.activeFilters], - () => - this.repaint() - + () => this.repaint() ); } titleHandler() { - if(this.isEmpty()) return; - this.toggle(); - this.repaint(); + if (this.isEmpty()) return; + this.toggle(); + this.repaint(); } - applyBtnHandler(){ - const { nextFilters, toggleOpen, applyNextFilters } = - dropdownFilterStore.getState(); - this.applyValues(nextFilters); - applyNextFilters(); - toggleOpen(); - this.repaint(); + applyBtnHandler() { + const { nextFilters, toggleOpen, applyNextFilters } = dropdownFilterStore.getState(); + this.applyValues(nextFilters); + applyNextFilters(); + toggleOpen(); + this.repaint(); } - resetBtnHandler(){ - const { - resetFilters: resetDropdownFilter, - toggleOpen, - defaultFilters - } = dropdownFilterStore.getState(); - resetDropdownFilter(); - this.applyValues(defaultFilters); - toggleOpen(); + resetBtnHandler() { + const { + resetFilters: resetDropdownFilter, + toggleOpen, + defaultFilters + } = dropdownFilterStore.getState(); + resetDropdownFilter(); + this.applyValues(defaultFilters); + toggleOpen(); } isEmpty() { - const {_lastResponse} = pointStore.getState() - return _lastResponse.length < 2 + return false; } defaultFilterFor(name) { const { toFilterOptions, defaultFilters } = filterStore.getState(); @@ -152,87 +151,141 @@ class FilterDropdown { } applyValues(filters) { + const defaultDropdownValues = { + GeoShowFilter: "all", + GeoTimeFilter: "only_active", + GeoType: "all" + }; if (!filters) { - filters = { - GeoShowFilter: "all", - GeoTimeFilter: "all", - GeoType: "all" - }; + filters = defaultDropdownValues; } + filters = { ...defaultDropdownValues, ...filters }; const { setFilters, activeFilters, defaultFilters } = filterStore.getState(); - const newFilters = activeFilters.filter((filter) => { - const [filterName] = Object.keys(filter); - return !["resourceTypeFilter", "timeFilter", "geoencodedFilter"].includes( - filterName - ); - }); + let newFilters = [...activeFilters]; + const withoutGeoShowFilter = (filters) => + filters.filter((f) => { + const [filterName] = Object.keys(f); + return filterName !== "geoencodedFilter"; + }); + const withoutTimeFilter = (filters) => + filters.filter((f) => { + const [filterName] = Object.keys(f); + return filterName !== "timeFilter"; + }); + const withoutTypeFilter = (filters) => + filters.filter((f) => { + const [filterName] = Object.keys(f); + return filterName !== "resourceTypeFilter"; + }); switch (filters.GeoShowFilter || defaultFilters.GeoShowFilter) { case "all": + newFilters = withoutGeoShowFilter(newFilters); break; case "only_geoencoded": - newFilters.push({ - geoencodedFilter: { geoencoded: true } - }); + newFilters = [ + ...withoutGeoShowFilter(newFilters), + { + geoencodedFilter: { geoencoded: true } + } + ]; break; case "only_virtual": - newFilters.push({ - geoencodedFilter: { geoencoded: false } - }); + newFilters = [ + ...withoutGeoShowFilter(newFilters), + { + geoencodedFilter: { geoencoded: false } + } + ]; break; } + switch (filters.GeoTimeFilter || defaultFilters.GeoTimeFilter) { case "all": - newFilters.push({ - timeFilter: { time: "all" } - }); + newFilters = [ + ...withoutTimeFilter(newFilters), + { + timeFilter: { time: "all" } + } + ]; break; case "only_past": - newFilters.push({ - timeFilter: { time: "past" } - }); + newFilters = [ + ...withoutTimeFilter(newFilters), + { + timeFilter: { time: "past" } + } + ]; break; case "only_active": - newFilters.push({ - timeFilter: { time: "active" } - }); + newFilters = [ + ...withoutTimeFilter(newFilters), + { + timeFilter: { time: "active" } + } + ]; break; case "only_future": - newFilters.push({ - timeFilter: { time: "future" } - }); + newFilters = [ + ...withoutTimeFilter(newFilters), + { + timeFilter: { time: "future" } + } + ]; break; } switch (filters.GeoType || defaultFilters.GeoType) { case "all": - newFilters.push({ - resourceTypeFilter: { resourceType: "all" } - }); + newFilters = [ + ...withoutTypeFilter(newFilters), + { + resourceTypeFilter: { resourceType: "all" } + } + ]; break; case "only_processes": - newFilters.push({ - resourceTypeFilter: { resourceType: "Decidim::ParticipatoryProcess" } - }); + newFilters = [ + ...withoutTypeFilter(newFilters), + { + resourceTypeFilter: { resourceType: "Decidim::ParticipatoryProcess" } + } + ]; break; case "only_assemblies": - newFilters.push({ resourceTypeFilter: { resourceType: "Decidim::Assembly" } }); + newFilters = [ + ...withoutTypeFilter(newFilters), + { + resourceTypeFilter: { resourceType: "Decidim::Assembly" } + } + ]; break; case "only_proposals": - newFilters.push({ - resourceTypeFilter: { resourceType: "Decidim::Proposals::Proposal" } - }); + newFilters = [ + ...withoutTypeFilter(newFilters), + { + resourceTypeFilter: { resourceType: "Decidim::Proposals::Proposal" } + } + ]; break; case "only_meetings": - newFilters.push({ - resourceTypeFilter: { resourceType: "Decidim::Meetings::Meeting" } - }); + newFilters = [ + ...withoutTypeFilter(newFilters), + { + resourceTypeFilter: { resourceType: "Decidim::Meetings::Meeting" } + } + ]; + break; case "only_debates": - newFilters.push({ - resourceTypeFilter: { resourceType: "Decidim::Debates::Debate" } - }); + newFilters = [ + ...withoutTypeFilter(newFilters), + { + resourceTypeFilter: { resourceType: "Decidim::Debates::Debate" } + } + ]; break; } + setFilters(newFilters); } @@ -307,17 +360,14 @@ class FilterDropdown { const { filterCount } = dropdownFilterStore.getState(); const { isOpen: isScopeOpen } = scopeDropdownStore.getState(); const badgeCount = filterCount(); - this.title.onclick = selectedPoint - ? () => {} - : this.titleHandler.bind(this); + this.title.onclick = selectedPoint ? () => {} : this.titleHandler.bind(this); this.countBadge.className = createClasses("decidimGeo__filterDropdown__counter", [ badgeCount === 0 && "hidden" ]); this.countBadge.textContent = badgeCount; this.title.className = createClasses("decidimGeo__filterDropdown__title", [ "button", - this.isOpen() && "active", - + this.isOpen() && "active" ]); this.titleContainer.className = createClasses( "decidimGeo__filterDropdown__titleContainer", diff --git a/app/packs/src/decidim/geo/ui/ScopeDropdown.js b/app/packs/src/decidim/geo/ui/ScopeDropdown.js index 5b40406..6238286 100644 --- a/app/packs/src/decidim/geo/ui/ScopeDropdown.js +++ b/app/packs/src/decidim/geo/ui/ScopeDropdown.js @@ -7,298 +7,312 @@ import FilterDropdown from "./FilterDropdown"; import scopeDropdownStore from "../stores/scopeDropdownStore"; import dropdownFilterStore from "../stores/dropdownFilterStore"; import memoryStore from "../stores/memoryStore"; +import _ from "lodash"; +export default class ScopeDropdown { + //View + constructor(parent) { + this.parent = parent; + this.menu = null; + this.heading = null; + this.toggleDrawer = null; + this.filterDropdown = null; + this.title = null; + this.titleTxt = null; + this.dropDownOptions = null; + this.resetBtn = null; + this.closeOverlay = null; + this.repaintResetBtn = null; -export default class ScopeDropdown { - //View - constructor(parent) { - this.parent = parent - this.menu = null; - this.heading = null; - this.toggleDrawer = null; - this.filterDropdown = null; - this.title = null; - this.titleTxt = null; - this.dropDownOptions = null; - this.resetBtn = null; - this.closeOverlay = null; - this.repaintResetBtn = null; + // Cache + this.cachedScopes = null; + scopeDropdownStore.subscribe( + (state) => [state.isOpen], + () => { + this.repaint(); + } + ); + } - // Cache - this.cachedScopes = null; - scopeDropdownStore.subscribe( - (state) => [state.isOpen], - () => { - this.repaint(); - } - ); + //Controlers + toggleOpen() { + scopeDropdownStore.getState().toggleOpen(); + } + scopes() { + if (this.cachedScopes !== null) return this.cachedScopes; + const { scopeForId, scopes } = pointStore.getState(); + const { space_ids } = configStore.getState(); + this.cachedScopes = scopes || []; + if (space_ids.length > 0) { + this.cachedScopes = space_ids.map((id) => scopeForId(id)).filter(Boolean) || []; } + return this.cachedScopes; + } + isEmpty() { + return this.scopes().length === 0; + } + hasOneOption() { + return this.scopes().length === 1; + } + activeScope() { + return geoStore.getState().selectedScope; + } + initMenuElements() { + const { isLoading } = pointStore.getState(); + const { isOpen } = scopeDropdownStore.getState(); - //Controlers - toggleOpen() { - scopeDropdownStore.getState().toggleOpen(); - } - scopes() { - if(this.cachedScopes !== null) return this.cachedScopes; - const { scopeForId, scopes } = pointStore.getState(); - const { space_ids } = configStore.getState(); - this.cachedScopes = scopes || [] - if (space_ids.length > 0){ - this.cachedScopes = space_ids.map((id) => scopeForId(id)).filter(Boolean) || []; - } - return this.cachedScopes; - } - isEmpty() { - return this.scopes().length === 0; - } - hasOneOption() { - return this.scopes().length === 1; - } - activeScope() { - return geoStore.getState().selectedScope; - } - initMenuElements() { - const { isLoading } = pointStore.getState(); - const { isOpen } = scopeDropdownStore.getState(); - - this.heading = L.DomUtil.create( - "div", - createClasses("decidimGeo__scopesDropdown__heading", [!isOpen && "closed"]), - this.menu - ); - const firstRow = L.DomUtil.create( - "div", - createClasses("decidimGeo__scopesDropdown__headingRow", ["filters"]), - this.heading - ); - const secondRow = L.DomUtil.create( - "div", - createClasses("decidimGeo__scopesDropdown__headingRow", ["drawer-toggle"]), - this.heading - ); - this.title = L.DomUtil.create( - "h6", - createClasses("decidimGeo__scopesDropdown__title", [ - isOpen ? "open" : "closed", - isLoading && "loading", - this.hasOneOption() && "alone", - this.isEmpty() && "empty", - ]), - firstRow - ); - this.title.onclick = this.titleClickHandler.bind(this) - this.titleTxt = L.DomUtil.create( - "span", - "decidimGeo__scopesDropdown__titleTxt", - this.title - ); - const titleIcn = L.DomUtil.create( - "span", - "decidimGeo__scopesDropdown__titleIcn decidimGeo__scopesDropdown__titleIcn--more", - this.title - ); - titleIcn.innerHTML = ` + this.heading = L.DomUtil.create( + "div", + createClasses("decidimGeo__scopesDropdown__heading", [!isOpen && "closed"]), + this.menu + ); + const firstRow = L.DomUtil.create( + "div", + createClasses("decidimGeo__scopesDropdown__headingRow", ["filters"]), + this.heading + ); + const secondRow = L.DomUtil.create( + "div", + createClasses("decidimGeo__scopesDropdown__headingRow", ["drawer-toggle"]), + this.heading + ); + this.title = L.DomUtil.create( + "h6", + createClasses("decidimGeo__scopesDropdown__title", [ + isOpen ? "open" : "closed", + isLoading && "loading", + this.hasOneOption() && "alone", + this.isEmpty() && "empty" + ]), + firstRow + ); + this.title.onclick = _.debounce(this.titleClickHandler.bind(this), 400, { + leading: false + }); + this.titleTxt = L.DomUtil.create( + "span", + "decidimGeo__scopesDropdown__titleTxt", + this.title + ); + const titleIcn = L.DomUtil.create( + "span", + "decidimGeo__scopesDropdown__titleIcn decidimGeo__scopesDropdown__titleIcn--more", + this.title + ); + titleIcn.innerHTML = ` `; - const titleBack = L.DomUtil.create( - "span", - "decidimGeo__scopesDropdown__titleIcn decidimGeo__scopesDropdown__titleIcn--back" - ); - this.title.prepend(titleBack); - titleBack.innerHTML = ` + const titleBack = L.DomUtil.create( + "span", + "decidimGeo__scopesDropdown__titleIcn decidimGeo__scopesDropdown__titleIcn--back" + ); + this.title.prepend(titleBack); + titleBack.innerHTML = ` `; - this.filterDropdown = new FilterDropdown(firstRow, this.parent); - this.dropDownOptions = L.DomUtil.create( - "ul", - createClasses("decidimGeo__scopesDropdown__list", [ - isOpen ? "open" : "closed", - this.isEmpty() && "empty" - ]), - this.parent - ); + this.filterDropdown = new FilterDropdown(firstRow, this.parent); + this.dropDownOptions = L.DomUtil.create( + "ul", + createClasses("decidimGeo__scopesDropdown__list", [ + isOpen ? "open" : "closed", + this.isEmpty() && "empty" + ]), + this.parent + ); - this.toggleDrawer = L.DomUtil.create( - "div", - createClasses("decidimGeo__scopesDropdown__drawerToggle", [ - isOpen ? "open" : "closed" - ]), - secondRow - ); - this.toggleDrawer.innerHTML = ` + this.toggleDrawer = L.DomUtil.create( + "div", + createClasses("decidimGeo__scopesDropdown__drawerToggle", [ + isOpen ? "open" : "closed" + ]), + secondRow + ); + this.toggleDrawer.innerHTML = ` -` - this.toggleDrawer.onclick = this.toggleDrawerHandler.bind(this) - this.repaint(); - } - toggleDrawerHandler(){ - configStore.setState(({isAsideOpen}) => ({isAsideOpen: !isAsideOpen})) - - } - titleClickHandler() { - if(this.isEmpty()) return false; - this.toggleOpen(); - this.repaint(); - } - repaintHeading() { - const { i18n } = configStore.getState(); - const { selectedPoint } = geoStore.getState(); - - if (selectedPoint) { - this.title.className += " decidimGeo__scopesDropdown__list--select-state"; - this.titleTxt.textContent = i18n["decidim_geo.filters.back"]; - this.title.onclick = this.goBackHandler.bind(this) - return; - } - if (this.isEmpty()) { - this.title.className += " decidimGeo__scopesDropdown__list--disabled"; - return; - } else { - this.title.className += " decidimGeo__scopesDropdown__list--with-options"; - if(this.hasOneOption()) - this.title.className += " decidimGeo__scopesDropdown__list--alone"; - - } +`; + this.toggleDrawer.onclick = this.toggleDrawerHandler.bind(this); + this.repaint(); + } + toggleDrawerHandler() { + configStore.setState(({ isAsideOpen }) => ({ isAsideOpen: !isAsideOpen })); + } + titleClickHandler() { + this.toggleOpen(); + this.repaint(); + } + repaintHeading() { + const { i18n } = configStore.getState(); + const { selectedPoint } = geoStore.getState(); - // Dropdown heading text - if (this.activeScope()) { - // specific scope - this.titleTxt.textContent = this.activeScope().name; - } else { - // all scopes - this.titleTxt.textContent = i18n["decidim_geo.scopes.dropdown"]; - } + if (selectedPoint) { + this.title.className += " decidimGeo__scopesDropdown__list--select-state"; + this.titleTxt.textContent = i18n["decidim_geo.filters.back"]; + this.title.onclick = this.goBackHandler.bind(this); + return; + } else { + this.title.onclick = _.debounce(this.titleClickHandler.bind(this), 400, { + leading: false + }); } - goBackHandler(){ - geoStore.getState().goBack(); + if (this.isEmpty()) { + this.title.className += " decidimGeo__scopesDropdown__list--disabled"; + return; + } else { + this.title.className += " decidimGeo__scopesDropdown__list--with-options"; + if (this.hasOneOption()) + this.title.className += " decidimGeo__scopesDropdown__list--alone"; } - itemClickHandler() { - const { popState } = memoryStore.getState(); - popState(); - this.toggleOpen(); - geoStore.getState().selectScope(null); - geoStore.getState().selectPoint(null); + + // Dropdown heading text + if (this.activeScope()) { + // specific scope + this.titleTxt.textContent = this.activeScope().name; + } else { + // all scopes + this.titleTxt.textContent = i18n["decidim_geo.scopes.dropdown"]; } - resetItem() { - if (this.resetBtn) return this.resetBtn; - const { i18n } = configStore.getState(); - const [btn, repaintBtn] = scopeDropdownItem({ - scopeId: "all", - label: i18n["decidim_geo.scopes.all"], - onClick: this.itemClickHandler.bind(this) - }); - this.resetBtn = btn; - this.repaintResetBtn = repaintBtn; - return this.resetBtn; + } + goBackHandler() { + geoStore.getState().goBack(); + } + itemClickHandler() { + const { popState } = memoryStore.getState(); + popState(); + this.toggleOpen(); + geoStore.getState().selectScope(null); + geoStore.getState().selectPoint(null); + } + resetItem() { + if (this.resetBtn) return this.resetBtn; + const { i18n } = configStore.getState(); + const [btn, repaintBtn] = scopeDropdownItem({ + scopeId: "all", + label: i18n["decidim_geo.scopes.all"], + onClick: this.itemClickHandler.bind(this) + }); + this.resetBtn = btn; + this.repaintResetBtn = repaintBtn; + return this.resetBtn; + } + repaintOptions() { + const scopes = this.scopes(); + // Dropdown options + L.DomUtil.empty(this.dropDownOptions); + if (this.isEmpty()) { + return; } - repaintOptions() { - const scopes = this.scopes(); - // Dropdown options - L.DomUtil.empty(this.dropDownOptions); - if (this.isEmpty()) { - return; - } - // Add a "All Scope" menu item - this.dropDownOptions.appendChild(this.resetItem()); - this.repaintResetBtn(); - // Add all the other scopes - scopes.forEach((geoScope) => { - geoScope.menuItemRepaint(); - this.dropDownOptions.appendChild(geoScope.menuItem); - }); - } - repaintOpenClose() { - const { selectedPoint } = geoStore.getState(); - const { isOpen } = scopeDropdownStore.getState(); - const { isOpen: isFilterOpen } = dropdownFilterStore.getState(); + // Add a "All Scope" menu item + this.dropDownOptions.appendChild(this.resetItem()); + this.repaintResetBtn(); + // Add all the other scopes + scopes.forEach((geoScope) => { + geoScope.menuItemRepaint(); + this.dropDownOptions.appendChild(geoScope.menuItem); + }); + } + repaintOpenClose() { + const { selectedPoint } = geoStore.getState(); + const { isOpen } = scopeDropdownStore.getState(); + const { isOpen: isFilterOpen } = dropdownFilterStore.getState(); - // Dropdown backdrop open/close - this.title.className = createClasses("decidimGeo__scopesDropdown__title", [ - isOpen ? "open" : "closed", - !selectedPoint && this.isEmpty() && "empty", - !selectedPoint && this.hasOneOption() && "alone", - selectedPoint && "button", - isFilterOpen && "disabled" - ]); - this.dropDownOptions.className = createClasses("decidimGeo__scopesDropdown__list", [ - isOpen ? "open" : "closed", - this.isEmpty() && "empty", - selectedPoint && "hidden", - !selectedPoint && this.hasOneOption() && "alone" - ]); - this.heading.className = createClasses("decidimGeo__scopesDropdown__heading", [ - isOpen ? "open" : "closed" - ]); - this.closeOverlay.className = createClasses("decidimGeo__scopesDropdown_overlay", [ - isOpen ? "open" : "closed" - ]); - this.toggleDrawer.className = createClasses( - "decidimGeo__scopesDropdown__drawerToggle", - [isOpen ? "open" : "closed"] - ); + // Dropdown backdrop open/close + this.title.className = createClasses("decidimGeo__scopesDropdown__title", [ + isOpen ? "open" : "closed", + this.isEmpty() && "empty", + this.hasOneOption() && "alone", + selectedPoint && "button", + isFilterOpen && "disabled" + ]); + this.dropDownOptions.className = createClasses("decidimGeo__scopesDropdown__list", [ + isOpen ? "open" : "closed", + this.isEmpty() && "empty", + selectedPoint && "hidden", + this.hasOneOption() && "alone" + ]); + this.heading.className = createClasses("decidimGeo__scopesDropdown__heading", [ + isOpen ? "open" : "closed" + ]); + this.closeOverlay.className = createClasses("decidimGeo__scopesDropdown_overlay", [ + isOpen ? "open" : "closed" + ]); + this.toggleDrawer.className = createClasses( + "decidimGeo__scopesDropdown__drawerToggle", + [isOpen ? "open" : "closed"] + ); - if (this.menu) - this.menu.className = - "leaflet-control " + - createClasses("decidimGeo__scopesDropdown", [ - isOpen ? "open" : "closed", - this.isEmpty() && "empty", - selectedPoint && "hidden", - !selectedPoint && this.hasOneOption() && "alone" - ]); - } - repaint() { - this.repaintHeading(); - this.repaintOptions(); - this.repaintOpenClose(); - } - onAdd() { - this.menu = L.DomUtil.create( - "div", - "leaflet-control decidimGeo__scopesDropdown", - this.parent - ); - L.DomEvent.disableClickPropagation(this.menu); - L.DomEvent.disableScrollPropagation(this.menu); - this.closeOverlay = L.DomUtil.create( - "span", - createClasses("decidimGeo__scopesDropdown_overlay", []) - ); - L.DomEvent.disableClickPropagation(this.closeOverlay); - L.DomEvent.disableScrollPropagation(this.closeOverlay); - const [mapContainer] = document.getElementsByClassName("js-decidimgeo"); - this.closeOverlay.onclick = this.titleClickHandler.bind(this); - mapContainer.prepend(this.closeOverlay); + if (this.menu) + this.menu.className = + "leaflet-control " + + createClasses("decidimGeo__scopesDropdown", [ + isOpen ? "open" : "closed", + this.isEmpty() && "empty", + selectedPoint && "hidden", + this.hasOneOption() && "alone" + ]); + } + repaint() { + this.repaintHeading(); + this.repaintOptions(); + this.repaintOpenClose(); + } + onAdd() { + this.menu = L.DomUtil.create( + "div", + "leaflet-control decidimGeo__scopesDropdown", + this.parent + ); + L.DomEvent.disableClickPropagation(this.menu); + L.DomEvent.disableScrollPropagation(this.menu); + this.closeOverlay = L.DomUtil.create( + "span", + createClasses("decidimGeo__scopesDropdown_overlay", []) + ); + L.DomEvent.disableClickPropagation(this.closeOverlay); + L.DomEvent.disableScrollPropagation(this.closeOverlay); + const [mapContainer] = document.getElementsByClassName("js-decidimgeo"); + this.closeOverlay.onclick = _.debounce(this.titleClickHandler.bind(this), 400, { + leading: false + }); + mapContainer.prepend(this.closeOverlay); - // We we change the available scopes, repaint. - geoStore.subscribe( - (state) => [state.selectedScope], - ([_geoScope], [prevGeoScope]) => { - if (prevGeoScope) { - prevGeoScope.repaint(); - } - this.repaint(); + // We we change the available scopes, repaint. + geoStore.subscribe( + (state) => [state.selectedScope], + ([_geoScope], [prevGeoScope]) => { + if (prevGeoScope) { + prevGeoScope.repaint(); } - ); - pointStore.subscribe( - (state) => [state.scopes, state.isLoading], - ([scopes, isLoading]) => { - if (!scopes || scopes.length == 0 || isLoading) return; - this.repaint(); - } - ); - dropdownFilterStore.subscribe((state) => [state.isOpen], () => this.repaint()) - this.initMenuElements(); - this.repaint(); // first repaint + this.repaint(); + } + ); + geoStore.subscribe( + (state) => [state.selectedPoint], + () => { + this.repaint(); + } + ); + pointStore.subscribe( + (state) => [state.scopes, state.isLoading], + ([scopes, isLoading]) => { + if (!scopes || scopes.length == 0 || isLoading) return; + this.repaint(); + } + ); + dropdownFilterStore.subscribe( + (state) => [state.isOpen], + () => this.repaint() + ); + this.initMenuElements(); + this.repaint(); // first repaint - return this.menu; - } - reset() { - scopeDropdownStore.getState().close(); - } - close() { - scopeDropdownStore.getState().close(); - } + return this.menu; + } + reset() { + scopeDropdownStore.getState().close(); + } + close() { + scopeDropdownStore.getState().close(); } +} diff --git a/app/packs/src/decidim/geo/ui/aside.js b/app/packs/src/decidim/geo/ui/aside.js index 82ca101..048bbf9 100644 --- a/app/packs/src/decidim/geo/ui/aside.js +++ b/app/packs/src/decidim/geo/ui/aside.js @@ -11,13 +11,16 @@ const createControl = (position) => children: [], container: null, repaint() { - const { isAsideOpen } = configStore.getState(); + const { isAsideOpen, map, mapReady } = configStore.getState(); this.container.className = "leaflet-control " + createClasses("decidimGeo__aside", [isAsideOpen ? "open" : "closed"]); const [mapContainer] = document.getElementsByClassName("js-decidimgeo"); if (!mapContainer) throw new Error("Can not find .js-decidimgeo element in DOM"); mapContainer.setAttribute("data-fill", isAsideOpen ? "stretch" : "truncated"); + if (mapReady) { + setTimeout(() => map.invalidateSize(), 64); + } }, onAdd(map) { this.container = L.DomUtil.create( @@ -27,7 +30,7 @@ const createControl = (position) => this.children.map((ChildKlass) => { const comp = new ChildKlass(this.container); comp.onAdd(map); - }) + }); geoStore.subscribe( (state) => [state.selectedScope, state.selectedPoint], () => this.repaint() @@ -47,7 +50,7 @@ async function aside(children) { const asideComponent = new controlKlass(); children.map((child) => { asideComponent.children.push(child); - }) + }); map.addControl(asideComponent); } diff --git a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_decidimGeo.scss b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_decidimGeo.scss index b372b16..8a4b996 100644 --- a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_decidimGeo.scss +++ b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_decidimGeo.scss @@ -12,7 +12,7 @@ .js-decidimgeo.decidimgeo__map { display: flex; min-height: 640px; - height: 90vh; + height: 80vh; width: 100%; max-width: 100%; max-height: 100vh; diff --git a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_drawer_list.scss b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_drawer_list.scss index f160730..7856e93 100644 --- a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_drawer_list.scss +++ b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_drawer_list.scss @@ -1,10 +1,10 @@ .decidimGeo__container .js-decidimgeo.decidimgeo__map { .decidimGeo__drawer__list { @include decidimGeo__drawerContainer; - min-height: 400px; + min-height: 210px; margin-left: 16px; - transform: translateY(31px); + transform: translateY(20px); padding: 4px 16px; background: #f0f0f0; box-shadow: 0px -1px 0px 0px #e0e0e0 inset; diff --git a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_filter_dropdown.scss b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_filter_dropdown.scss index 6d05f66..79eba24 100644 --- a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_filter_dropdown.scss +++ b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_filter_dropdown.scss @@ -7,13 +7,15 @@ height: auto; max-height: 400px; border-top: #cccccc 1px solid; + border-bottom: #cccccc 1px solid; + overflow: auto; list-style: none; transition: 0.3s; background: white; margin: 0; padding: 0; - margin-top: 59px; + margin-top: 48px; margin-left: 16px; margin-bottom: 0; padding-bottom: 10px; diff --git a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mixins.scss b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mixins.scss index 724ad50..881b385 100644 --- a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mixins.scss +++ b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mixins.scss @@ -1,7 +1,7 @@ @mixin decidimGeo__drawerContainer { width: 299px; height: auto; - max-height: 75vh; + max-height: 70vh; border-top: #cccccc 1px solid; overflow-y: auto; list-style: none; diff --git a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mobile.scss b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mobile.scss index ba8bb90..b70fe5c 100644 --- a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mobile.scss +++ b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_mobile.scss @@ -113,6 +113,7 @@ max-height: unset; padding: 0; border-top-width: 0; + border-bottom-width: 0; margin: 0; position: relative; @@ -247,6 +248,16 @@ padding: 8px; position: relative; } + .decidimGeo__filterDropdown__field { + width: auto; + & > label, + select { + flex: 1; + } + select.decidimGeo__filterDropdown__select { + text-align: left; + } + } .decidimGeo__scopesDropdown__list { z-index: 3000; transform: translateY(calc(-100% - 23px)); @@ -280,7 +291,7 @@ min-width: 100vw; background: #e0e0e0; margin: 0; - padding: 16px 16px 0 16px; + padding: 16px 16px 96px 16px; transform: translateY(0); max-height: 0; min-height: 0; diff --git a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_scope_dropdown.scss b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_scope_dropdown.scss index 5898782..adf44ea 100644 --- a/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_scope_dropdown.scss +++ b/app/packs/stylesheets/decidim/geo/geo/_decidim_geo_scope_dropdown.scss @@ -4,11 +4,12 @@ position: absolute; z-index: 9999; background: white; - margin-top: 60px; - width: 206px; - margin-left: 17px; + margin-top: 48px; + width: 300px; + margin-left: 15px; max-height: 75vh; border-top: #cccccc 1px solid; + border-bottom: #cccccc 1px solid; overflow-y: auto; list-style: none; &--closed { @@ -106,7 +107,6 @@ & .decidimGeo__scopesDropdown__titleIcn { display: none; margin-top: 3px; - } &--button { padding-top: 0; diff --git a/app/packs/stylesheets/decidim/geo/vendor/_leaflet.scss b/app/packs/stylesheets/decidim/geo/vendor/_leaflet.scss index 89b1112..bd5bd5d 100644 --- a/app/packs/stylesheets/decidim/geo/vendor/_leaflet.scss +++ b/app/packs/stylesheets/decidim/geo/vendor/_leaflet.scss @@ -177,7 +177,7 @@ float: right; } .leaflet-top .leaflet-control { - margin-top: 28px; + margin-top: 16px; } .leaflet-bottom .leaflet-control { margin-bottom: 104px; diff --git a/config/locales/decidim-geo.en.yml b/config/locales/decidim-geo.en.yml index 69be5cf..693a854 100644 --- a/config/locales/decidim-geo.en.yml +++ b/config/locales/decidim-geo.en.yml @@ -75,8 +75,9 @@ en: scopes: dropdown: "Scopes" all: "All Scopes" + actions: + view: View mobile: - open: "Open Map" open_fullscreen: "Open the map in fullscreen" close_fullscreen: "Close Map" filters: @@ -84,6 +85,9 @@ en: button: "Filters" reset_button: "Reset" apply_button: "Apply" + empty: + message: "No data with these filters" + reset_button: "Reset filters" geo: label: Show all: All diff --git a/config/locales/decidim-geo.fr.yml b/config/locales/decidim-geo.fr.yml index 5b7a59b..8c1127c 100644 --- a/config/locales/decidim-geo.fr.yml +++ b/config/locales/decidim-geo.fr.yml @@ -75,22 +75,27 @@ fr: scopes: dropdown: "Régions" all: "Toutes les régions" + actions: + view: Parcourir mobile: open: "Ouvrir la carte" - open_fullscreen: "Open the map in fullscreen" - close_fullscreen: "Close Map" + open_fullscreen: "Ouvrir en plein écran" + close_fullscreen: "Fermer" filters: back: "Retour" - button: "Filtrer" - reset_button: "Rétablir" - apply_button: "Appliquer" + button: "Filtres" + reset_button: "Annuler" + apply_button: "Afficher" + empty: + message: "Aucun résultat" + reset_button: "Retour" geo: label: Localisation all: Tout only_geoencoded: "Éléments localisés" only_virtual: "Autres éléments" time: - label: "Par temps" + label: "Période" all: "Tout" only_past: "Passé" only_active: "Actif" @@ -102,7 +107,7 @@ fr: only_assemblies: "Assemblées" only_proposals: "Propositions" only_meetings: "Réunions" - only_debates: "Debates" + only_debates: "Débats" components: geo: name: Géo diff --git a/docker-compose.yml b/docker-compose.yml index ecbc15d..ac82b51 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,11 +32,13 @@ services: networks: - private - default + pg: image: postgis/postgis:14-3.4-alpine restart: always volumes: - pg_data:/var/lib/postgresql/data + - .dump/:/tmp/dump environment: - POSTGRES_PASSWORD=insecure-password - POSTGRES_USER=decidim diff --git a/lib/decidim/api/geo_datasource_connection.rb b/lib/decidim/api/geo_datasource_connection.rb new file mode 100644 index 0000000..3bcc496 --- /dev/null +++ b/lib/decidim/api/geo_datasource_connection.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Decidim + module Geo + class GeoDatasourceConnection < GraphQL::Pagination::Connection + def cursor_for(item) + return nil if item.nil? + + Base64.strict_encode64("#{item.model_name}##{item.id}") + end + + def page_size + @first_value || max_page_size + end + + def results + @finished = false + (search_results, finished) = Rails.cache.fetch(resource_cache_key, expires_in: 12.hours) do + finished = false + search_results = [] + result_count = 0 + last_cursor = from_cursor(@after_value) + current_model_index = @items.find_index do |filter, _query| + filter.model_klass == last_cursor.first + end || 0 + last_id = last_cursor.last + while result_count < page_size && current_model_index < @items.size + resource_limit = page_size - result_count + resource = @items[current_model_index] + filter = filter_for(resource) + query = query_for(resource).where(filter.search_context.arel_table[:id].gt(last_id)).limit(resource_limit) + query_count = query.count + query_array = query.to_a + result_count += query_count + search_results.concat(query_array) + + next unless query_count < resource_limit + + current_model_index += 1 + last_id = -1 + if current_model_index == @items.size + finished = true + return search_results + end + end + [search_results, finished] + end + @finished = finished + search_results + end + + def total_result_count + @total_result_count ||= @items.map do |_filter, query| + query.count + end.sum + end + + def from_cursor(encoded_cursor) + return [0, -1] unless encoded_cursor + + matches = Base64.strict_decode64(encoded_cursor).split("#") + [ + matches.first, + matches.last.to_i + ] + end + + def filter_for(item) + item.first + end + + def query_for(item) + item.last + end + + def nodes + results + end + + def has_next_page + return total_result_count > page_size unless after_value + + last_cursor = cursor_for(@items.select { |_f, query| query.exists? }.last.last.last) + after_value != last_cursor && !@finished + end + + # Always return false because we're not implementing backwards pagination + def has_previous_page + false + end + + private + + def resource_cache_key + @resource_cache_key ||= begin + cache_key = "geo_datasource/#{Digest::MD5.hexdigest(arguments.to_json)}/#{(context[:current_user] && context[:current_user].id) || 0}" + last_updated = supported_filters.map do |filter| + last_updated = filter.order(updated_at: :desc).last + if last_updated + last_updated.updated_at + else + 1.year.from_now + end + end.max + "#{cache_key}/#{last_updated.to_i}" + end + end + + def supported_filters + ::Decidim::Geo.config.supported_filters.map do |filter| + filter.constantize.new(self).klass + end + end + end + end +end diff --git a/lib/decidim/api/geo_datasource_type.rb b/lib/decidim/api/geo_datasource_type.rb index aea280d..2f7b568 100644 --- a/lib/decidim/api/geo_datasource_type.rb +++ b/lib/decidim/api/geo_datasource_type.rb @@ -5,7 +5,7 @@ module Decidim module Geo class GeoDatasourceType < Decidim::Api::Types::BaseObject - description "A datasource for all objects" + description "A datasource for all decidim geo support" include Decidim::SanitizeHelper field :type, String, null: false field :id, ID, null: false @@ -13,9 +13,9 @@ class GeoDatasourceType < Decidim::Api::Types::BaseObject field :participatory_space_id, ID, null: true field :participatory_space_type, String, null: true field :title, Decidim::Core::TranslatedFieldType, "The title for this title", null: true - field :short_description, Decidim::Core::TranslatedFieldType, "The short description for this short description", + field :short_description, Decidim::Core::TranslatedFieldType, "The short description", null: true - field :description, Decidim::Core::TranslatedFieldType, "The description for this description", null: true + field :description, Decidim::Core::TranslatedFieldType, "The description", null: true field :banner_image, String, null: true field :coordinates, Decidim::Geo::GeoCoordinatesType, "Physical coordinates for this object", null: true field :start_time, Decidim::Core::DateTimeType, "The time this object starts", null: true @@ -30,35 +30,49 @@ def type delegate :id, to: :object def component_id - object.component.id if object.respond_to?(:component) + Rails.cache.fetch("#{cache_key_with_version}/component_id") do + object.component.id if component? + end end def participatory_space_id - return object.component.participatory_space_id if object.respond_to?(:component) + Rails.cache.fetch("#{cache_key_with_version}/participatory_space_id") do + object.component.participatory_space_id if component? + end end def participatory_space_type - return object.component.participatory_space_type if object.respond_to?(:component) + Rails.cache.fetch("#{cache_key_with_version}/participatory_space_type") do + object.component.participatory_space_type if component? + end end def link - Decidim::ResourceLocatorPresenter.new(object).path(anchor: "DecidimGeo") + Rails.cache.fetch("#{cache_key_with_version}/link") do + Decidim::ResourceLocatorPresenter.new(object).path + end end def title - return object.title if object.respond_to?(:title) - return object.name if object.respond_to?(:name) + Rails.cache.fetch("#{cache_key_with_version}/title") do + return object.title if object.respond_to?(:title) + return object.name if object.respond_to?(:name) + end end def short_description - return truncate_translated(object.short_description, 250) if object.respond_to?(:short_description) - return truncate_translated(object.body, 250) if object.respond_to?(:body) - return truncate_translated(object.description, 250) if object.respond_to?(:description) + Rails.cache.fetch("#{cache_key_with_version}/short_description") do + return truncate_translated(object.short_description, 250) if object.respond_to?(:short_description) + return truncate_translated(object.body, 250) if object.respond_to?(:body) + return truncate_translated(object.description, 250) if object.respond_to?(:description) + end end def description - return truncate_translated(object.body) if object.respond_to?(:body) - return truncate_translated(object.description) if object.respond_to?(:description) + Rails.cache.fetch("#{cache_key_with_version}/description") do + return truncate_translated(object.body) if object.respond_to?(:body) + return truncate_translated(object.description) if object.respond_to?(:description) + end end def truncate_translated(value, chars = 2800) @@ -84,63 +98,95 @@ def truncate_translated(value, chars = 2800) end def banner_image - return object.attached_uploader(:banner_image).url(only_path: true) if object.respond_to?(:banner_image) - return object.attachments.first.url if object.respond_to?(:attachments) && object.attachments.first + Rails.cache.fetch("#{cache_key_with_version}/banner_image") do + return object.attached_uploader(:banner_image).url(only_path: true) if object.respond_to?(:banner_image) + return object.attachments.first.url if object.respond_to?(:attachments) && object.attachments.first + end end def coordinates - { latitude: latitude, longitude: longitude } if has_coordinates? + Rails.cache.fetch("#{cache_key_with_version}/coordinates") do + { latitude: latitude, longitude: longitude } if has_coordinates? + end end def start_time - object.start_time if object.respond_to?(:start_time) + Rails.cache.fetch("#{cache_key_with_version}/start_time") do + object.start_time if object.respond_to?(:start_time) + end end def end_time - object.end_time if object.respond_to?(:end_time) + Rails.cache.fetch("#{cache_key_with_version}/end_time") do + object.end_time if object.respond_to?(:end_time) + end end def latitude - return location.latitude if has_geo_location? - - object.latitude + Rails.cache.fetch("#{cache_key_with_version}/latitude") do + if has_geo_location? + location.latitude + else + object.latitude + end + end end def longitude - return location.longitude if has_geo_location? - - object.longitude + Rails.cache.fetch("#{cache_key_with_version}/longitude") do + if has_geo_location? + location.longitude + else + object.longitude + end + end end def geom - RGeo::GeoJSON.encode(object.shapedata.geom) if object.respond_to?(:shapedata) && !object.shapedata.nil? + Rails.cache.fetch("#{cache_key_with_version}/geom") do + RGeo::GeoJSON.encode(object.shapedata.geom) if object.respond_to?(:shapedata) && !object.shapedata.nil? + end end def scope - object.scope if object.respond_to?(:scope) + Rails.cache.fetch("#{cache_key_with_version}/scope") do + object.scope if object.respond_to?(:scope) + end end def has_geo_location? - object.respond_to?(:decidim_geo_space_location) && object.decidim_geo_space_location + Rails.cache.fetch("#{cache_key_with_version}/has_geo_location") do + object.respond_to?(:decidim_geo_space_location) && object.decidim_geo_space_location + end end def location - return nil unless has_geo_location? - - object.decidim_geo_space_location + Rails.cache.fetch("#{cache_key_with_version}/location") do + object.decidim_geo_space_location if has_geo_location? + end end def has_coordinates? - ( - has_geo_location? && - !location.latitude.nil? && - !location.longitude.nil? - ) || ( - object.respond_to?(:latitude) && - object.respond_to?(:longitude) && - !object.latitude.nil? && - !object.longitude.nil? - ) + Rails.cache.fetch("#{cache_key_with_version}/has_coordinates") do + ( + has_geo_location? && + !location.latitude.nil? && + !location.longitude.nil? + ) || ( + object.respond_to?(:latitude) && + object.respond_to?(:longitude) && + !object.latitude.nil? && + !object.longitude.nil? + ) + end + end + + def component? + @_component ||= object.respond_to?(:component) + end + + def cache_key_with_version + @cache_key_with_version ||= object.cache_key_with_version end end end diff --git a/lib/decidim/api/query_extension.rb b/lib/decidim/api/query_extension.rb index f9fccaa..fb725fa 100644 --- a/lib/decidim/api/query_extension.rb +++ b/lib/decidim/api/query_extension.rb @@ -45,18 +45,23 @@ def self.included(type) def geo_datasource(**kwargs) locale = kwargs[:locale] || I18n.locale - ::Decidim::Geo::Api::GeoQuery.new( - current_organization, - current_user, - kwargs[:filters], - locale - ).results + ::Decidim::Geo::GeoDatasourceConnection.new( + ::Decidim::Geo::Api::GeoQuery.new( + current_organization, + current_user, + kwargs[:filters], + locale + ).results + ) end def geo_shapefiles(title: nil) return Decidim::Geo::Shapefile.where(title: title) if title.present? - Decidim::Geo::Shapefile.all + all_shapefiles = Decidim::Geo::Shapefile.all + Rails.cache.fetch("decidim_geo/#{all_shapefiles.cache_key_with_version}") do + all_shapefiles + end end def geo_shapedata(name: nil) @@ -70,9 +75,9 @@ def geo_config end def geo_scope(id: []) - return Decidim::Scope.all if id.empty? + return Decidim::Scope.includes(:shapedata).all if id.empty? - Decidim::Scope.where(id: id) + Decidim::Scope.includes(:shapedata).where(id: id) end private diff --git a/lib/decidim/decidim_geo/api.rb b/lib/decidim/decidim_geo/api.rb index 7b57f76..b9b9fd3 100644 --- a/lib/decidim/decidim_geo/api.rb +++ b/lib/decidim/decidim_geo/api.rb @@ -3,6 +3,7 @@ module Decidim module Geo autoload :QueryExtension, "decidim/api/query_extension" + autoload :GeoDatasourceConnection, "decidim/api/geo_datasource_connection" autoload :GeoShapefileType, "decidim/api/geo_shapefile_type" autoload :GeoShapedataType, "decidim/api/geo_shapedata_type" autoload :GeoConfigType, "decidim/api/geo_config_type" diff --git a/lib/decidim/decidim_geo/geo_queries/assembly_filter.rb b/lib/decidim/decidim_geo/geo_queries/assembly_filter.rb index e24060b..b181a69 100644 --- a/lib/decidim/decidim_geo/geo_queries/assembly_filter.rb +++ b/lib/decidim/decidim_geo/geo_queries/assembly_filter.rb @@ -12,15 +12,32 @@ def self.graphql_key :assembly_filter end - def apply_filters(assembly_ids) - return [] unless manifest # Not registered as Decidim participatory space. + def apply_filters(assemblies) + matches = scoped_by_geoencoded(scoped_by_time(assemblies)) + if assembly_filter.empty? + matches + else + matches.where(id: assembly_filter).or(matches.where(parent_id: assembly_filter)) + end + end - assemblies = Decidim::Assembly.visible_for(current_user).where(id: assembly_ids) - scoped_by_geoencoded(scoped_by_time(assemblies)) + def search_context + klass.visible_for(current_user) end private + def assembly_filter + @assembly_filter ||= begin + processes = filters.select { |f| f[:assembly_filter].present? } + if processes.empty? + [] + else + processes.collect { |f| f[:assembly_filter][:id] } + end + end + end + def scoped_by_geoencoded(assemblies) if !geoencode_filtered? assemblies.left_joins(:decidim_geo_space_location) @@ -45,12 +62,6 @@ def scoped_by_time(assemblies) assemblies end end - - def manifest - @manifest ||= Decidim.participatory_space_manifests.find do |manifest| - manifest.name == :assemblies - end - end end end end diff --git a/lib/decidim/decidim_geo/geo_queries/debate_filter.rb b/lib/decidim/decidim_geo/geo_queries/debate_filter.rb index f652fde..4177d13 100644 --- a/lib/decidim/decidim_geo/geo_queries/debate_filter.rb +++ b/lib/decidim/decidim_geo/geo_queries/debate_filter.rb @@ -8,15 +8,14 @@ def self.model_klass "Decidim::Debates::Debate" end - def apply_filters(debate_ids) - debates = Decidim::Debates::Debate.where(id: debate_ids).includes(:component, :scope) - scoped_by_geoencoded(scoped_by_time(debates)) + def apply_filters(debates) + scoped_by_geoencoded(scoped_by_time(debates.includes(:component, :scope))) end private def scoped_by_geoencoded(debates) - return [] if only_geoencoded? + return debates.where("1=0") if only_geoencoded? debates end diff --git a/lib/decidim/decidim_geo/geo_queries/generic_filter.rb b/lib/decidim/decidim_geo/geo_queries/generic_filter.rb index 8ce2e83..dc21200 100644 --- a/lib/decidim/decidim_geo/geo_queries/generic_filter.rb +++ b/lib/decidim/decidim_geo/geo_queries/generic_filter.rb @@ -6,7 +6,7 @@ module Api class GenericFilter extend Forwardable - def_delegators :@geo_query, :organization, :current_user, :locale, :filters + def_delegators :@geo_query, :organization, :current_user, :locale, :filters, :participatory_spaces attr_reader :geo_query, :options def initialize(geo_query) @@ -56,12 +56,80 @@ def participatory_space? ## # Take the given matches ids. + # Should be implemented in child classes. def apply_filters(_matches) raise "not implemented." end + def search_context + klass.all + end + + ## + # + def results(scope_ids = [], id = nil) + matches = if component? + search_context.joins(:component) + else + search_context + end + matches = apply_filters(matches) + + matches = if scope_ids.empty? + matches + else + filtered_by_scope(matches, scope_ids) + end + + matches = if component? && !participatory_spaces.empty? + if participatory_spaces.size == 1 + matches = matches.where(component: participatory_spaces.first) + else + first_participatory_space = participatory_spaces.first + filtered_matches = matches + match_space = matches.where(component: first_participatory_space) + # It's a component, filtered by space + participatory_spaces[1..].each do |ps| + matches = match_space.or(filtered_matches.where(component: ps)) + end + matches + end + matches + elsif participatory_space? && !participatory_spaces.empty? + # It's participatory space, filtered by spaces. + restrict_ids = participatory_spaces.select do |ps| + ps[:participatory_space_type] == model_klass + end.collect do |ps| + ps[:participatory_space_id] + end.flatten + + matches = if restrict_ids.empty? + matches + else + matches.where(id: restrict_ids) + end + else + matches + end + + matches = matches.where(id: id) unless id.nil? + matches + end + protected + def filtered_by_scope(matches, scope_ids) + return matches.where(decidim_scope_id: scope_ids) if participatory_space? + + matches = matches.where(decidim_scope_id: scope_ids) + spaces_in_scopes(scope_ids).each do |scope_params| + matches = matches.or( + matches.joins(:component).where(component: scope_params) + ) + end + matches + end + def extract_time_filter @extract_time_filter ||= filters.find { |f| f[:time_filter].present? } end @@ -87,6 +155,33 @@ def exclude_geoencoded? def geoencoded_filter @geoencoded_filter ||= filters.find { |f| f[:geoencoded_filter].present? } end + + private + + ## + # Get spaces types and ids in the scope + def spaces_in_scopes(scope_ids) + scope_ids + active_space_filters.map do |active_space| + [ + active_space.model_klass, + active_space.search_context.where(decidim_scope_id: scope_ids) + ] + end.select do |_model_klass, query| + query.exists? + end.map do |model_klass, query| + { + decidim_participatory_space_type: model_klass, + decidim_participatory_space_id: query.ids + } + end + end + + def active_space_filters + @active_space_filters ||= ::Decidim::Geo.config.supported_filters.map do |filter| + filter.constantize.new(geo_query) + end.select(&:participatory_space?) + end end end end diff --git a/lib/decidim/decidim_geo/geo_queries/geo_query.rb b/lib/decidim/decidim_geo/geo_queries/geo_query.rb index d76314c..cc9fd35 100644 --- a/lib/decidim/decidim_geo/geo_queries/geo_query.rb +++ b/lib/decidim/decidim_geo/geo_queries/geo_query.rb @@ -14,21 +14,46 @@ def initialize(organization, current_user, filters, locale) end def results - fetch_results(filtered_data) + fetch_results end def results_count - count_results(filtered_data) + count_results + end + + def participatory_spaces + @participatory_spaces ||= begin + matches = active_space_filters.map do |space_filter| + [ + space_filter.model_klass, + normalized_filters.select do |filter| + filter[space_filter.graphql_key].present? + end.collect { |filter| filter[space_filter.graphql_key][:id] } + ] + end.reject { |_model_klass, ids| ids.empty? }.map do |model_klass, ids| + { + participatory_space_type: model_klass, + participatory_space_id: model_klass.constantize.visible_for(current_user).where(id: ids).ids + } + end + + if matches.size.positive? + matches + else + default_participatory_spaces + end + end end private - def filtered_data - @filtered_data ||= if normalized_filters.present? - data_by_resource_type(query) - else - data_by_resource_type(nofilter_query) - end + def default_participatory_spaces + @default_participatory_spaces ||= active_space_filters.map do |space_filter| + { + participatory_space_type: space_filter.model_klass, + participatory_space_id: space_filter.klass.visible_for(current_user).ids + } + end end def normalized_filters @@ -43,76 +68,6 @@ def normalized_filters end end - def query_with_scope_contraints(search_params) - scopes = normalized_filters.select { |f| f[:scope_filter].present? } - if scopes.length.positive? - # Search only in a given scope - scope_ids = scopes.map do |scope| - scope[:scope_filter][:scope_id] - end - search_params = search_params.merge({ scope_ids: scope_ids }) - end - search_params - end - - # Participatory spaces filter (Assemblies, Processes, Conferences, Initiatives) - def query_space_filters - @query_space_filters ||= active_filters.select(&:participatory_space?).select do |filter| - key = filter.graphql_key - normalized_filters.any? { |f| f[key].present? } - end - end - - # Define class_name search. - # if resource_type is "all" => do nothing - # else if resource_type is defined => search the resource type everywhere - # else if we are filtering by space => exclude all the other kind of spaces in the filter. - def query_with_resource_type(search_params) - resource_type = normalized_filters.find { |f| f[:resource_type_filter].present? } - if resource_type - # Search only for a resource type - class_name = resource_type.resource_type_filter.resource_type - - search_params = search_params.merge({ class_name: class_name }) unless class_name == "all" - elsif query_space_filters.length.positive? - # Exclude all the spaces that are not included in the filter. - not_filtered_spaces = query_space_filters.select do |filter| - normalized_filters.find { |f| f[filter.graphql_key].present? }.nil? - end.map(&:model_klass) - class_name = supported_geo_components.reject do |k| - not_filtered_spaces.include?(k) - end - search_params = search_params.merge({ class_name: class_name }) - end - search_params - end - - def query - search_params = { locale: locale, class_name: supported_geo_components } - # Handle scope constraints - search_params = query_with_scope_contraints(search_params) - - # Handle resource type filter (ex: select only Meetings) - search_params = query_with_resource_type(search_params) - - # Execute the query - search_results = filtered_query_for(**search_params) - - # Bind results to the selected spaces. - query_space_filters.each do |space_filter| - graphql_key = space_filter.graphql_key - ids = normalized_filters.select { |f| f[graphql_key].present? }.map do |graphql_filter| - graphql_filter[graphql_key][:id] - end - # The results must be within the filtered space - search_results = search_results.where( - decidim_participatory_space_type: space_filter.model_klass, - decidim_participatory_space_id: ids - ) - end - search_results - end - def active_filters @active_filters ||= ::Decidim::Geo.config.supported_filters.map do |filter| filter.constantize.new(self) @@ -123,70 +78,57 @@ def supported_geo_components active_filters.map(&:model_klass) end - def nofilter_query - filtered_query_for(locale: locale, class_name: supported_geo_components) + def scope_ids_params + @scope_ids_params ||= normalized_filters.select { |f| f[:scope_filter].present? }.map do |filter| + filter[:scope_filter][:scope_id] + end end - def filtered_query_for(filter_options = {}) - class_name = filter_options[:class_name] - id = filter_options[:id] - term = filter_options[:term] - scope_ids = filter_options[:scope_ids] - locale = filter_options[:locale] - spaces = filter_options[:spaces] - query = { organization: organization, - locale: locale, - resource_type: class_name } - result_query = SearchableResource.where(query) - if scope_ids.present? - if spaces.present? - # If searching a scope AND a space, then should look in BOTH - # => scope || space. - space_query = query.dup - space_query.update(decidim_participatory_space: spaces) - result_query = result_query.or( - SearchableResource.where(space_query) - ) - end + def id_params + nil + end - result_query = result_query.and(SearchableResource.where(decidim_scope_id: scope_ids)) - elsif spaces.present? - result_query = result_query.where(decidim_participatory_space: spaces) if spaces.present? + def active_space_filters + @active_space_filters ||= active_filters.select(&:participatory_space?) + end + + def resource_filters + @resource_filters ||= begin + resource_type = normalized_filters.find { |f| f[:resource_type_filter].present? } + filters = if participatory_spaces + participatory_spaces_types = participatory_spaces.collect { |ps| ps[:participatory_space_type] } + active_filters.select { |filter| filter.component? || participatory_spaces_types.include?(filter.model_klass) } + else + active_filters + end + if resource_type + # Search only for a resource type + class_name = resource_type.resource_type_filter.resource_type + return filters if class_name == "all" + + filters.select do |filter| + filter.model_klass == class_name + end + else + filters + end end - result_query = result_query.where(resource_id: id) if id.present? - result_query = result_query.order("datetime DESC") - result_query = result_query.global_search(I18n.transliterate(term)) if term.present? - result_query end - def fetch_results(data) + def fetch_results # Apply resource specific filtering - active_filters.map do |filter| - matches = data[filter.model_klass] || [] - filter.apply_filters(matches) - end.flatten + resource_filters.map do |filter| + [filter, filter.results(scope_ids_params, id_params)] + end end - def count_results(data) + def count_results # Apply resource specific filtering - active_filters.map do |filter| - matches = data[filter.model_klass] || [] - filter.apply_filters(matches).count + resource_filters.map do |filter| + matches = filter.results(scope_ids_params, id_params) + matches.count end.sum end - - def data_by_resource_type(searchable_results) - results = {} - # Fetch resources, and store them by type - searchable_results.select("resource_id,resource_type").each do |resource| - if results[resource.resource_type].blank? - results[resource.resource_type] = - [] - end - results[resource.resource_type].push(resource.resource_id) - end - results - end end end end diff --git a/lib/decidim/decidim_geo/geo_queries/meeting_filter.rb b/lib/decidim/decidim_geo/geo_queries/meeting_filter.rb index 11546ca..ce5a038 100644 --- a/lib/decidim/decidim_geo/geo_queries/meeting_filter.rb +++ b/lib/decidim/decidim_geo/geo_queries/meeting_filter.rb @@ -8,15 +8,17 @@ def self.model_klass "Decidim::Meetings::Meeting" end - def apply_filters(meeting_ids) - meetings = Decidim::Meetings::Meeting.visible_for( + def apply_filters(meetings) + meetings = meetings.visible_for( current_user - ).where( - id: meeting_ids - ).includes(:component, :scope).left_joins(:attachments) + ) scoped_by_geoencoded(scoped_by_time(meetings)) end + def search_context + Decidim::Meetings::Meeting.joins(:component).where.not(component: { published_at: nil }).left_joins(:attachments) + end + private def scoped_by_geoencoded(meetings) @@ -30,23 +32,31 @@ def scoped_by_geoencoded(meetings) end def scoped_by_time(meetings) - meetings = Decidim::Meetings::Meeting.visible_for(current_user).where(id: meetings) case time_filter when "past" meetings.where("end_time < ?", Time.zone.now) + when "future" + meetings.where("start_time >= ?", Time.zone.now) when "active" + # Active meetings.where( # Meeting is happening "start_time <= ? AND end_time > ?", Time.zone.now, Time.zone.now ).or( # Meeting is about to start (now..in 15 days) - meetings.where("start_time > ? AND start_time < ?", Time.zone.now, 15.days.from_now) + meetings.where( + "start_time > ? AND start_time < ?", + Time.zone.now, + 15.days.from_now + ) ).or( # Meeting has just ended (15days ago ... now) - meetings.where("end_time > ? AND end_time < ?", 15.days.ago, Time.zone.now) + meetings.where( + "end_time > ? AND end_time < ?", + 15.days.ago, + Time.zone.now + ) ) - when "future" - meetings.where("start_time >= ?", Time.zone.now) else meetings end diff --git a/lib/decidim/decidim_geo/geo_queries/process_filter.rb b/lib/decidim/decidim_geo/geo_queries/process_filter.rb index 62a9268..864f509 100644 --- a/lib/decidim/decidim_geo/geo_queries/process_filter.rb +++ b/lib/decidim/decidim_geo/geo_queries/process_filter.rb @@ -12,43 +12,52 @@ def self.graphql_key :process_filter end - def apply_filters(process_ids) - return [] unless manifest # Not registered as Decidim participatory space. - - public_spaces = manifest.participatory_spaces.call(organization).public_spaces.where(id: process_ids) - return public_spaces if filters.empty? + def apply_filters(processes) + matches = scoped_by_geoencoded(scoped_by_time(processes)) + if process_filter.empty? + matches + else + matches.where(id: process_filter) + end + end - scoped_by_geoencoded(scoped_by_time(public_spaces)) + def search_context + klass.visible_for(current_user) end private - def scoped_by_geoencoded(public_spaces) + def process_filter + @process_filter ||= begin + processes = filters.select { |f| f[:process_filter].present? } + if processes.empty? + [] + else + processes.collect { |f| f[:process_filter][:id] } + end + end + end + + def scoped_by_geoencoded(processes) if !geoencode_filtered? - public_spaces.left_joins(:decidim_geo_space_location) + processes.left_joins(:decidim_geo_space_location) elsif only_geoencoded? - public_spaces.joins(:decidim_geo_space_location).where.not(decidim_geo_space_location: { latitude: nil }) + processes.joins(:decidim_geo_space_location).where.not(decidim_geo_space_location: { latitude: nil }) elsif exclude_geoencoded? - public_spaces.left_joins(:decidim_geo_space_location).where(decidim_geo_space_location: { latitude: nil }) + processes.left_joins(:decidim_geo_space_location).where(decidim_geo_space_location: { latitude: nil }) end end - def scoped_by_time(public_spaces) + def scoped_by_time(processes) case time_filter when "active" - public_spaces.active_spaces + processes.active_spaces when "future" - public_spaces.future_spaces + processes.future_spaces when "past" - public_spaces.past_spaces + processes.past_spaces else - public_spaces - end - end - - def manifest - @manifest ||= Decidim.participatory_space_manifests.find do |manifest| - manifest.name == :participatory_processes + processes end end end diff --git a/lib/decidim/decidim_geo/geo_queries/proposal_filter.rb b/lib/decidim/decidim_geo/geo_queries/proposal_filter.rb index 6808754..0574e6b 100644 --- a/lib/decidim/decidim_geo/geo_queries/proposal_filter.rb +++ b/lib/decidim/decidim_geo/geo_queries/proposal_filter.rb @@ -8,11 +8,15 @@ def self.model_klass "Decidim::Proposals::Proposal" end - def apply_filters(proposal_ids) - proposals = Decidim::Proposals::Proposal.where(id: proposal_ids).includes(:component, :scope).left_joins(:attachments).published + def apply_filters(proposals) + proposals = proposals.left_joins(:attachments) scoped_by_geoencoded(scoped_by_time(proposals)) end + def search_context + Decidim::Proposals::Proposal.published.joins(:component).where.not(component: { published_at: nil }) + end + private def scoped_by_geoencoded(proposals) @@ -22,6 +26,8 @@ def scoped_by_geoencoded(proposals) proposals.where.not(latitude: nil) elsif exclude_geoencoded? proposals.where(latitude: nil) + else + proposals end end diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..be2bab9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,551 @@ +{ + "name": "decidim_module_geo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "buffer": "^6.0.3", + "date-fns": "^3.0.0", + "graphql": "16.6.0", + "graphql-ws": "^5.12.1", + "leaflet": "1.9.2", + "leaflet-svgicon": "0.0.2", + "leaflet.fullscreen": "^3.0.2", + "leaflet.markercluster": "1.5.3", + "polylabel": "^1.1.0", + "shpjs": "4.0.4", + "zustand": "^4.4.7" + }, + "devDependencies": { + "prettier": "^3.1.0" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.17", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.17.tgz", + "integrity": "sha512-IPvU9A31qRCZ7lds/x+ksuK/UMndd0EASveAvCvEtFFKIZjZ+m/a4a0L7S28KEWoR5ka8526hlSghDo4Hrc2Hg==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", + "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/graphql": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", + "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-ws": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz", + "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/leaflet": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.2.tgz", + "integrity": "sha512-Kc77HQvWO+y9y2oIs3dn5h5sy2kr3j41ewdqCMEUA4N89lgfUUfOBy7wnnHEstDpefiGFObq12FdopGRMx4J7g==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet-svgicon": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/leaflet-svgicon/-/leaflet-svgicon-0.0.2.tgz", + "integrity": "sha512-9hGBLBHHcCSZAdVLwdiZbU2c/Z47eziDQslDrRQRcBNomEazH4NXvqY8egDMw+zGh/nBQub6jvTl1ty2nlEwmQ==", + "license": "ISC", + "dependencies": { + "src": "^1.1.2" + } + }, + "node_modules/leaflet.fullscreen": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/leaflet.fullscreen/-/leaflet.fullscreen-3.0.2.tgz", + "integrity": "sha512-m27waFVmwdrLGXjZw2L8b7w/W28EY+u7IGzK2x8K99XaPuzKbjI+/H1j0OMawLcRPZyDRh+39XpyLsvDwHUEoA==", + "license": "MIT" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "license": "ISC" + }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parsedbf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-1.1.1.tgz", + "integrity": "sha512-jndFmhcrzSAGCMccM4za+3bIRxqV6L2doQjYN8Xgz0kZUpyBT5I8Gs6Y6hL5GcO2rih9OBkPcLlx2uBoLi8R8Q==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.4.15", + "text-encoding-polyfill": "^0.6.7" + } + }, + "node_modules/polylabel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", + "integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==", + "license": "ISC", + "dependencies": { + "tinyqueue": "^2.0.3" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proj4": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz", + "integrity": "sha512-SasuTkAx8HnWQHfIyhkdUNJorSJqINHAN3EyMWYiQRVorftz9DHz650YraFgczwgtHOxqnfuDxSNv3C8MUnHeg==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.3.3" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/redis": { + "version": "4.6.15", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.15.tgz", + "integrity": "sha512-2NtuOpMW3tnYzBw6S8mbXSX7RPzvVFCA2wFJq9oErushO2UeBkxObk+uvo7gv7n0rhWeOj/IzrHO8TjcFlRSOg==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.17", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.6", + "@redis/search": "1.1.6", + "@redis/time-series": "1.0.5" + } + }, + "node_modules/redis-url": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/redis-url/-/redis-url-0.2.0.tgz", + "integrity": "sha512-4HyF3QI0vbmXAMRudLIxnaz/sUGzTEmHBV5swe2keie1ynCrJbCnraDmwrEncI6F0BQz8t2ivYrEZ0Wrq77Yjg==", + "dependencies": { + "redis": ">= 0.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/shpjs": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-4.0.4.tgz", + "integrity": "sha512-+IcS2DoiTGqAONUEN46ZociKGJ2ecs1EVwJuSqnAOkMafxWC8noO3X/zI959RCbqHGvBr1RM1bdk4jc7fYONVg==", + "license": "MIT", + "dependencies": { + "jszip": "^3.5.0", + "lie": "^3.0.1", + "lru-cache": "^2.7.0", + "parsedbf": "^1.1.0", + "proj4": "^2.1.4" + } + }, + "node_modules/src": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/src/-/src-1.1.2.tgz", + "integrity": "sha512-xOKk/hC+DMAWZVEu0zf/1y2vegH1dQD2W2jldxpFCAnavfrK8IPwECKw5ZDcxhd3+kmzaVyPECMAYVy2WDV9YQ==", + "license": "MIT", + "dependencies": { + "redis-url": "~0.2.0", + "underscore": "~1.6.0", + "uuid": "~1.4.1" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/text-encoding-polyfill": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz", + "integrity": "sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg==", + "license": "Unlicense" + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "license": "ISC" + }, + "node_modules/underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==" + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-1.4.2.tgz", + "integrity": "sha512-woV5Ei+GBJyrqMXt0mJ9p8/I+47LYKp/4urH76FNTMjl22EhLPz1tNrQufTsrFf/PYV/7ctSZYAK7fKPWQKg+Q==" + }, + "node_modules/wkt-parser": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz", + "integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==", + "license": "MIT" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.4.tgz", + "integrity": "sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 8aba064..3bb44ef 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -89,7 +89,7 @@ const config: Config = { items: [ { label: 'Documentation', - to: '//', + to: '/', }, ], }, diff --git a/yarn.lock b/yarn.lock index 0d21506..d253f5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4,12 +4,12 @@ "@redis/bloom@1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" + resolved "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz" integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== -"@redis/client@1.5.17": +"@redis/client@^1.0.0", "@redis/client@1.5.17": version "1.5.17" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.17.tgz#44d179f2b5b542754d6f218bb352bac3ccf150eb" + resolved "https://registry.npmjs.org/@redis/client/-/client-1.5.17.tgz" integrity sha512-IPvU9A31qRCZ7lds/x+ksuK/UMndd0EASveAvCvEtFFKIZjZ+m/a4a0L7S28KEWoR5ka8526hlSghDo4Hrc2Hg== dependencies: cluster-key-slot "1.1.2" @@ -18,32 +18,32 @@ "@redis/graph@1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea" + resolved "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz" integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw== "@redis/json@1.0.6": version "1.0.6" - resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.6.tgz#b7a7725bbb907765d84c99d55eac3fcf772e180e" + resolved "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz" integrity sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw== "@redis/search@1.1.6": version "1.1.6" - resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.6.tgz#33bcdd791d9ed88ab6910243a355d85a7fedf756" + resolved "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz" integrity sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw== "@redis/time-series@1.0.5": version "1.0.5" - resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad" + resolved "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz" integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg== base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -51,64 +51,69 @@ buffer@^6.0.3: cluster-key-slot@1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== core-util-is@~1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== date-fns@^3.0.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz" integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== generic-pool@3.9.0: version "3.9.0" - resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + resolved "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz" integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== graphql-ws@^5.12.1: version "5.16.0" - resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.16.0.tgz#849efe02f384b4332109329be01d74c345842729" + resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz" integrity sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A== -graphql@16.6.0: +"graphql@>=0.11 <=16", graphql@16.6.0: version "16.6.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" + resolved "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz" integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== iconv-lite@^0.4.15: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== immediate@~3.0.5: version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== inherits@~2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== isarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + jszip@^3.5.0: version "3.10.1" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== dependencies: lie "~3.3.0" @@ -118,51 +123,58 @@ jszip@^3.5.0: leaflet-svgicon@0.0.2: version "0.0.2" - resolved "https://registry.yarnpkg.com/leaflet-svgicon/-/leaflet-svgicon-0.0.2.tgz#ec590365d01e66e2e72613c6eb6ad93862055f01" + resolved "https://registry.npmjs.org/leaflet-svgicon/-/leaflet-svgicon-0.0.2.tgz" integrity sha512-9hGBLBHHcCSZAdVLwdiZbU2c/Z47eziDQslDrRQRcBNomEazH4NXvqY8egDMw+zGh/nBQub6jvTl1ty2nlEwmQ== dependencies: src "^1.1.2" leaflet.fullscreen@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/leaflet.fullscreen/-/leaflet.fullscreen-3.0.2.tgz#b37e66ae0d16a139a98a1641bec4a9403915fe2d" + resolved "https://registry.npmjs.org/leaflet.fullscreen/-/leaflet.fullscreen-3.0.2.tgz" integrity sha512-m27waFVmwdrLGXjZw2L8b7w/W28EY+u7IGzK2x8K99XaPuzKbjI+/H1j0OMawLcRPZyDRh+39XpyLsvDwHUEoA== leaflet.markercluster@1.5.3: version "1.5.3" - resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056" + resolved "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz" integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA== -leaflet@1.9.2: +leaflet@^1.3.1, leaflet@1.9.2: version "1.9.2" - resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.2.tgz#168b6c6ef1d4d1e8409bde2c4ad050c249c4dbe6" + resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.2.tgz" integrity sha512-Kc77HQvWO+y9y2oIs3dn5h5sy2kr3j41ewdqCMEUA4N89lgfUUfOBy7wnnHEstDpefiGFObq12FdopGRMx4J7g== lie@^3.0.1, lie@~3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^2.7.0: version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" integrity sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ== mgrs@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/mgrs/-/mgrs-1.0.0.tgz#fb91588e78c90025672395cb40b25f7cd6ad1829" + resolved "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz" integrity sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA== pako@~1.0.2: version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== parsedbf@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/parsedbf/-/parsedbf-1.1.1.tgz#f3af1a1edc432a4e3dba76bcf2c1858f2ebf8066" + resolved "https://registry.npmjs.org/parsedbf/-/parsedbf-1.1.1.tgz" integrity sha512-jndFmhcrzSAGCMccM4za+3bIRxqV6L2doQjYN8Xgz0kZUpyBT5I8Gs6Y6hL5GcO2rih9OBkPcLlx2uBoLi8R8Q== dependencies: iconv-lite "^0.4.15" @@ -170,32 +182,39 @@ parsedbf@^1.1.0: polylabel@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/polylabel/-/polylabel-1.1.0.tgz#9483e64fc7a12a49f43e07e7a06752214ed2a8e7" + resolved "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz" integrity sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA== dependencies: tinyqueue "^2.0.3" prettier@^3.1.0: version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz" integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== proj4@^2.1.4: version "2.11.0" - resolved "https://registry.yarnpkg.com/proj4/-/proj4-2.11.0.tgz#795a5790aed30a7535d6a4c5775c0ce2a763cc41" + resolved "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz" integrity sha512-SasuTkAx8HnWQHfIyhkdUNJorSJqINHAN3EyMWYiQRVorftz9DHz650YraFgczwgtHOxqnfuDxSNv3C8MUnHeg== dependencies: mgrs "1.0.0" wkt-parser "^1.3.3" +"react@^16.8.0 || ^17.0.0 || ^18.0.0", react@>=16.8: + version "18.3.1" + resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + readable-stream@~2.3.6: version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" @@ -208,14 +227,14 @@ readable-stream@~2.3.6: redis-url@~0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/redis-url/-/redis-url-0.2.0.tgz#1b7a2daccc3ea8264b1fb656c0d901d9ca9c5470" + resolved "https://registry.npmjs.org/redis-url/-/redis-url-0.2.0.tgz" integrity sha512-4HyF3QI0vbmXAMRudLIxnaz/sUGzTEmHBV5swe2keie1ynCrJbCnraDmwrEncI6F0BQz8t2ivYrEZ0Wrq77Yjg== dependencies: redis ">= 0.0.1" "redis@>= 0.0.1": version "4.6.15" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.15.tgz#b94599fbbd8279182b02f5bb34866c2a1556d71c" + resolved "https://registry.npmjs.org/redis/-/redis-4.6.15.tgz" integrity sha512-2NtuOpMW3tnYzBw6S8mbXSX7RPzvVFCA2wFJq9oErushO2UeBkxObk+uvo7gv7n0rhWeOj/IzrHO8TjcFlRSOg== dependencies: "@redis/bloom" "1.2.0" @@ -227,22 +246,22 @@ redis-url@~0.2.0: safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== setimmediate@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== shpjs@4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/shpjs/-/shpjs-4.0.4.tgz#8849032ba1a738e7a983cd9c508ba3097ad2717c" + resolved "https://registry.npmjs.org/shpjs/-/shpjs-4.0.4.tgz" integrity sha512-+IcS2DoiTGqAONUEN46ZociKGJ2ecs1EVwJuSqnAOkMafxWC8noO3X/zI959RCbqHGvBr1RM1bdk4jc7fYONVg== dependencies: jszip "^3.5.0" @@ -253,7 +272,7 @@ shpjs@4.0.4: src@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/src/-/src-1.1.2.tgz#78abdd1c08caca26cc6cf45bd580b56d93acfb7f" + resolved "https://registry.npmjs.org/src/-/src-1.1.2.tgz" integrity sha512-xOKk/hC+DMAWZVEu0zf/1y2vegH1dQD2W2jldxpFCAnavfrK8IPwECKw5ZDcxhd3+kmzaVyPECMAYVy2WDV9YQ== dependencies: redis-url "~0.2.0" @@ -262,54 +281,54 @@ src@^1.1.2: string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" text-encoding-polyfill@^0.6.7: version "0.6.7" - resolved "https://registry.yarnpkg.com/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz#4d27de0153e4c86eb2631ffd74c2f3f57969a9ec" + resolved "https://registry.npmjs.org/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz" integrity sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg== tinyqueue@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + resolved "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz" integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== underscore@~1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" integrity sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ== use-sync-external-store@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== util-deprecate@~1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== uuid@~1.4.1: version "1.4.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-1.4.2.tgz#453019f686966a6df83cdc5244e7c990ecc332fc" + resolved "https://registry.npmjs.org/uuid/-/uuid-1.4.2.tgz" integrity sha512-woV5Ei+GBJyrqMXt0mJ9p8/I+47LYKp/4urH76FNTMjl22EhLPz1tNrQufTsrFf/PYV/7ctSZYAK7fKPWQKg+Q== wkt-parser@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/wkt-parser/-/wkt-parser-1.3.3.tgz#46b4e3032dd9c86907f7e630b57e3c6ea2bb772b" + resolved "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz" integrity sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw== yallist@4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== zustand@^4.4.7: version "4.5.4" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.4.tgz#63abdd81edfb190bc61e0bbae045cc4d52158a05" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.4.tgz" integrity sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg== dependencies: use-sync-external-store "1.2.0"