From 401341f7f0286d73e702eb944097c2f29ff7f13d Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 21 Oct 2024 06:45:03 -0500 Subject: [PATCH] Persist user maps (#127) --- .../sidebar/GerryDBViewSelector.tsx | 53 +- .../components/sidebar/RecentMapsModal.tsx | 157 ++++++ app/src/app/store/mapEditSubs.ts | 19 +- app/src/app/store/mapStore.ts | 484 ++++++++++-------- app/src/app/store/persistConfig.ts | 12 + app/src/app/utils/api/queries.ts | 62 ++- app/src/app/utils/api/queryParamsListener.ts | 7 +- 7 files changed, 538 insertions(+), 256 deletions(-) create mode 100644 app/src/app/components/sidebar/RecentMapsModal.tsx create mode 100644 app/src/app/store/persistConfig.ts diff --git a/app/src/app/components/sidebar/GerryDBViewSelector.tsx b/app/src/app/components/sidebar/GerryDBViewSelector.tsx index 0cfc2520..ab304fb5 100644 --- a/app/src/app/components/sidebar/GerryDBViewSelector.tsx +++ b/app/src/app/components/sidebar/GerryDBViewSelector.tsx @@ -1,22 +1,18 @@ import { useState } from "react"; -import { Select } from "@radix-ui/themes"; -import { getAvailableDistrictrMaps } from "../../utils/api/apiHandlers"; +import { Flex, Select } from "@radix-ui/themes"; import { useMapStore } from "../../store/mapStore"; -import { useQuery } from "@tanstack/react-query"; import { document } from "@/app/utils/api/mutations"; +import { RecentMapsModal } from "./RecentMapsModal"; export function GerryDBViewSelector() { const [limit, setLimit] = useState(30); const [offset, setOffset] = useState(0); const mapDocument = useMapStore((state) => state.mapDocument); + const mapViews = useMapStore((state) => state.mapViews); + const { isPending, isError, data, error } = mapViews || {}; - const { isPending, isError, data, error } = useQuery({ - queryKey: ["views", limit, offset], - queryFn: () => getAvailableDistrictrMaps(limit, offset), - }); - const selectedView = data?.find( - (view) => view.gerrydb_table_name === mapDocument?.gerrydb_table, + (view) => view.gerrydb_table_name === mapDocument?.gerrydb_table ); const handleValueChange = (value: string) => { @@ -36,21 +32,32 @@ export function GerryDBViewSelector() { if (isPending) return
Loading geographies... 🌎
; - if (isError) return
Error loading geographies: {error.message}
; + if (isError) return
Error loading geographies: {error?.message}
; return ( - - - - - Districtr map options - {data.map((view, index) => ( - - {view.name} - - ))} - - - + + + + + + Districtr map options + {data?.map((view, index) => ( + + {view.name} + + ))} + + + + + ); } diff --git a/app/src/app/components/sidebar/RecentMapsModal.tsx b/app/src/app/components/sidebar/RecentMapsModal.tsx new file mode 100644 index 00000000..6f6d1d0c --- /dev/null +++ b/app/src/app/components/sidebar/RecentMapsModal.tsx @@ -0,0 +1,157 @@ +import { useMapStore } from "@/app/store/mapStore"; +import React from "react"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import { + Button, + Flex, + Text, + Table, + Dialog, + Box, + TextField, + IconButton, +} from "@radix-ui/themes"; +import { usePathname, useSearchParams, useRouter } from "next/navigation"; +import { DocumentObject } from "../../utils/api/apiHandlers"; +type NamedDocumentObject = DocumentObject & { name?: string }; +export const RecentMapsModal = () => { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const mapDocument = useMapStore((store) => store.mapDocument); + const userMaps = useMapStore((store) => store.userMaps); + const upcertUserMap = useMapStore((store) => store.upcertUserMap); + const setMapDocument = useMapStore((store) => store.setMapDocument); + const [dialogOpen, setDialogOpen] = React.useState(false); + + const handleMapDocument = (data: NamedDocumentObject) => { + setMapDocument(data); + const urlParams = new URLSearchParams(searchParams.toString()); + urlParams.set("document_id", data.document_id); + router.push(pathname + "?" + urlParams.toString()); + // close dialog + setDialogOpen(false); + }; + + if (!userMaps?.length) { + return null; + } + + return ( + + + + + + + + Recent Maps + + + + + + + + + + + Map Name + + Last Updated + {/* load */} + {/* delete */} + + + + + {userMaps.map((userMap, i) => ( + + upcertUserMap({ + userMapData, + userMapDocumentId: userMap.document_id, + }) + } + data={userMap} + onSelect={handleMapDocument} + /> + ))} + + + + + ); +}; + +const RecentMapsRow: React.FC<{ + data: NamedDocumentObject; + onSelect: (data: NamedDocumentObject) => void; + active: boolean; + onChange?: (data?: NamedDocumentObject) => void; +}> = ({ data, onSelect, active, onChange }) => { + const updatedDate = new Date(data.updated_at as string); + const formattedData = updatedDate.toLocaleDateString(); + const name = data?.name || data.gerrydb_table; + + const handleChangeName = (name?: string) => { + name?.length && onChange?.({ ...data, name }); + }; + + return ( + + + {!!(active && onChange) ? ( + + handleChangeName(e.target.value)} + > + + ) : ( + {name} + )} + + + {formattedData} + + + {!active && ( + + )} + + + {!active && ( + onChange?.()} + variant="ghost" + color="ruby" + className="size-full" + > + + + )} + + + ); +}; diff --git a/app/src/app/store/mapEditSubs.ts b/app/src/app/store/mapEditSubs.ts index cd374173..1967b34a 100644 --- a/app/src/app/store/mapEditSubs.ts +++ b/app/src/app/store/mapEditSubs.ts @@ -25,12 +25,21 @@ const zoneUpdates = ({ }; const debouncedZoneUpdate = debounce(zoneUpdates, 25); +type zoneSubState = [ + MapStore['getMapRef'], + MapStore['zoneAssignments'], + MapStore['appLoadingState'], + MapStore['mapRenderingState'] +] export const getMapEditSubs = (useMapStore: typeof _useMapStore) => { - const sendZonesOnMapRefSub = useMapStore.subscribe( - (state) => [state.getMapRef, state.zoneAssignments], - () => { - const { getMapRef, zoneAssignments, appLoadingState } = - useMapStore.getState(); + const sendZonesOnMapRefSub = useMapStore.subscribe( + (state) => [state.getMapRef, state.zoneAssignments, state.appLoadingState, state.mapRenderingState], + ([getMapRef, zoneAssignments, appLoadingState, mapRenderingState], [ _prevMapRef, _prevZoneAssignments, prevAppLoadingState, prevMapRenderingState]) => { + const previousNotLoaded = [appLoadingState, mapRenderingState, prevAppLoadingState, prevMapRenderingState].some(state => state !== 'loaded') + if (!getMapRef() || previousNotLoaded) { + return + } + console.log("!!!SENDING UPDATES", appLoadingState, mapRenderingState, prevAppLoadingState, prevMapRenderingState) debouncedZoneUpdate({ getMapRef, zoneAssignments, appLoadingState }); }, { equalityFn: shallowCompareArray} diff --git a/app/src/app/store/mapStore.ts b/app/src/app/store/mapStore.ts index 8f66c9df..5bfd8212 100644 --- a/app/src/app/store/mapStore.ts +++ b/app/src/app/store/mapStore.ts @@ -1,6 +1,7 @@ +"use client" import type { MapGeoJSONFeature, MapOptions } from "maplibre-gl"; import { create } from "zustand"; -import { devtools, subscribeWithSelector } from "zustand/middleware"; +import { devtools, subscribeWithSelector, persist } from "zustand/middleware"; import type { ActiveTool, MapFeatureInfo, @@ -10,12 +11,13 @@ import type { import { Zone, GDBPath } from "../constants/types"; import { Assignment, + DistrictrMap, DocumentObject, ZonePopulation, } from "../utils/api/apiHandlers"; import maplibregl from "maplibre-gl"; import type { MutableRefObject } from "react"; -import { UseQueryResult } from "@tanstack/react-query"; +import { QueryObserverResult, UseQueryResult } from "@tanstack/react-query"; import { ContextMenuState, LayerVisibility, @@ -28,6 +30,8 @@ import { patchShatter } from "../utils/api/mutations"; import { getSearchParamsObersver } from "../utils/api/queryParamsListener"; import { getMapMetricsSubs } from "./metricsSubs"; import { getMapEditSubs } from "./mapEditSubs"; +import { getMapViewsSubs } from "../utils/api/queries"; +import { persistOptions } from "./persistConfig"; const prodWrapper: typeof devtools = (store: any) => store @@ -42,6 +46,8 @@ export interface MapStore { setMapRef: (map: MutableRefObject) => void; mapLock: boolean; setMapLock: (lock: boolean) => void; + mapViews: Partial>; + setMapViews: (maps: MapStore["mapViews"]) => void; mapDocument: DocumentObject | null; setMapDocument: (mapDocument: DocumentObject) => void; shatterIds: { @@ -75,7 +81,7 @@ export interface MapStore { zonePopulations: Map; setZonePopulations: (zone: Zone, population: number) => void; accumulatedGeoids: Set; - setAccumulatedGeoids: (geoids: MapStore['accumulatedGeoids']) => void; + setAccumulatedGeoids: (geoids: MapStore["accumulatedGeoids"]) => void; brushSize: number; setBrushSize: (size: number) => void; isPainting: boolean; @@ -95,231 +101,287 @@ export interface MapStore { updateVisibleLayerIds: (layerIds: LayerVisibility[]) => void; contextMenu: ContextMenuState | null; setContextMenu: (menu: ContextMenuState | null) => void; + userMaps: Array; + setUserMaps: (userMaps: MapStore["userMaps"]) => void; + upcertUserMap: (props: { + documentId?: string; + mapDocument?: MapStore["mapDocument"]; + userMapDocumentId?: string; + userMapData?: MapStore["userMaps"][number]; + }) => void; } -const initialLoadingState = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has( - "document_id" -) - ? "loading" - : "initializing"; +const initialLoadingState = + typeof window !== "undefined" && + new URLSearchParams(window.location.search).has("document_id") + ? "loading" + : "initializing"; -export const useMapStore = create(devtools( - subscribeWithSelector((set, get) => ({ - appLoadingState: initialLoadingState, - setAppLoadingState: (appLoadingState) => set({ appLoadingState }), - mapRenderingState: "initializing", - setMapRenderingState: (mapRenderingState) => set({ mapRenderingState }), - getMapRef: () => null, - setMapRef: (mapRef) => { - set({ - getMapRef: () => mapRef.current, - appLoadingState: - initialLoadingState === "initializing" - ? "loaded" - : get().appLoadingState, - }) - }, - mapLock: false, - setMapLock: (mapLock) => set({ mapLock }), - mapDocument: null, - setMapDocument: (mapDocument) => { - const currentMapDocument = get().mapDocument - if (currentMapDocument?.document_id === mapDocument.document_id) { - return - } - get().setFreshMap(true); - get().resetZoneAssignments(); - - set({ - mapDocument: mapDocument, - shatterIds: { parents: new Set(), children: new Set() }, - }); - }, - shatterIds: { - parents: new Set(), - children: new Set(), - }, - handleShatter: async (document_id, geoids) => { - set({ mapLock: true }); - const shatterResult = await patchShatter.mutate({ - document_id, - geoids, - }); +export const useMapStore = create( + persist( + devwrapper( + subscribeWithSelector((set, get) => ({ + appLoadingState: initialLoadingState, + setAppLoadingState: (appLoadingState) => set({ appLoadingState }), + mapRenderingState: "initializing", + setMapRenderingState: (mapRenderingState) => set({ mapRenderingState }), + getMapRef: () => null, + setMapRef: (mapRef) => { + set({ + getMapRef: () => mapRef.current, + appLoadingState: + initialLoadingState === "initializing" + ? "loaded" + : get().appLoadingState, + }); + }, + mapLock: false, + setMapLock: (mapLock) => set({ mapLock }), + mapViews: { isPending: true }, + setMapViews: (mapViews) => set({ mapViews }), + mapDocument: null, + setMapDocument: (mapDocument) => { + const currentMapDocument = get().mapDocument; + if (currentMapDocument?.document_id === mapDocument.document_id) { + return; + } + get().setFreshMap(true); + get().resetZoneAssignments(); + get().upcertUserMap({ + mapDocument, + }) + set({ + mapDocument: mapDocument, + shatterIds: { parents: new Set(), children: new Set() }, + }); + }, + upcertUserMap: ({ mapDocument, userMapData, userMapDocumentId }) => { + let userMaps = [ ...get().userMaps ]; + const mapViews = get().mapViews.data + if (mapDocument?.document_id && mapViews) { + const documentIndex = userMaps.findIndex( + (f) => f.document_id === mapDocument?.document_id + ); + const documentInfo = mapViews.find( + (view) => view.gerrydb_table_name === mapDocument.gerrydb_table + ); + if (documentIndex !== -1) { + userMaps[documentIndex] = { + ...documentInfo, + ...userMaps[documentIndex], + ...mapDocument, + }; + } else { + userMaps = [{ ...mapDocument, ...documentInfo }, ...userMaps]; + } + } else if (userMapDocumentId) { + const i = userMaps.findIndex(map => map.document_id === userMapDocumentId) + if (userMapData) { + userMaps.splice(i, 1, userMapData); // Replace the map at index i with the new data + } else { + const urlParams = new URL(window.location.href).searchParams; + urlParams.delete("document_id"); // Remove the document_id parameter + window.history.pushState({}, '', window.location.pathname + '?' + urlParams.toString()); // Update the URL without document_id + userMaps.splice(i, 1); + } + } + set({ + userMaps, + }); + }, + shatterIds: { + parents: new Set(), + children: new Set(), + }, + handleShatter: async (document_id, geoids) => { + set({ mapLock: true }); + const shatterResult = await patchShatter.mutate({ + document_id, + geoids, + }); - const zoneAssignments = new Map(get().zoneAssignments); - const shatterIds = get().shatterIds; + const zoneAssignments = new Map(get().zoneAssignments); + const shatterIds = get().shatterIds; - let existingParents = new Set(shatterIds.parents); - let existingChildren = new Set(shatterIds.children); + let existingParents = new Set(shatterIds.parents); + let existingChildren = new Set(shatterIds.children); - const newParent = shatterResult.parents.geoids; - const newChildren = new Set( - shatterResult.children.map((child) => child.geo_id) - ); + const newParent = shatterResult.parents.geoids; + const newChildren = new Set( + shatterResult.children.map((child) => child.geo_id) + ); - const multipleShattered = shatterResult.parents.geoids.length > 1; - if (!multipleShattered) { - setZones(zoneAssignments, newParent[0], newChildren); - } else { - // todo handle multiple shattered case - } - newParent.forEach((parent) => existingParents.add(parent)); - // there may be a faster way to do this - [newChildren].forEach( - (children) => existingChildren = new Set([...existingChildren, ...children]) - ) + const multipleShattered = shatterResult.parents.geoids.length > 1; + if (!multipleShattered) { + setZones(zoneAssignments, newParent[0], newChildren); + } else { + // todo handle multiple shattered case + } + newParent.forEach((parent) => existingParents.add(parent)); + // there may be a faster way to do this + [newChildren].forEach( + (children) => + (existingChildren = new Set([...existingChildren, ...children])) + ); - set({ - shatterIds: { - parents: existingParents, - children: existingChildren, + set({ + shatterIds: { + parents: existingParents, + children: existingChildren, + }, + zoneAssignments, + }); }, - zoneAssignments, - }); - }, - setShatterIds: ( - existingParents, - existingChildren, - newParent, - newChildren, - multipleShattered - ) => { - const zoneAssignments = new Map(get().zoneAssignments); + setShatterIds: ( + existingParents, + existingChildren, + newParent, + newChildren, + multipleShattered + ) => { + const zoneAssignments = new Map(get().zoneAssignments); - if (!multipleShattered) { - setZones(zoneAssignments, newParent[0], newChildren[0]); - } else { - // todo handle multiple shattered case - } - newParent.forEach((parent) => existingParents.add(parent)); - // there may be a faster way to do this - newChildren.forEach( - (children) => existingChildren = new Set([...existingChildren, ...children]) - ); + if (!multipleShattered) { + setZones(zoneAssignments, newParent[0], newChildren[0]); + } else { + // todo handle multiple shattered case + } + newParent.forEach((parent) => existingParents.add(parent)); + // there may be a faster way to do this + newChildren.forEach( + (children) => + (existingChildren = new Set([...existingChildren, ...children])) + ); - set({ - shatterIds: { - parents: existingParents, - children: existingChildren, + set({ + shatterIds: { + parents: existingParents, + children: existingChildren, + }, + zoneAssignments, + }); }, - zoneAssignments, - }); - }, - hoverFeatures: [], - setHoverFeatures: (_features) => { - const hoverFeatures = _features - ? _features.map((f) => ({ - source: f.source, - sourceLayer: f.sourceLayer, - id: f.id, - })) - : []; + hoverFeatures: [], + setHoverFeatures: (_features) => { + const hoverFeatures = _features + ? _features.map((f) => ({ + source: f.source, + sourceLayer: f.sourceLayer, + id: f.id, + })) + : []; - set({ hoverFeatures }); - }, - mapOptions: { - center: [-98.5795, 39.8283], - zoom: 3, - pitch: 0, - bearing: 0, - container: "", - }, - setMapOptions: (options) => set({ mapOptions: options }), - activeTool: "pan", - setActiveTool: (tool) => set({ activeTool: tool }), - spatialUnit: "tract", - setSpatialUnit: (unit) => set({ spatialUnit: unit }), - selectedZone: 1, - setSelectedZone: (zone) => set({ selectedZone: zone }), - zoneAssignments: new Map(), - accumulatedGeoids: new Set(), - setAccumulatedGeoids: (accumulatedGeoids) => set({accumulatedGeoids}), - setZoneAssignments: (zone, geoids) => { - const zoneAssignments = get().zoneAssignments; - const newZoneAssignments = new Map(zoneAssignments); - geoids.forEach((geoid) => { - newZoneAssignments.set(geoid, zone); - }); - set({ - zoneAssignments: newZoneAssignments, - accumulatedGeoids: new Set(), - }); - }, - loadZoneAssignments: (assignments) => { - const zoneAssignments = new Map(); - const shatterIds = { - parents: new Set(), - children: new Set(), - }; - assignments.forEach((assignment) => { - zoneAssignments.set(assignment.geo_id, assignment.zone); - if (assignment.parent_path) { - shatterIds.parents.add(assignment.parent_path); - shatterIds.children.add(assignment.geo_id); - } - }); - set({ zoneAssignments, shatterIds, appLoadingState: "loaded" }); - }, - accumulatedBlockPopulations: new Map(), - resetAccumulatedBlockPopulations: () => - set({ accumulatedBlockPopulations: new Map() }), - zonePopulations: new Map(), - setZonePopulations: (zone, population) => - set((state) => { - const newZonePopulations = new Map(state.zonePopulations); - newZonePopulations.set(zone, population); - return { - zonePopulations: newZonePopulations, - }; - }), - resetZoneAssignments: () => set({ zoneAssignments: new Map() }), - brushSize: 50, - setBrushSize: (size) => set({ brushSize: size }), - isPainting: false, - setIsPainting: (isPainting) => set({ isPainting }), - paintFunction: getFeaturesInBbox, - setPaintFunction: (paintFunction) => set({ paintFunction }), - clearMapEdits: () => - set({ + set({ hoverFeatures }); + }, + mapOptions: { + center: [-98.5795, 39.8283], + zoom: 3, + pitch: 0, + bearing: 0, + container: "", + }, + setMapOptions: (options) => set({ mapOptions: options }), + activeTool: "pan", + setActiveTool: (tool) => set({ activeTool: tool }), + spatialUnit: "tract", + setSpatialUnit: (unit) => set({ spatialUnit: unit }), + selectedZone: 1, + setSelectedZone: (zone) => set({ selectedZone: zone }), zoneAssignments: new Map(), accumulatedGeoids: new Set(), - selectedZone: 1, - }), - freshMap: false, - setFreshMap: (resetMap) => set({ freshMap: resetMap }), - mapMetrics: null, - setMapMetrics: (metrics) => set({ mapMetrics: metrics }), - visibleLayerIds: ["counties_boundary", "counties_labels"], - setVisibleLayerIds: (layerIds) => set({ visibleLayerIds: layerIds }), - addVisibleLayerIds: (layerIds: string[]) => { - set((state) => { - const newVisibleLayerIds = new Set(state.visibleLayerIds); - layerIds.forEach((layerId) => { - newVisibleLayerIds.add(layerId); - }); - return { visibleLayerIds: Array.from(newVisibleLayerIds) }; - }); - }, - updateVisibleLayerIds: (layerVisibilities: LayerVisibility[]) => { - set((state) => { - const newVisibleLayerIds = new Set(state.visibleLayerIds); - layerVisibilities.forEach((layerVisibility) => { - if (layerVisibility.visibility === "visible") { - newVisibleLayerIds.add(layerVisibility.layerId); - } else { - newVisibleLayerIds.delete(layerVisibility.layerId); - } - }); - return { visibleLayerIds: Array.from(newVisibleLayerIds) }; - }); - }, - contextMenu: null, - setContextMenu: (contextMenu) => set({ contextMenu }), - })) -)); + setAccumulatedGeoids: (accumulatedGeoids) => set({ accumulatedGeoids }), + setZoneAssignments: (zone, geoids) => { + const zoneAssignments = get().zoneAssignments; + const newZoneAssignments = new Map(zoneAssignments); + geoids.forEach((geoid) => { + newZoneAssignments.set(geoid, zone); + }); + set({ + zoneAssignments: newZoneAssignments, + accumulatedGeoids: new Set(), + }); + }, + loadZoneAssignments: (assignments) => { + const zoneAssignments = new Map(); + const shatterIds = { + parents: new Set(), + children: new Set(), + }; + assignments.forEach((assignment) => { + zoneAssignments.set(assignment.geo_id, assignment.zone); + if (assignment.parent_path) { + shatterIds.parents.add(assignment.parent_path); + shatterIds.children.add(assignment.geo_id); + } + }); + set({ zoneAssignments, shatterIds, appLoadingState: "loaded" }); + }, + accumulatedBlockPopulations: new Map(), + resetAccumulatedBlockPopulations: () => + set({ accumulatedBlockPopulations: new Map() }), + zonePopulations: new Map(), + setZonePopulations: (zone, population) => + set((state) => { + const newZonePopulations = new Map(state.zonePopulations); + newZonePopulations.set(zone, population); + return { + zonePopulations: newZonePopulations, + }; + }), + resetZoneAssignments: () => set({ zoneAssignments: new Map() }), + brushSize: 50, + setBrushSize: (size) => set({ brushSize: size }), + isPainting: false, + setIsPainting: (isPainting) => set({ isPainting }), + paintFunction: getFeaturesInBbox, + setPaintFunction: (paintFunction) => set({ paintFunction }), + clearMapEdits: () => + set({ + zoneAssignments: new Map(), + accumulatedGeoids: new Set(), + selectedZone: 1, + }), + freshMap: false, + setFreshMap: (resetMap) => set({ freshMap: resetMap }), + mapMetrics: null, + setMapMetrics: (metrics) => set({ mapMetrics: metrics }), + visibleLayerIds: ["counties_boundary", "counties_labels"], + setVisibleLayerIds: (layerIds) => set({ visibleLayerIds: layerIds }), + addVisibleLayerIds: (layerIds: string[]) => { + set((state) => { + const newVisibleLayerIds = new Set(state.visibleLayerIds); + layerIds.forEach((layerId) => { + newVisibleLayerIds.add(layerId); + }); + return { visibleLayerIds: Array.from(newVisibleLayerIds) }; + }); + }, + updateVisibleLayerIds: (layerVisibilities: LayerVisibility[]) => { + set((state) => { + const newVisibleLayerIds = new Set(state.visibleLayerIds); + layerVisibilities.forEach((layerVisibility) => { + if (layerVisibility.visibility === "visible") { + newVisibleLayerIds.add(layerVisibility.layerId); + } else { + newVisibleLayerIds.delete(layerVisibility.layerId); + } + }); + return { visibleLayerIds: Array.from(newVisibleLayerIds) }; + }); + }, + contextMenu: null, + setContextMenu: (contextMenu) => set({ contextMenu }), + userMaps: [], + setUserMaps: (userMaps) => set({ userMaps }), + })) + ), + persistOptions + ) +); // these need to initialize after the map store getRenderSubscriptions(useMapStore); getMapMetricsSubs(useMapStore); +getMapViewsSubs(useMapStore); getMapEditSubs(useMapStore); -getSearchParamsObersver(); \ No newline at end of file +getSearchParamsObersver(); diff --git a/app/src/app/store/persistConfig.ts b/app/src/app/store/persistConfig.ts new file mode 100644 index 00000000..c7f10baf --- /dev/null +++ b/app/src/app/store/persistConfig.ts @@ -0,0 +1,12 @@ +import { PersistOptions } from "zustand/middleware" +import { MapStore } from "./mapStore" + + +export const persistOptions: PersistOptions> = { + name: 'districtr-persistrictr', + version: 0, + partialize: (state) => ({ + userMaps: state.userMaps + }), + +} diff --git a/app/src/app/utils/api/queries.ts b/app/src/app/utils/api/queries.ts index b5d04db1..b4f3cd55 100644 --- a/app/src/app/utils/api/queries.ts +++ b/app/src/app/utils/api/queries.ts @@ -1,6 +1,8 @@ import { QueryObserver, skipToken } from "@tanstack/react-query"; import { queryClient } from "./queryClient"; import { + DistrictrMap, + getAvailableDistrictrMaps, Assignment, DocumentObject, getAssignments, @@ -10,6 +12,9 @@ import { } from "./apiHandlers"; import { MapStore, useMapStore } from "@/app/store/mapStore"; +const INITIAL_VIEW_LIMIT = 30 +const INITIAL_VIEW_OFFSET = 0 + export const mapMetrics = new QueryObserver(queryClient, { queryKey: ["_zonePopulations"], queryFn: skipToken, @@ -22,29 +27,58 @@ export const updateMapMetrics = (mapDocument: DocumentObject) => { }); }; + mapMetrics.subscribe((result) => { useMapStore.getState().setMapMetrics(result); }); +export const mapViewsQuery = new QueryObserver(queryClient, { + queryKey: ["views", INITIAL_VIEW_LIMIT, INITIAL_VIEW_OFFSET], + queryFn: () => getAvailableDistrictrMaps(INITIAL_VIEW_LIMIT, INITIAL_VIEW_OFFSET), +}); + +export const updateMapViews = (limit: number, offset: number) => { + mapViewsQuery.setOptions({ + queryKey: ["views", limit, offset], + queryFn: () => getAvailableDistrictrMaps(limit, offset), + }); +}; + +export const getMapViewsSubs = (_useMapStore: typeof useMapStore) => { + mapViewsQuery.subscribe((result) => { + if (result) { + _useMapStore.getState().setMapViews(result) + } + }) +} + +const getDocumentFunction = (documentId?: string) => { + return async () => { + const currentId = useMapStore.getState().mapDocument?.document_id; + if (documentId && documentId !== currentId) { + useMapStore.getState().setAppLoadingState('loading'); + return await getDocument(documentId); + } else { + return null; + } +} +} + export const updateDocumentFromId = new QueryObserver( queryClient, { - queryKey: ["mapDocument"], - queryFn: async () => { - const document_id = new URL(window.location.href).searchParams.get( - "document_id" - ); - const mapDocument = useMapStore.getState().mapDocument; - if (document_id && mapDocument?.document_id !== document_id) { - useMapStore.getState().setAppLoadingState('loading'); - return await getDocument(document_id); - } else { - return null; - } - }, - } + queryKey: ["mapDocument", undefined], + queryFn: getDocumentFunction() + }, ); +export const updateGetDocumentFromId = (documentId:string) => { + updateDocumentFromId.setOptions({ + queryKey: ["mapDocument", documentId], + queryFn: getDocumentFunction(documentId) + }); +} + updateDocumentFromId.subscribe((mapDocument) => { if (mapDocument.data) { useMapStore.getState().setMapDocument(mapDocument.data); diff --git a/app/src/app/utils/api/queryParamsListener.ts b/app/src/app/utils/api/queryParamsListener.ts index 42e947dc..955ed752 100644 --- a/app/src/app/utils/api/queryParamsListener.ts +++ b/app/src/app/utils/api/queryParamsListener.ts @@ -1,4 +1,5 @@ -import { updateDocumentFromId } from "./queries"; +import { updateDocumentFromId, updateGetDocumentFromId } from "./queries"; +export let previousDocumentID = '' export const getSearchParamsObersver = () => { // next ssr safety @@ -21,8 +22,8 @@ export const getSearchParamsObersver = () => { "document_id" ); if (documentId && documentId !== previousDocumentID) { - previousDocumentID = documentId; - updateDocumentFromId.refetch(); + previousDocumentID = documentId + updateGetDocumentFromId(documentId) } }); const config = { subtree: true, childList: true };