From 4c5ef483084b8e9dea70e00c401c672b0931ead0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Mon, 28 Aug 2023 12:54:31 +0200 Subject: [PATCH 01/28] WIP --- app/hooks/wdpa/index.ts | 75 +++++++---- app/hooks/wdpa/types.ts | 16 --- .../protected-areas/actions-menu/index.tsx | 97 ++++++++++++++ .../bulk-action-menu/index.tsx | 83 ++++++++++++ .../protected-areas/bulk-action-menu/utils.ts | 75 +++++++++++ .../project/inventory-panel/wdpas/index.tsx | 120 +++++++++++++++++- app/services/wdpa/index.ts | 19 --- app/store/slices/projects/[id].ts | 17 ++- app/types/api/wdpa.ts | 21 +++ 9 files changed, 462 insertions(+), 61 deletions(-) delete mode 100644 app/hooks/wdpa/types.ts create mode 100644 app/layout/project/sidebar/project/inventory-panel/protected-areas/actions-menu/index.tsx create mode 100644 app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx create mode 100644 app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts delete mode 100644 app/services/wdpa/index.ts create mode 100644 app/types/api/wdpa.ts diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index eb4cb799e6..3e53f7f268 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -1,31 +1,32 @@ -import { useMemo } from 'react'; - -import { useMutation, useQuery } from 'react-query'; +import { useMutation, useQuery, QueryObserverOptions } from 'react-query'; +import { AxiosRequestConfig } from 'axios'; import { useSession } from 'next-auth/react'; -import SCENARIOS from 'services/scenarios'; -import WDPA from 'services/wdpa'; +import { Project } from 'types/api/project'; +import { Scenario } from 'types/api/scenario'; +import { WDPA, WDPACategory } from 'types/api/wdpa'; -import { - UseWDPACategoriesProps, - UseSaveScenarioProtectedAreasProps, - SaveScenarioProtectedAreasProps, -} from './types'; +import { API } from 'services/api'; +import SCENARIOS from 'services/scenarios'; export function useWDPACategories({ adminAreaId, customAreaId, scenarioId, -}: UseWDPACategoriesProps) { +}: { + adminAreaId?: WDPA['id']; + customAreaId?: WDPA['id']; + scenarioId: Scenario['id']; +}) { const { data: session } = useSession(); return useQuery( ['protected-areas', adminAreaId, customAreaId], async () => - WDPA.request({ + API.request({ method: 'GET', - url: `/${scenarioId}/protected-areas`, + url: `/scenarios/${scenarioId}/protected-areas`, params: { ...(adminAreaId && { 'filter[adminAreaId]': adminAreaId, @@ -49,10 +50,24 @@ export function useSaveScenarioProtectedAreas({ requestConfig = { method: 'POST', }, -}: UseSaveScenarioProtectedAreasProps) { +}: { + requestConfig?: AxiosRequestConfig; +}) { const { data: session } = useSession(); - const saveScenarioProtectedAreas = ({ id, data }: SaveScenarioProtectedAreasProps) => { + const saveScenarioProtectedAreas = ({ + id, + data, + }: { + id: Scenario['id']; + data: { + areas: { + id: string; + selected: boolean; + }[]; + threshold: number; + }; + }) => { return SCENARIOS.request({ url: `/${id}/protected-areas`, data, @@ -63,12 +78,28 @@ export function useSaveScenarioProtectedAreas({ }); }; - return useMutation(saveScenarioProtectedAreas, { - onSuccess: (data: any, variables, context) => { - console.info('Succces', data, variables, context); - }, - onError: (error, variables, context) => { - console.info('Error', error, variables, context); - }, + return useMutation(saveScenarioProtectedAreas); +} + +export function useProjectWDPAs( + pid: Project['id'], + params: { sort?: string } = {}, + queryOptions: QueryObserverOptions = {} +) { + const { data: session } = useSession(); + + return useQuery({ + queryKey: ['protected-areas', pid], + queryFn: async () => + API.request({ + method: 'GET', + url: `/projects/${pid}/protected-areas`, + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + params, + }).then(({ data }) => data), + enabled: Boolean(pid), + ...queryOptions, }); } diff --git a/app/hooks/wdpa/types.ts b/app/hooks/wdpa/types.ts deleted file mode 100644 index 050cc2e1d6..0000000000 --- a/app/hooks/wdpa/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AxiosRequestConfig } from 'axios'; - -export interface UseWDPACategoriesProps { - adminAreaId?: string; - customAreaId?: string; - scenarioId: string[] | string; -} - -export interface UseSaveScenarioProtectedAreasProps { - requestConfig?: AxiosRequestConfig; -} - -export interface SaveScenarioProtectedAreasProps { - data: unknown; - id: string[] | string; -} diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/actions-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/protected-areas/actions-menu/index.tsx new file mode 100644 index 0000000000..9d5cf5c54d --- /dev/null +++ b/app/layout/project/sidebar/project/inventory-panel/protected-areas/actions-menu/index.tsx @@ -0,0 +1,97 @@ +import { useCallback, useState } from 'react'; + +import Icon from 'components/icon'; +import Modal from 'components/modal/component'; +import DeleteModal from 'layout/project/sidebar/project/inventory-panel/features/modals/delete'; +import EditModal from 'layout/project/sidebar/project/inventory-panel/features/modals/edit'; +import { cn } from 'utils/cn'; + +import DELETE_SVG from 'svgs/ui/new-layout/delete.svg?sprite'; +import TAG_SVG from 'svgs/ui/tag.svg?sprite'; + +const BUTTON_CLASSES = + 'flex items-center px-4 py-2 w-full text-sm cursor-pointer bg-gray-700 hover:bg-gray-500 transition transition-colors space-x-2 group'; + +const ICON_CLASSES = 'h-5 w-5 text-gray-400 group-hover:text-white'; + +const ActionsMenu = ({ + item, +}: { + item: { + id: string; + name: string; + scenarios: number; + tag: string; + custom: boolean; + }; +}): JSX.Element => { + const isDeletable = !item.custom && !item.scenarios; + + // item.isCustom && !item.scenarioUsageCount + const [modalState, setModalState] = useState<{ edit: boolean; delete: boolean }>({ + edit: false, + delete: false, + }); + + const handleModal = useCallback((modalKey: keyof typeof modalState, isVisible: boolean) => { + setModalState((prevState) => ({ ...prevState, [modalKey]: isVisible })); + }, []); + + return ( +
    +
  • + + handleModal('edit', false)} + > + + +
  • + {isDeletable && ( +
  • + + { + handleModal('delete', false); + }} + > + + +
  • + )} +
+ ); +}; + +export default ActionsMenu; diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx new file mode 100644 index 0000000000..ab35ef62c6 --- /dev/null +++ b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx @@ -0,0 +1,83 @@ +import { useCallback, useState } from 'react'; + +import Button from 'components/button'; +import Icon from 'components/icon'; +import Modal from 'components/modal/component'; +import DeleteModal from 'layout/project/sidebar/project/inventory-panel/features/modals/delete/index'; +import EditBulkModal from 'layout/project/sidebar/project/inventory-panel/features/modals/edit-bulk'; +import { Feature } from 'types/api/feature'; + +import EDIT_SVG from 'svgs/ui/edit.svg?sprite'; +import DELETE_SVG from 'svgs/ui/new-layout/delete.svg?sprite'; + +const BUTTON_CLASSES = + 'col-span-1 flex items-center space-x-2 rounded-lg bg-gray-700 px-4 text-xs text-gray-50'; +const ICON_CLASSES = 'h-5 w-5 transition-colors text-gray-400 group-hover:text-gray-50'; + +const FeaturesBulkActionMenu = ({ + selectedFeaturesIds, +}: { + selectedFeaturesIds: Feature['id'][]; +}): JSX.Element => { + const [modalState, setModalState] = useState<{ edit: boolean; delete: boolean }>({ + edit: false, + delete: false, + }); + + const handleModal = useCallback((modalKey: keyof typeof modalState, isVisible: boolean) => { + setModalState((prevState) => ({ ...prevState, [modalKey]: isVisible })); + }, []); + + return ( + <> +
+ + + {selectedFeaturesIds.length} + + Selected + + + +
+ + handleModal('edit', false)} + > + + + + handleModal('delete', false)} + > + + + + ); +}; + +export default FeaturesBulkActionMenu; diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts new file mode 100644 index 0000000000..e0cba8e243 --- /dev/null +++ b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts @@ -0,0 +1,75 @@ +import { Session } from 'next-auth'; + +import { Feature } from 'types/api/feature'; +import { Project } from 'types/api/project'; + +import PROJECTS from 'services/projects'; + +export function bulkDeleteFeatureFromProject( + pid: Project['id'], + fids: Feature['id'][], + session: Session +) { + const deleteFeatureFromProject = ({ pid, fid }: { pid: Project['id']; fid: Feature['id'] }) => { + return PROJECTS.delete(`/${pid}/features/${fid}`, { + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + }); + }; + + return Promise.all(fids.map((fid) => deleteFeatureFromProject({ pid, fid }))); +} + +export function editFeaturesTagsBulk( + projectId: Project['id'], + featureIds: Feature['id'][], + session: Session, + data: { + tagName: string; + } +) { + const editFeatureTag = ({ + featureId, + projectId, + data, + }: { + featureId: Feature['id']; + projectId: Project['id']; + data: { + tagName: string; + }; + }) => { + return PROJECTS.request({ + method: 'PATCH', + url: `/${projectId}/features/${featureId}/tags`, + data, + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + }); + }; + return Promise.all(featureIds.map((featureId) => editFeatureTag({ projectId, featureId, data }))); +} + +export function deleteFeaturesTagsBulk( + projectId: Project['id'], + featureIds: Feature['id'][], + session: Session +) { + const deleteFeatureTags = ({ + projectId, + featureId, + }: { + projectId: Project['id']; + featureId: Feature['id']; + }) => { + return PROJECTS.delete(`/${projectId}/features/${featureId}/tags`, { + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + }); + }; + + return Promise.all(featureIds.map((featureId) => deleteFeatureTags({ projectId, featureId }))); +} diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index c564d99d16..1e0a9ba9be 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -1,9 +1,127 @@ +import { useState, useCallback, useEffect, ChangeEvent } from 'react'; + +import { useRouter } from 'next/router'; + +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { setSelectedWDPA } from 'store/slices/projects/[id]'; + +import { useProjectWDPAs } from 'hooks/wdpa'; + +import { WDPA } from 'types/api/wdpa'; + +import InventoryTable, { type DataItem } from '../components/inventory-table'; + +import ActionsMenu from './actions-menu'; +import WDPABulkActionMenu from './bulk-action-menu'; + +const WDPA_TABLE_COLUMNS = { + name: 'fullName', +}; + const InventoryPanelProtectedAreas = ({ noData: noDataMessage, }: { noData: string; }): JSX.Element => { - return
{noDataMessage}
; + const dispatch = useAppDispatch(); + const { selectedWDPA, search } = useAppSelector((state) => state['/projects/[id]']); + const { query } = useRouter(); + const { pid } = query as { pid: string }; + + const allProjectWDPAsQuery = useProjectWDPAs( + pid, + {}, + { + select: (data) => + data?.map((wdpa) => ({ + id: wdpa.id, + name: wdpa.fullName, + scenarios: wdpa.scenarioUsageCount, + })), + keepPreviousData: true, + placeholderData: [], + } + ); + + const [selectedWDPAIds, setSelectedWDPAIds] = useState([]); + const [filters, setFilters] = useState[1]>({ + sort: WDPA_TABLE_COLUMNS.name, + }); + + const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ + ...wdpa, + isVisibleOnMap: selectedWDPA.includes(wdpa.id), + })); + + const WDPAIds = allProjectWDPAsQuery.data?.map((wdpa) => wdpa.id); + + const handleSelectAll = useCallback( + (evt: ChangeEvent) => { + setSelectedWDPAIds(evt.target.checked ? WDPAIds : []); + }, + [WDPAIds] + ); + + const handleSelectWDPA = useCallback((evt: ChangeEvent) => { + if (evt.target.checked) { + setSelectedWDPAIds((prevSelectedFeatures) => [...prevSelectedFeatures, evt.target.value]); + } else { + setSelectedWDPAIds((prevSelectedFeatures) => + prevSelectedFeatures.filter((featureId) => featureId !== evt.target.value) + ); + } + }, []); + + const toggleSeeOnMap = useCallback( + (WDPAId: WDPA['id']) => { + const newSelectedWDPAs = [...selectedWDPA]; + if (!newSelectedWDPAs.includes(WDPAId)) { + newSelectedWDPAs.push(WDPAId); + } else { + const i = newSelectedWDPAs.indexOf(WDPAId); + newSelectedWDPAs.splice(i, 1); + } + dispatch(setSelectedWDPA(newSelectedWDPAs)); + }, + [dispatch, selectedWDPA] + ); + + const handleSort = useCallback( + (_sortType: (typeof filters)['sort']) => { + const sort = filters.sort === _sortType ? `-${_sortType}` : _sortType; + + setFilters((prevFilters) => ({ + ...prevFilters, + sort, + })); + }, + [filters.sort] + ); + + const displayBulkActions = selectedWDPAIds.length > 0; + + useEffect(() => { + setSelectedWDPAIds([]); + }, [search]); + + return ( +
+ + {displayBulkActions && } +
+ ); }; export default InventoryPanelProtectedAreas; diff --git a/app/services/wdpa/index.ts b/app/services/wdpa/index.ts deleted file mode 100644 index 241bf96313..0000000000 --- a/app/services/wdpa/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import axios from 'axios'; - -const WDPA = axios.create({ - baseURL: `${process.env.NEXT_PUBLIC_API_URL}/api/v1/scenarios`, - headers: { 'Content-Type': 'application/json' }, - transformResponse: (data) => { - try { - const parsedData = JSON.parse(data); - return { - data: parsedData, - meta: {}, - }; - } catch (error) { - return data; - } - }, -}); - -export default WDPA; diff --git a/app/store/slices/projects/[id].ts b/app/store/slices/projects/[id].ts index 06bb3d6cba..1cce691e59 100644 --- a/app/store/slices/projects/[id].ts +++ b/app/store/slices/projects/[id].ts @@ -1,12 +1,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { CostSurface } from 'types/api/cost-surface'; +import { Feature } from 'types/api/feature'; +import { WDPA } from 'types/api/wdpa'; + interface ProjectShowStateProps { search: string; filters: Record | []; sort: string; layerSettings: Record; - selectedFeatures: string[]; - selectedCostSurface: string | null; + selectedCostSurface: CostSurface['id'][]; + selectedFeatures: Feature['id'][]; + selectedWDPA: WDPA['id'][]; isSidebarOpen: boolean; } @@ -16,7 +21,8 @@ const initialState: ProjectShowStateProps = { sort: '-lastModifiedAt', layerSettings: {}, selectedFeatures: [], - selectedCostSurface: null, + selectedCostSurface: [], + selectedWDPA: [], isSidebarOpen: true, } satisfies ProjectShowStateProps; @@ -71,6 +77,10 @@ const projectsDetailSlice = createSlice({ ) => { state.selectedCostSurface = action.payload; }, + // WDPA + setSelectedWDPA: (state, action: PayloadAction) => { + state.selectedFeatures = action.payload; + }, }, }); @@ -81,6 +91,7 @@ export const { setLayerSettings, setSelectedFeatures, setSelectedCostSurface, + setSelectedWDPA, setSidebarVisibility, } = projectsDetailSlice.actions; diff --git a/app/types/api/wdpa.ts b/app/types/api/wdpa.ts new file mode 100644 index 0000000000..8baf21cf97 --- /dev/null +++ b/app/types/api/wdpa.ts @@ -0,0 +1,21 @@ +import { Job } from './job'; + +export interface WDPA { + id: string; + wdpaId: string; + fullName: string; + iucnCategory: string; + countryId: string; + shapeLength?: number; + shapeArea?: number; + status?: Job['status']; + designation?: string; + scenarioUsageCount: number; +} + +export interface WDPACategory { + id: string; + kind: 'global' | 'project'; + name: string; + selected: boolean; +} From ae500b89c9a2d5dea768f2adff9b7e31b6d748a3 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Mon, 28 Aug 2023 14:45:35 +0200 Subject: [PATCH 02/28] fix data empty --- .../inventory-panel/components/inventory-table/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx index f2b6e6c4dd..70aaa7781d 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx @@ -24,7 +24,7 @@ const InventoryTable = ({ return ( <> - {loading && !data.length && ( + {loading && !data?.length && (
Date: Mon, 28 Aug 2023 17:14:22 +0200 Subject: [PATCH 03/28] list protected areas by mock --- app/hooks/wdpa/index.ts | 41 ++++++++++++++++++- .../components/inventory-table/types.ts | 2 +- .../project/inventory-panel/wdpas/index.tsx | 8 ++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 3e53f7f268..6e57514623 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -88,6 +88,45 @@ export function useProjectWDPAs( ) { const { data: session } = useSession(); + const mockData: WDPA[] = [ + { + id: 'hgdfghmdgf', + wdpaId: 'hfdgjfhdg', + fullName: 'IUCN Category', + iucnCategory: 'IUCN Category', + countryId: 'hgdfjkd', + shapeLength: 45, + shapeArea: 30, + status: 'done', + designation: 'd', + scenarioUsageCount: 3, + }, + { + id: 'hfghmdgf', + wdpaId: 'hfdgjfhdg', + fullName: 'IUCN Ia', + iucnCategory: 'IUCN Ia', + countryId: 'mdfgjf', + shapeLength: 45, + shapeArea: 30, + status: 'done', + designation: 'd', + scenarioUsageCount: 0, + }, + { + id: 'hfgxssshmdgf', + wdpaId: 'hfdgjfhdg', + fullName: 'IUCN Ib', + iucnCategory: 'IUCN Ib', + countryId: 'mdfgjf', + shapeLength: 45, + shapeArea: 30, + status: 'done', + designation: 'd', + scenarioUsageCount: 0, + }, + ]; + return useQuery({ queryKey: ['protected-areas', pid], queryFn: async () => @@ -98,7 +137,7 @@ export function useProjectWDPAs( Authorization: `Bearer ${session.accessToken}`, }, params, - }).then(({ data }) => data), + }).then(({ data }) => mockData), enabled: Boolean(pid), ...queryOptions, }); diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts index 55942fbe87..970b61cd98 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts @@ -3,7 +3,7 @@ import { ChangeEvent } from 'react'; export type DataItem = { id: string; name: string; - scenarios: number; + scenarios?: number; tag?: string; isVisibleOnMap: boolean; isCustom?: boolean; diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index 1e0a9ba9be..35bcc87b3a 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -35,8 +35,8 @@ const InventoryPanelProtectedAreas = ({ select: (data) => data?.map((wdpa) => ({ id: wdpa.id, - name: wdpa.fullName, - scenarios: wdpa.scenarioUsageCount, + fullName: wdpa.fullName, + scenarioUsageCount: wdpa.scenarioUsageCount, })), keepPreviousData: true, placeholderData: [], @@ -50,7 +50,9 @@ const InventoryPanelProtectedAreas = ({ const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ ...wdpa, - isVisibleOnMap: selectedWDPA.includes(wdpa.id), + name: wdpa.fullName, + scenarios: wdpa.scenarioUsageCount, + isVisibleOnMap: selectedWDPA?.includes(wdpa.id), })); const WDPAIds = allProjectWDPAsQuery.data?.map((wdpa) => wdpa.id); From c57998d1d620a104fd15ac5359d3627275b2c3de Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Mon, 28 Aug 2023 18:54:51 +0200 Subject: [PATCH 04/28] bulk remove --- app/hooks/wdpa/index.ts | 2 +- .../bulk-action-menu/index.tsx | 40 ++---- .../protected-areas/bulk-action-menu/utils.ts | 67 ++------- .../protected-areas/modals/delete/index.tsx | 129 ++++++++++++++++++ .../project/inventory-panel/wdpas/index.tsx | 2 +- 5 files changed, 151 insertions(+), 89 deletions(-) create mode 100644 app/layout/project/sidebar/project/inventory-panel/protected-areas/modals/delete/index.tsx diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 6e57514623..21161909d9 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -128,7 +128,7 @@ export function useProjectWDPAs( ]; return useQuery({ - queryKey: ['protected-areas', pid], + queryKey: ['wdpas', pid], queryFn: async () => API.request({ method: 'GET', diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx index ab35ef62c6..181321f693 100644 --- a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx @@ -3,21 +3,19 @@ import { useCallback, useState } from 'react'; import Button from 'components/button'; import Icon from 'components/icon'; import Modal from 'components/modal/component'; -import DeleteModal from 'layout/project/sidebar/project/inventory-panel/features/modals/delete/index'; -import EditBulkModal from 'layout/project/sidebar/project/inventory-panel/features/modals/edit-bulk'; -import { Feature } from 'types/api/feature'; +import DeleteModal from 'layout/project/sidebar/project/inventory-panel/protected-areas/modals/delete'; +import { WDPA } from 'types/api/wdpa'; -import EDIT_SVG from 'svgs/ui/edit.svg?sprite'; import DELETE_SVG from 'svgs/ui/new-layout/delete.svg?sprite'; const BUTTON_CLASSES = 'col-span-1 flex items-center space-x-2 rounded-lg bg-gray-700 px-4 text-xs text-gray-50'; const ICON_CLASSES = 'h-5 w-5 transition-colors text-gray-400 group-hover:text-gray-50'; -const FeaturesBulkActionMenu = ({ - selectedFeaturesIds, +const WDPABulkActionMenu = ({ + selectedWDPAIds, }: { - selectedFeaturesIds: Feature['id'][]; + selectedWDPAIds: WDPA['id'][]; }): JSX.Element => { const [modalState, setModalState] = useState<{ edit: boolean; delete: boolean }>({ edit: false, @@ -30,22 +28,14 @@ const FeaturesBulkActionMenu = ({ return ( <> -
+
- {selectedFeaturesIds.length} + {selectedWDPAIds.length} Selected - +
- handleModal('edit', false)} - > - - - handleModal('delete', false)} > - + ); }; -export default FeaturesBulkActionMenu; +export default WDPABulkActionMenu; diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts index e0cba8e243..116bca93ea 100644 --- a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts +++ b/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils.ts @@ -1,75 +1,28 @@ import { Session } from 'next-auth'; -import { Feature } from 'types/api/feature'; import { Project } from 'types/api/project'; +import { WDPA } from 'types/api/wdpa'; import PROJECTS from 'services/projects'; -export function bulkDeleteFeatureFromProject( +export function bulkDeleteWDPAFromProject( pid: Project['id'], - fids: Feature['id'][], + wdpaids: WDPA['id'][], session: Session ) { - const deleteFeatureFromProject = ({ pid, fid }: { pid: Project['id']; fid: Feature['id'] }) => { - return PROJECTS.delete(`/${pid}/features/${fid}`, { - headers: { - Authorization: `Bearer ${session.accessToken}`, - }, - }); - }; - - return Promise.all(fids.map((fid) => deleteFeatureFromProject({ pid, fid }))); -} - -export function editFeaturesTagsBulk( - projectId: Project['id'], - featureIds: Feature['id'][], - session: Session, - data: { - tagName: string; - } -) { - const editFeatureTag = ({ - featureId, - projectId, - data, - }: { - featureId: Feature['id']; - projectId: Project['id']; - data: { - tagName: string; - }; - }) => { - return PROJECTS.request({ - method: 'PATCH', - url: `/${projectId}/features/${featureId}/tags`, - data, - headers: { - Authorization: `Bearer ${session.accessToken}`, - }, - }); - }; - return Promise.all(featureIds.map((featureId) => editFeatureTag({ projectId, featureId, data }))); -} - -export function deleteFeaturesTagsBulk( - projectId: Project['id'], - featureIds: Feature['id'][], - session: Session -) { - const deleteFeatureTags = ({ - projectId, - featureId, + const deleteFeatureFromProject = ({ + pid, + wdpaid, }: { - projectId: Project['id']; - featureId: Feature['id']; + pid: Project['id']; + wdpaid: WDPA['id']; }) => { - return PROJECTS.delete(`/${projectId}/features/${featureId}/tags`, { + return PROJECTS.delete(`/${pid}/protected-areas/${wdpaid}`, { headers: { Authorization: `Bearer ${session.accessToken}`, }, }); }; - return Promise.all(featureIds.map((featureId) => deleteFeatureTags({ projectId, featureId }))); + return Promise.all(wdpaids.map((wdpaid) => deleteFeatureFromProject({ pid, wdpaid }))); } diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/modals/delete/index.tsx b/app/layout/project/sidebar/project/inventory-panel/protected-areas/modals/delete/index.tsx new file mode 100644 index 0000000000..225109a4e6 --- /dev/null +++ b/app/layout/project/sidebar/project/inventory-panel/protected-areas/modals/delete/index.tsx @@ -0,0 +1,129 @@ +import { useCallback, useMemo } from 'react'; + +import { useQueryClient } from 'react-query'; + +import { useRouter } from 'next/router'; + +import { useSession } from 'next-auth/react'; + +import { useToasts } from 'hooks/toast'; +import { useProjectWDPAs } from 'hooks/wdpa'; + +import { Button } from 'components/button/component'; +import Icon from 'components/icon/component'; +import { ModalProps } from 'components/modal'; +import { bulkDeleteWDPAFromProject } from 'layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/utils'; +import { WDPA } from 'types/api/wdpa'; + +import ALERT_SVG from 'svgs/ui/new-layout/alert.svg?sprite'; + +const DeleteModal = ({ + selectedWDPAIds, + onDismiss, +}: { + selectedWDPAIds: WDPA['id'][]; + onDismiss?: ModalProps['onDismiss']; +}): JSX.Element => { + const { data: session } = useSession(); + const queryClient = useQueryClient(); + const { query } = useRouter(); + const { pid } = query as { pid: string }; + const { addToast } = useToasts(); + + const allProjectWDPAsQuery = useProjectWDPAs(pid, {}); + + const selectedPAs = useMemo(() => { + return allProjectWDPAsQuery.data?.filter(({ id }) => selectedWDPAIds.includes(id)); + }, [allProjectWDPAsQuery.data, selectedWDPAIds]); + + const PAsNames = selectedPAs.map(({ fullName }) => fullName); + + // ? the user will be able to delete the protected areas only if they are not being used by any scenario. + const haveScenarioAssociated = selectedPAs.some(({ scenarioUsageCount }) => + Boolean(scenarioUsageCount) + ); + + const handleBulkDelete = useCallback(() => { + const deletableWDPAsIds = selectedPAs.map(({ id }) => id); + + bulkDeleteWDPAFromProject(pid, deletableWDPAsIds, session) + .then(async () => { + await queryClient.invalidateQueries(['wdpas', pid]); + + onDismiss(); + + addToast( + 'delete-bulk-project-wdpas', + <> +

Success

+

The protected areas were deleted successfully.

+ , + { + level: 'success', + } + ); + }) + .catch(() => { + addToast( + 'delete-bulk-project-wdpas', + <> +

Error!

+

Something went wrong deleting the protected areas

+ , + { + level: 'error', + } + ); + }); + }, [selectedPAs, addToast, onDismiss, pid, queryClient, session]); + + return ( +
+

{`Delete feature${ + selectedWDPAIds.length > 1 ? 's' : '' + }`}

+

+ {selectedWDPAIds.length > 1 ? ( +

+ + Are you sure you want to delete the following protected areas?
+ This action cannot be undone. +
+
    + {PAsNames.map((name) => ( +
  • {name}
  • + ))} +
+
+ ) : ( + + Are you sure you want to delete "{PAsNames[0]}" feature?
+ This action cannot be undone. +
+ )} +

+
+ +

+ A protected area can be deleted ONLY if it's not being used by any scenario +

+
+
+ + +
+
+ ); +}; + +export default DeleteModal; diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index 35bcc87b3a..ea76a0c150 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -121,7 +121,7 @@ const InventoryPanelProtectedAreas = ({ onToggleSeeOnMap={toggleSeeOnMap} ActionsComponent={ActionsMenu} /> - {displayBulkActions && } + {displayBulkActions && }
); }; From 53c6c12b54f600cf3c63fd38f55e091c67d077ff Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Mon, 28 Aug 2023 19:00:36 +0200 Subject: [PATCH 05/28] rname folder --- .../{protected-areas => wdpas}/actions-menu/index.tsx | 0 .../{protected-areas => wdpas}/bulk-action-menu/index.tsx | 4 ++-- .../{protected-areas => wdpas}/bulk-action-menu/utils.ts | 0 .../{protected-areas => wdpas}/modals/delete/index.tsx | 6 +++--- 4 files changed, 5 insertions(+), 5 deletions(-) rename app/layout/project/sidebar/project/inventory-panel/{protected-areas => wdpas}/actions-menu/index.tsx (100%) rename app/layout/project/sidebar/project/inventory-panel/{protected-areas => wdpas}/bulk-action-menu/index.tsx (96%) rename app/layout/project/sidebar/project/inventory-panel/{protected-areas => wdpas}/bulk-action-menu/utils.ts (100%) rename app/layout/project/sidebar/project/inventory-panel/{protected-areas => wdpas}/modals/delete/index.tsx (97%) diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/actions-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx similarity index 100% rename from app/layout/project/sidebar/project/inventory-panel/protected-areas/actions-menu/index.tsx rename to app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx diff --git a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/index.tsx similarity index 96% rename from app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx rename to app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/index.tsx index 181321f693..6442968dfd 100644 --- a/app/layout/project/sidebar/project/inventory-panel/protected-areas/bulk-action-menu/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/index.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import Button from 'components/button'; import Icon from 'components/icon'; import Modal from 'components/modal/component'; -import DeleteModal from 'layout/project/sidebar/project/inventory-panel/protected-areas/modals/delete'; +import DeleteModal from 'layout/project/sidebar/project/inventory-panel/wdpas/modals/delete'; import { WDPA } from 'types/api/wdpa'; import DELETE_SVG from 'svgs/ui/new-layout/delete.svg?sprite'; @@ -48,7 +48,7 @@ const WDPABulkActionMenu = ({
-

{`Delete feature${ +

{`Delete protected area${ selectedWDPAIds.length > 1 ? 's' : '' }`}

@@ -97,7 +97,7 @@ const DeleteModal = ({ ) : ( - Are you sure you want to delete "{PAsNames[0]}" feature?
+ Are you sure you want to delete "{PAsNames[0]}" protected area?
This action cannot be undone.
)} From 8fb75e3e06d6761dacfb5179ff0c280319fb776f Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Mon, 28 Aug 2023 19:34:30 +0200 Subject: [PATCH 06/28] edit protected area --- app/hooks/wdpa/index.ts | 45 +++++- .../wdpas/actions-menu/index.tsx | 12 +- .../wdpas/modals/edit/index.tsx | 133 ++++++++++++++++++ 3 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 21161909d9..e8e265e3b3 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery, QueryObserverOptions } from 'react-query'; +import { useMutation, useQuery, QueryObserverOptions, useQueryClient } from 'react-query'; import { AxiosRequestConfig } from 'axios'; import { useSession } from 'next-auth/react'; @@ -8,6 +8,8 @@ import { Scenario } from 'types/api/scenario'; import { WDPA, WDPACategory } from 'types/api/wdpa'; import { API } from 'services/api'; +import GEOFEATURES from 'services/geo-features'; +import PROJECTS from 'services/projects'; import SCENARIOS from 'services/scenarios'; export function useWDPACategories({ @@ -116,7 +118,7 @@ export function useProjectWDPAs( { id: 'hfgxssshmdgf', wdpaId: 'hfdgjfhdg', - fullName: 'IUCN Ib', + fullName: 'Florida scrub jay', iucnCategory: 'IUCN Ib', countryId: 'mdfgjf', shapeLength: 45, @@ -142,3 +144,42 @@ export function useProjectWDPAs( ...queryOptions, }); } + +export function useEditWDPA({ + requestConfig = { + method: 'PATCH', + }, +}) { + const queryClient = useQueryClient(); + const { data: session } = useSession(); + + const saveProjectWDPA = ({ + projectId, + wdpaId, + data, + }: { + projectId: string; + wdpaId: string; + data: { fullName: string }; + }) => { + return PROJECTS.request({ + method: 'PATCH', + url: `/${projectId}/protected-areas/${wdpaId}`, + data, + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + ...requestConfig, + }); + }; + + return useMutation(saveProjectWDPA, { + onSuccess: async (data, variables) => { + const { projectId } = variables; + await queryClient.invalidateQueries(['wdpas', projectId]); + }, + onError: (error, variables, context) => { + console.info('Error', error, variables, context); + }, + }); +} diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx index 9d5cf5c54d..c4834e7ccc 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx @@ -2,8 +2,8 @@ import { useCallback, useState } from 'react'; import Icon from 'components/icon'; import Modal from 'components/modal/component'; -import DeleteModal from 'layout/project/sidebar/project/inventory-panel/features/modals/delete'; -import EditModal from 'layout/project/sidebar/project/inventory-panel/features/modals/edit'; +import DeleteModal from 'layout/project/sidebar/project/inventory-panel/wdpas/modals/delete'; +import EditModal from 'layout/project/sidebar/project/inventory-panel/wdpas/modals/edit'; import { cn } from 'utils/cn'; import DELETE_SVG from 'svgs/ui/new-layout/delete.svg?sprite'; @@ -53,13 +53,13 @@ const ActionsMenu = ({ Edit handleModal('edit', false)} > - + {isDeletable && ( @@ -86,7 +86,7 @@ const ActionsMenu = ({ handleModal('delete', false); }} > - + )} diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx new file mode 100644 index 0000000000..754a45fbd3 --- /dev/null +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx @@ -0,0 +1,133 @@ +import React, { useCallback, useRef } from 'react'; + +import { Form as FormRFF, Field as FieldRFF, FormProps } from 'react-final-form'; +import { useQueryClient } from 'react-query'; + +import { useRouter } from 'next/router'; + +import { useToasts } from 'hooks/toast'; +import { useEditWDPA, useProjectWDPAs } from 'hooks/wdpa'; + +import Button from 'components/button'; +import Field from 'components/forms/field'; +import Label from 'components/forms/label'; +import { composeValidators } from 'components/forms/validations'; +import { WDPA } from 'types/api/wdpa'; + +export type FormValues = { fullName: WDPA['fullName'] }; + +const EditModal = ({ + wdpaId, + handleModal, +}: { + wdpaId: WDPA['id']; + handleModal: (modalKey: 'delete' | 'edit', isVisible: boolean) => void; +}): JSX.Element => { + const queryClient = useQueryClient(); + const { addToast } = useToasts(); + const { query } = useRouter(); + const { pid } = query as { pid: string }; + + const formRef = useRef['form']>(null); + + const allProjectWDPAsQuery = useProjectWDPAs(pid, {}); + const editWDPAMutation = useEditWDPA({}); + + const onEditSubmit = useCallback( + (values: FormValues) => { + const { fullName } = values; + const editWDPAPromise = editWDPAMutation.mutateAsync({ + wdpaId: wdpaId, + projectId: pid, + data: { + fullName, + }, + }); + + Promise.all([editWDPAPromise]) + .then(async () => { + await queryClient.invalidateQueries(['wdpas', pid]); + handleModal('edit', false); + + addToast( + 'success-edit-wdpa', + <> +

Success!

+

Protected area edited

+ , + { + level: 'success', + } + ); + }) + .catch(() => { + addToast( + 'error-edit-wdpa', + <> +

Error!

+

It is not possible to edit this protected area

+ , + { + level: 'error', + } + ); + }); + }, + [addToast, editWDPAMutation, wdpaId, handleModal, pid, queryClient] + ); + + return ( + + initialValues={{ + fullName: allProjectWDPAsQuery.data?.[0]?.fullName, + }} + ref={formRef} + onSubmit={onEditSubmit} + render={({ form, handleSubmit }) => { + formRef.current = form; + + return ( +
+
+

Edit protected area

+ +
+ + name="fullName" + validate={composeValidators([{ presence: true }])} + > + {(fprops) => ( + + + + + + )} + +
+ +
+ + + +
+
+
+ ); + }} + /> + ); +}; + +export default EditModal; From 4748efae510f56d85f588b07ace40d9bee58e420 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 10:31:41 +0200 Subject: [PATCH 07/28] sort by full name --- .../project/inventory-panel/wdpas/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index ea76a0c150..e5bf9e5c6d 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -28,9 +28,16 @@ const InventoryPanelProtectedAreas = ({ const { query } = useRouter(); const { pid } = query as { pid: string }; + const [selectedWDPAIds, setSelectedWDPAIds] = useState([]); + const [filters, setFilters] = useState[1]>({ + sort: WDPA_TABLE_COLUMNS.name, + }); + const allProjectWDPAsQuery = useProjectWDPAs( pid, - {}, + { + ...filters, + }, { select: (data) => data?.map((wdpa) => ({ @@ -43,11 +50,6 @@ const InventoryPanelProtectedAreas = ({ } ); - const [selectedWDPAIds, setSelectedWDPAIds] = useState([]); - const [filters, setFilters] = useState[1]>({ - sort: WDPA_TABLE_COLUMNS.name, - }); - const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ ...wdpa, name: wdpa.fullName, From abece7809652c54ec0446b3405694fbc6d713a1a Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 11:18:41 +0200 Subject: [PATCH 08/28] fix data item types --- .../project/inventory-panel/components/inventory-table/types.ts | 2 +- .../project/inventory-panel/wdpas/actions-menu/index.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts index 970b61cd98..55942fbe87 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts @@ -3,7 +3,7 @@ import { ChangeEvent } from 'react'; export type DataItem = { id: string; name: string; - scenarios?: number; + scenarios: number; tag?: string; isVisibleOnMap: boolean; isCustom?: boolean; diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx index c4834e7ccc..de307bc624 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/actions-menu/index.tsx @@ -27,7 +27,6 @@ const ActionsMenu = ({ }): JSX.Element => { const isDeletable = !item.custom && !item.scenarios; - // item.isCustom && !item.scenarioUsageCount const [modalState, setModalState] = useState<{ edit: boolean; delete: boolean }>({ edit: false, delete: false, From 064c6363001bbc064d23a1ece470b5e3fd614dce Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 12:02:13 +0200 Subject: [PATCH 09/28] save visible protected areas in store --- .../inventory-panel/features/index.tsx | 1 + .../project/inventory-panel/wdpas/index.tsx | 27 ++++++++++--------- app/store/slices/projects/[id].ts | 12 ++++----- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx index c52a8162e2..68f3abdfdc 100644 --- a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx @@ -32,6 +32,7 @@ const InventoryPanelFeatures = ({ noData: noDataMessage }: { noData: string }): (state) => state['/projects/[id]'] ); + console.log({ visibleFeatures }); const [filters, setFilters] = useState[1]>({ sort: FEATURES_TABLE_COLUMNS[0].name, }); diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index e5bf9e5c6d..bf39e87145 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -3,7 +3,7 @@ import { useState, useCallback, useEffect, ChangeEvent } from 'react'; import { useRouter } from 'next/router'; import { useAppDispatch, useAppSelector } from 'store/hooks'; -import { setSelectedWDPA } from 'store/slices/projects/[id]'; +import { setSelectedWDPAs as setVisibleWDPAs } from 'store/slices/projects/[id]'; import { useProjectWDPAs } from 'hooks/wdpa'; @@ -24,7 +24,10 @@ const InventoryPanelProtectedAreas = ({ noData: string; }): JSX.Element => { const dispatch = useAppDispatch(); - const { selectedWDPA, search } = useAppSelector((state) => state['/projects/[id]']); + const { selectedWDPAs: visibleWDPAs, search } = useAppSelector( + (state) => state['/projects/[id]'] + ); + const { query } = useRouter(); const { pid } = query as { pid: string }; @@ -50,13 +53,6 @@ const InventoryPanelProtectedAreas = ({ } ); - const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ - ...wdpa, - name: wdpa.fullName, - scenarios: wdpa.scenarioUsageCount, - isVisibleOnMap: selectedWDPA?.includes(wdpa.id), - })); - const WDPAIds = allProjectWDPAsQuery.data?.map((wdpa) => wdpa.id); const handleSelectAll = useCallback( @@ -78,16 +74,16 @@ const InventoryPanelProtectedAreas = ({ const toggleSeeOnMap = useCallback( (WDPAId: WDPA['id']) => { - const newSelectedWDPAs = [...selectedWDPA]; + const newSelectedWDPAs = [...visibleWDPAs]; if (!newSelectedWDPAs.includes(WDPAId)) { newSelectedWDPAs.push(WDPAId); } else { const i = newSelectedWDPAs.indexOf(WDPAId); newSelectedWDPAs.splice(i, 1); } - dispatch(setSelectedWDPA(newSelectedWDPAs)); + dispatch(setVisibleWDPAs(newSelectedWDPAs)); }, - [dispatch, selectedWDPA] + [dispatch, visibleWDPAs] ); const handleSort = useCallback( @@ -108,6 +104,13 @@ const InventoryPanelProtectedAreas = ({ setSelectedWDPAIds([]); }, [search]); + const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ + ...wdpa, + name: wdpa.fullName, + scenarios: wdpa.scenarioUsageCount, + isVisibleOnMap: visibleWDPAs?.includes(wdpa.id), + })); + return (
; selectedCostSurface: CostSurface['id'][]; selectedFeatures: Feature['id'][]; - selectedWDPA: WDPA['id'][]; + selectedWDPAs: WDPA['id'][]; isSidebarOpen: boolean; } @@ -22,7 +22,7 @@ const initialState: ProjectShowStateProps = { layerSettings: {}, selectedFeatures: [], selectedCostSurface: [], - selectedWDPA: [], + selectedWDPAs: [], isSidebarOpen: true, } satisfies ProjectShowStateProps; @@ -77,9 +77,9 @@ const projectsDetailSlice = createSlice({ ) => { state.selectedCostSurface = action.payload; }, - // WDPA - setSelectedWDPA: (state, action: PayloadAction) => { - state.selectedFeatures = action.payload; + // WDPAs + setSelectedWDPAs: (state, action: PayloadAction) => { + state.selectedWDPAs = action.payload; }, }, }); @@ -91,7 +91,7 @@ export const { setLayerSettings, setSelectedFeatures, setSelectedCostSurface, - setSelectedWDPA, + setSelectedWDPAs, setSidebarVisibility, } = projectsDetailSlice.actions; From 843268fbee9fa925ff86d7f68ab3554ff16c28b2 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 12:42:13 +0200 Subject: [PATCH 10/28] show preview protected areas on inventory map --- app/hooks/map/index.ts | 1 - app/hooks/wdpa/index.ts | 20 ++++++++-------- .../inventory-panel/features/index.tsx | 1 - app/layout/projects/show/map/index.tsx | 23 ++++++++++++++++--- app/store/slices/projects/[id].ts | 4 ++-- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/app/hooks/map/index.ts b/app/hooks/map/index.ts index af12fe8619..1fbe551d07 100644 --- a/app/hooks/map/index.ts +++ b/app/hooks/map/index.ts @@ -181,7 +181,6 @@ export function useWDPAPreviewLayer({ options, }: UseWDPAPreviewLayer) { const { opacity = 1, visibility = true } = options || {}; - return useMemo(() => { if (!active || !bbox) return null; diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index e8e265e3b3..828ffacce1 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -92,9 +92,9 @@ export function useProjectWDPAs( const mockData: WDPA[] = [ { - id: 'hgdfghmdgf', - wdpaId: 'hfdgjfhdg', - fullName: 'IUCN Category', + id: 'Not Reported', + wdpaId: 'Not Reported', + fullName: 'Not Reported', iucnCategory: 'IUCN Category', countryId: 'hgdfjkd', shapeLength: 45, @@ -104,10 +104,10 @@ export function useProjectWDPAs( scenarioUsageCount: 3, }, { - id: 'hfghmdgf', - wdpaId: 'hfdgjfhdg', - fullName: 'IUCN Ia', - iucnCategory: 'IUCN Ia', + id: 'IV', + wdpaId: 'IV', + fullName: 'IUCN IV', + iucnCategory: 'IUCN IV', countryId: 'mdfgjf', shapeLength: 45, shapeArea: 30, @@ -116,10 +116,10 @@ export function useProjectWDPAs( scenarioUsageCount: 0, }, { - id: 'hfgxssshmdgf', - wdpaId: 'hfdgjfhdg', + id: 'II', + wdpaId: 'II', fullName: 'Florida scrub jay', - iucnCategory: 'IUCN Ib', + iucnCategory: 'IUCN II', countryId: 'mdfgjf', shapeLength: 45, shapeArea: 30, diff --git a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx index 68f3abdfdc..c52a8162e2 100644 --- a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx @@ -32,7 +32,6 @@ const InventoryPanelFeatures = ({ noData: noDataMessage }: { noData: string }): (state) => state['/projects/[id]'] ); - console.log({ visibleFeatures }); const [filters, setFilters] = useState[1]>({ sort: FEATURES_TABLE_COLUMNS[0].name, }); diff --git a/app/layout/projects/show/map/index.tsx b/app/layout/projects/show/map/index.tsx index 717a2187d4..ff001b3b19 100644 --- a/app/layout/projects/show/map/index.tsx +++ b/app/layout/projects/show/map/index.tsx @@ -20,6 +20,7 @@ import { useProjectPlanningAreaLayer, useBBOX, useFeaturePreviewLayers, + useWDPAPreviewLayer, } from 'hooks/map'; import { useDownloadScenarioComparisonReport, useProject } from 'hooks/projects'; import { useScenarios } from 'hooks/scenarios'; @@ -54,6 +55,7 @@ export const ProjectMap = (): JSX.Element => { layerSettings, selectedFeatures: selectedFeaturesIds, selectedCostSurface: selectedCostSurfaceId, + selectedWDPAs: selectedWDPAsIds, } = useAppSelector((state) => state['/projects/[id]']); const accessToken = useAccessToken(); @@ -144,6 +146,16 @@ export const ProjectMap = (): JSX.Element => { }, }); + const WDPAsPreviewLayers = useWDPAPreviewLayer({ + wdpaIucnCategories: selectedWDPAsIds, + pid: `${pid}`, + active: true, + bbox, + options: { + ...layerSettings['wdpa-preview'], + }, + }); + const selectedPreviewFeatures = useMemo(() => { return selectedFeaturesData ?.map(({ featureClassName, id }) => ({ name: featureClassName, id })) @@ -170,14 +182,19 @@ export const ProjectMap = (): JSX.Element => { } ); - const LAYERS = [PUGridLayer, PUCompareLayer, PlanningAreaLayer, ...FeaturePreviewLayers].filter( - (l) => !!l - ); + const LAYERS = [ + PUGridLayer, + PUCompareLayer, + PlanningAreaLayer, + WDPAsPreviewLayers, + ...FeaturePreviewLayers, + ].filter((l) => !!l); const LEGEND = useLegend({ layers: [ ...(!!selectedFeaturesData?.length ? ['features-preview'] : []), ...(!!selectedCostSurfaceId ? ['cost'] : []), + ...(!!selectedWDPAsIds?.length ? ['wdpa-preview'] : []), ...(!!sid1 && !sid2 ? ['frequency'] : []), ...(!!sid1 && !!sid2 ? ['compare'] : []), diff --git a/app/store/slices/projects/[id].ts b/app/store/slices/projects/[id].ts index 395f1daa0f..06bbb4f6eb 100644 --- a/app/store/slices/projects/[id].ts +++ b/app/store/slices/projects/[id].ts @@ -9,7 +9,7 @@ interface ProjectShowStateProps { filters: Record | []; sort: string; layerSettings: Record; - selectedCostSurface: CostSurface['id'][]; + selectedCostSurface: CostSurface['id']; selectedFeatures: Feature['id'][]; selectedWDPAs: WDPA['id'][]; isSidebarOpen: boolean; @@ -21,7 +21,7 @@ const initialState: ProjectShowStateProps = { sort: '-lastModifiedAt', layerSettings: {}, selectedFeatures: [], - selectedCostSurface: [], + selectedCostSurface: null, selectedWDPAs: [], isSidebarOpen: true, } satisfies ProjectShowStateProps; From f1c42ab4f9442e73be74ee3cf171aa171ff19427 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 13:47:30 +0200 Subject: [PATCH 11/28] protected areas modal ui --- app/layout/info/upload-wdpas.tsx | 24 ++ .../project/inventory-panel/constants.ts | 2 + .../wdpas/modals/upload/index.tsx | 395 ++++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100644 app/layout/info/upload-wdpas.tsx create mode 100644 app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx diff --git a/app/layout/info/upload-wdpas.tsx b/app/layout/info/upload-wdpas.tsx new file mode 100644 index 0000000000..491be9a9d6 --- /dev/null +++ b/app/layout/info/upload-wdpas.tsx @@ -0,0 +1,24 @@ +export const UploadWDPAsInfoButtonContent = (): JSX.Element => { + return ( +
+

+ When uploading shapefiles of protected areas, please make sure that: +

+
    +
  • this is a single zip file that includes all the components of a single shapefile;
  • +
  • + all the components are added to the “root”/top-level of the zip file itself (that is, not + within any folder within the zip file); +
  • +
  • + user-defined shapefile attributes are only considered for shapefiles of features, while + they are ignored for any other kind of shapefile (planning grid, lock-in/out, etc), so you + may consider excluding any attributes from shapefiles other than for features, in order to + keep the shapefile’s file size as small as possible. +
  • +
+
+ ); +}; + +export default UploadWDPAsInfoButtonContent; diff --git a/app/layout/project/sidebar/project/inventory-panel/constants.ts b/app/layout/project/sidebar/project/inventory-panel/constants.ts index 9aedb32c9e..2daa375733 100644 --- a/app/layout/project/sidebar/project/inventory-panel/constants.ts +++ b/app/layout/project/sidebar/project/inventory-panel/constants.ts @@ -9,6 +9,7 @@ import FeatureUploadModal from './features/modals/upload'; import { InventoryPanel } from './types'; import ProtectedAreasTable from './wdpas'; import ProtectedAreasFooter from './wdpas/footer'; +import WDPAUploadModal from './wdpas/modals/upload'; export const INVENTORY_TABS = { 'protected-areas': { @@ -17,6 +18,7 @@ export const INVENTORY_TABS = { noData: 'No protected areas found.', TableComponent: ProtectedAreasTable, FooterComponent: ProtectedAreasFooter, + UploadModalComponent: WDPAUploadModal, }, 'cost-surface': { title: 'Cost Surface', diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx new file mode 100644 index 0000000000..8bbba7613c --- /dev/null +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx @@ -0,0 +1,395 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import { useDropzone, DropzoneProps } from 'react-dropzone'; +import { Form as FormRFF, Field as FieldRFF, FormProps } from 'react-final-form'; + +import { useRouter } from 'next/router'; + +import { AxiosError, isAxiosError } from 'axios'; +import { motion } from 'framer-motion'; + +import { useUploadFeaturesCSV, useUploadFeaturesShapefile } from 'hooks/features'; +import { useDownloadShapefileTemplate } from 'hooks/projects'; +import { useToasts } from 'hooks/toast'; + +import Button from 'components/button'; +import Field from 'components/forms/field'; +import Input from 'components/forms/input'; +import Label from 'components/forms/label'; +import { composeValidators } from 'components/forms/validations'; +import Icon from 'components/icon'; +import InfoButton from 'components/info-button'; +import Loading from 'components/loading'; +import Modal from 'components/modal'; +import UploadTabs from 'components/upload-tabs'; +import { + FEATURES_UPLOADER_SHAPEFILE_MAX_SIZE, + FEATURES_UPLOADER_CSV_MAX_SIZE, +} from 'constants/file-uploader-size-limits'; +import UploadWDPAsInfoButtonContent from 'constants/info-button-content/upload-wdpas'; +import { WDPA } from 'types/api/wdpa'; +import { cn } from 'utils/cn'; +import { bytesToMegabytes } from 'utils/units'; + +import CLOSE_SVG from 'svgs/ui/close.svg?sprite'; + +export type FormValues = { + name: WDPA['fullName']; + file: File; +}; + +export const WDPAUploadModal = ({ + isOpen = false, + onDismiss, +}: { + isOpen?: boolean; + onDismiss: () => void; +}): JSX.Element => { + const formRef = useRef['form']>(null); + + const [loading, setLoading] = useState(false); + const [successFile, setSuccessFile] = useState<{ name: FormValues['name'] }>(null); + const [uploadMode, saveUploadMode] = useState<'shapefile' | 'csv'>('shapefile'); + + const { query } = useRouter(); + const { pid } = query as { pid: string }; + + const { addToast } = useToasts(); + + const uploadFeaturesShapefileMutation = useUploadFeaturesShapefile({ + requestConfig: { + method: 'POST', + }, + }); + + const uploadFeaturesCSVMutation = useUploadFeaturesCSV({}); + + const downloadShapefileTemplateMutation = useDownloadShapefileTemplate(); + + const UPLOADER_MAX_SIZE = + uploadMode === 'shapefile' + ? FEATURES_UPLOADER_SHAPEFILE_MAX_SIZE + : FEATURES_UPLOADER_CSV_MAX_SIZE; + + useEffect(() => { + return () => { + setSuccessFile(null); + }; + }, []); + + const onClose = useCallback(() => { + onDismiss(); + setSuccessFile(null); + }, [onDismiss]); + + const onDropAccepted = (acceptedFiles: Parameters[0]) => { + const f = acceptedFiles[0]; + setSuccessFile({ name: f.name }); + + formRef.current.change('file', f); + }; + + const onDropRejected = (rejectedFiles: Parameters[0]) => { + const r = rejectedFiles[0]; + + // `file-too-large` backend error message is not friendly. + // It'll display the max size in bytes which the average user may not understand. + const errors = r.errors.map((error) => { + return error.code === 'file-too-large' + ? { + ...error, + message: `File is larger than ${bytesToMegabytes(UPLOADER_MAX_SIZE)} MB`, + } + : error; + }); + + addToast( + 'drop-error', + <> +

Error!

+
    + {errors.map((e) => ( +
  • {e.message}
  • + ))} +
+ , + { + level: 'error', + } + ); + }; + + const onUploadSubmit = useCallback( + (values: FormValues) => { + setLoading(true); + const { file, name } = values; + + const data = new FormData(); + + data.append('file', file); + data.append('name', name); + + const mutationResponse = { + onSuccess: () => { + setSuccessFile({ ...successFile }); + onClose(); + addToast( + 'success-upload-wdpa-file', + <> +

Success!

+

File uploaded

+ , + { + level: 'success', + } + ); + }, + onError: (error: AxiosError | Error) => { + let errors: { status: number; title: string }[] = []; + + if (isAxiosError(error)) { + errors = [...error.response.data.errors]; + } else { + // ? in case of unknown error (not request error), display generic error message + errors = [{ status: 500, title: 'Something went wrong' }]; + } + + setSuccessFile(null); + + addToast( + 'error-upload-wdpa-csv', + <> +

Error

+
    + {errors.map((e) => ( +
  • {e.title}
  • + ))} +
+ , + { + level: 'error', + } + ); + }, + onSettled: () => { + setLoading(false); + }, + }; + + if (uploadMode === 'shapefile') { + uploadFeaturesShapefileMutation.mutate({ data, id: `${pid}` }, mutationResponse); + } + + if (uploadMode === 'csv') { + uploadFeaturesCSVMutation.mutate({ data, id: `${pid}` }, mutationResponse); + } + }, + [ + pid, + addToast, + onClose, + uploadMode, + uploadFeaturesShapefileMutation, + uploadFeaturesCSVMutation, + successFile, + ] + ); + + const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({ + multiple: false, + maxSize: UPLOADER_MAX_SIZE, + onDropAccepted, + onDropRejected, + }); + + const onDownloadTemplate = useCallback(() => { + downloadShapefileTemplateMutation.mutate( + { pid }, + { + onError: () => { + addToast( + 'download-error', + <> +

Error!

+
    Template not downloaded
+ , + { + level: 'error', + } + ); + }, + } + ); + }, [pid, downloadShapefileTemplateMutation, addToast]); + + return ( + + + initialValues={{ + name: '', + }} + ref={formRef} + onSubmit={onUploadSubmit} + render={({ form, handleSubmit }) => { + formRef.current = form; + + return ( +
+
+
+

Upload protected area

+ + + +
+ + saveUploadMode(mode)} /> + {uploadMode === 'csv' && ( +

+ Please download and fill in the{' '} + {' '} + before upload. +

+ )} + + {uploadMode === 'shapefile' && ( +
+ + {(fprops) => ( + + + + + )} + +
+ )} + + {!successFile && ( +
+ + + {(props) => ( +
+
+ + +

+ Drag and drop your{' '} + {uploadMode === 'shapefile' + ? 'polygon data file' + : 'protected area file'} +
+ or click here to upload +

+ +

{`Recommended file size < ${bytesToMegabytes( + UPLOADER_MAX_SIZE + )} MB`}

+ + +
+ +
+
Supported formats
+ + + {' '} +

+ List of supported file formats: +

+
    + Zipped: .shp (zipped shapefiles must include +
    + .shp, .shx, .dbf, and .prj files) +
+
+
+
+
+ )} +
+
+ )} + + {successFile && ( + +
+
Uploaded file:
+
+ + +
+
+
+ )} + +
+ + + +
+
+
+ ); + }} + /> + +
+ ); +}; + +export default WDPAUploadModal; From 155741e935951787cd4a97fce3a31da1b9192dbc Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 14:09:24 +0200 Subject: [PATCH 12/28] upload shapefile and csv wdpas hooks --- app/constants/file-uploader-size-limits.js | 3 +- app/hooks/wdpa/index.ts | 61 +++++++++++++++++++ .../wdpas/modals/upload/index.tsx | 25 ++++---- .../categories/pa-uploader/index.tsx | 10 +-- 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/app/constants/file-uploader-size-limits.js b/app/constants/file-uploader-size-limits.js index d8028b331e..10786d5233 100644 --- a/app/constants/file-uploader-size-limits.js +++ b/app/constants/file-uploader-size-limits.js @@ -8,7 +8,8 @@ export const PLANNING_UNIT_UPLOADER_MAX_SIZE = 1048576; // 1MiB export const PLANNING_AREA_UPLOADER_MAX_SIZE = 1048576; // 1MiB export const PLANNING_AREA_GRID_UPLOADER_MAX_SIZE = 10485760; // 10MiB -export const PROTECTED_AREA_UPLOADER_MAX_SIZE = 10485760; // 10MiB +export const PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE = 10485760; // 10MiB +export const PROTECTED_AREA_UPLOADER_CSV_MAX_SIZE = 10485760; // 10MiB export const FEATURES_UPLOADER_SHAPEFILE_MAX_SIZE = 20971520; // 20MiB export const FEATURES_UPLOADER_CSV_MAX_SIZE = 52428800; // 50MiB export const COST_SURFACE_UPLOADER_MAX_SIZE = 20971520; // 20MiB diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 828ffacce1..5125c61d6d 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -11,6 +11,7 @@ import { API } from 'services/api'; import GEOFEATURES from 'services/geo-features'; import PROJECTS from 'services/projects'; import SCENARIOS from 'services/scenarios'; +import UPLOADS from 'services/uploads'; export function useWDPACategories({ adminAreaId, @@ -183,3 +184,63 @@ export function useEditWDPA({ }, }); } + +export function useUploadWDPAsShapefile({ + requestConfig = { + method: 'POST', + }, +}: { + requestConfig?: AxiosRequestConfig; +}) { + const queryClient = useQueryClient(); + const { data: session } = useSession(); + + const uploadWDPAShapefile = ({ id, data }: { id: Project['id']; data: FormData }) => { + return UPLOADS.request<{ success: true }>({ + url: `/projects/${id}/protected-areas/shapefile`, + data, + headers: { + Authorization: `Bearer ${session.accessToken}`, + 'Content-Type': 'multipart/form-data', + }, + ...requestConfig, + } as typeof requestConfig); + }; + + return useMutation(uploadWDPAShapefile, { + onSuccess: async (data, variables) => { + const { id: projectId } = variables; + await queryClient.invalidateQueries(['wdpas', projectId]); + }, + }); +} + +export function useUploadWDPAsCSV({ + requestConfig = { + method: 'POST', + }, +}: { + requestConfig?: AxiosRequestConfig; +}) { + const queryClient = useQueryClient(); + const { data: session } = useSession(); + + const uploadWDPACSV = ({ id, data }: { id: Project['id']; data: FormData }) => { + return UPLOADS.request<{ success: true }>({ + url: `/projects/${id}/protected-areas/csv`, + data, + headers: { + Authorization: `Bearer ${session.accessToken}`, + 'Content-Type': 'multipart/form-data', + }, + ...requestConfig, + } as typeof requestConfig); + }; + + return useMutation(uploadWDPACSV, { + onSuccess: async (data, variables) => { + const { id: projectId } = variables; + await queryClient.invalidateQueries(['wdpas', projectId]); + }, + }); +} diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx index 8bbba7613c..a5bdf3cecf 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx @@ -11,6 +11,7 @@ import { motion } from 'framer-motion'; import { useUploadFeaturesCSV, useUploadFeaturesShapefile } from 'hooks/features'; import { useDownloadShapefileTemplate } from 'hooks/projects'; import { useToasts } from 'hooks/toast'; +import { useUploadWDPAsCSV, useUploadWDPAsShapefile } from 'hooks/wdpa'; import Button from 'components/button'; import Field from 'components/forms/field'; @@ -23,8 +24,8 @@ import Loading from 'components/loading'; import Modal from 'components/modal'; import UploadTabs from 'components/upload-tabs'; import { - FEATURES_UPLOADER_SHAPEFILE_MAX_SIZE, - FEATURES_UPLOADER_CSV_MAX_SIZE, + PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE, + PROTECTED_AREA_UPLOADER_CSV_MAX_SIZE, } from 'constants/file-uploader-size-limits'; import UploadWDPAsInfoButtonContent from 'constants/info-button-content/upload-wdpas'; import { WDPA } from 'types/api/wdpa'; @@ -56,20 +57,16 @@ export const WDPAUploadModal = ({ const { addToast } = useToasts(); - const uploadFeaturesShapefileMutation = useUploadFeaturesShapefile({ - requestConfig: { - method: 'POST', - }, - }); + const uploadWDPAsShapefileMutation = useUploadWDPAsShapefile({}); - const uploadFeaturesCSVMutation = useUploadFeaturesCSV({}); + const uploadWDPAsCSVMutation = useUploadWDPAsCSV({}); const downloadShapefileTemplateMutation = useDownloadShapefileTemplate(); const UPLOADER_MAX_SIZE = uploadMode === 'shapefile' - ? FEATURES_UPLOADER_SHAPEFILE_MAX_SIZE - : FEATURES_UPLOADER_CSV_MAX_SIZE; + ? PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE + : PROTECTED_AREA_UPLOADER_CSV_MAX_SIZE; useEffect(() => { return () => { @@ -177,11 +174,11 @@ export const WDPAUploadModal = ({ }; if (uploadMode === 'shapefile') { - uploadFeaturesShapefileMutation.mutate({ data, id: `${pid}` }, mutationResponse); + uploadWDPAsShapefileMutation.mutate({ data, id: `${pid}` }, mutationResponse); } if (uploadMode === 'csv') { - uploadFeaturesCSVMutation.mutate({ data, id: `${pid}` }, mutationResponse); + uploadWDPAsCSVMutation.mutate({ data, id: `${pid}` }, mutationResponse); } }, [ @@ -189,8 +186,8 @@ export const WDPAUploadModal = ({ addToast, onClose, uploadMode, - uploadFeaturesShapefileMutation, - uploadFeaturesCSVMutation, + uploadWDPAsShapefileMutation, + uploadWDPAsCSVMutation, successFile, ] ); diff --git a/app/layout/project/sidebar/scenario/grid-setup/protected-areas/categories/pa-uploader/index.tsx b/app/layout/project/sidebar/scenario/grid-setup/protected-areas/categories/pa-uploader/index.tsx index b1ebb59f46..69e86bea4d 100644 --- a/app/layout/project/sidebar/scenario/grid-setup/protected-areas/categories/pa-uploader/index.tsx +++ b/app/layout/project/sidebar/scenario/grid-setup/protected-areas/categories/pa-uploader/index.tsx @@ -24,7 +24,7 @@ import Icon from 'components/icon'; import InfoButton from 'components/info-button'; import Loading from 'components/loading'; import Uploader from 'components/uploader'; -import { PROTECTED_AREA_UPLOADER_MAX_SIZE } from 'constants/file-uploader-size-limits'; +import { PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE } from 'constants/file-uploader-size-limits'; import { cn } from 'utils/cn'; import { bytesToMegabytes } from 'utils/units'; @@ -89,7 +89,9 @@ export const ProtectedAreaUploader: React.FC = ({ return error.code === 'file-too-large' ? { ...error, - message: `File is larger than ${bytesToMegabytes(PROTECTED_AREA_UPLOADER_MAX_SIZE)} MB`, + message: `File is larger than ${bytesToMegabytes( + PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE + )} MB`, } : error; }); @@ -165,7 +167,7 @@ export const ProtectedAreaUploader: React.FC = ({ const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({ multiple: false, - maxSize: PROTECTED_AREA_UPLOADER_MAX_SIZE, + maxSize: PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE, onDropAccepted, onDropRejected, }); @@ -259,7 +261,7 @@ export const ProtectedAreaUploader: React.FC = ({

{`Recommended file size < ${bytesToMegabytes( - PROTECTED_AREA_UPLOADER_MAX_SIZE + PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE )} MB`}

Date: Tue, 29 Aug 2023 15:08:01 +0200 Subject: [PATCH 13/28] add search to params --- app/hooks/wdpa/index.ts | 3 +-- .../sidebar/project/inventory-panel/wdpas/index.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 5125c61d6d..b7131017c2 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -8,7 +8,6 @@ import { Scenario } from 'types/api/scenario'; import { WDPA, WDPACategory } from 'types/api/wdpa'; import { API } from 'services/api'; -import GEOFEATURES from 'services/geo-features'; import PROJECTS from 'services/projects'; import SCENARIOS from 'services/scenarios'; import UPLOADS from 'services/uploads'; @@ -86,7 +85,7 @@ export function useSaveScenarioProtectedAreas({ export function useProjectWDPAs( pid: Project['id'], - params: { sort?: string } = {}, + params: { search?: string; sort?: string; filters?: Record } = {}, queryOptions: QueryObserverOptions = {} ) { const { data: session } = useSession(); diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index bf39e87145..18901b65fc 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -40,6 +40,7 @@ const InventoryPanelProtectedAreas = ({ pid, { ...filters, + search, }, { select: (data) => @@ -72,6 +73,10 @@ const InventoryPanelProtectedAreas = ({ } }, []); + useEffect(() => { + setSelectedWDPAIds([]); + }, [search]); + const toggleSeeOnMap = useCallback( (WDPAId: WDPA['id']) => { const newSelectedWDPAs = [...visibleWDPAs]; @@ -100,10 +105,6 @@ const InventoryPanelProtectedAreas = ({ const displayBulkActions = selectedWDPAIds.length > 0; - useEffect(() => { - setSelectedWDPAIds([]); - }, [search]); - const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ ...wdpa, name: wdpa.fullName, From 116dc9a82db254bace141b120748ce8f11aced87 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 29 Aug 2023 15:49:49 +0200 Subject: [PATCH 14/28] remove csv uploader --- app/constants/file-uploader-size-limits.js | 1 - app/hooks/wdpa/index.ts | 30 ----- .../wdpas/modals/upload/index.tsx | 111 ++++-------------- 3 files changed, 22 insertions(+), 120 deletions(-) diff --git a/app/constants/file-uploader-size-limits.js b/app/constants/file-uploader-size-limits.js index 10786d5233..0f09a849b8 100644 --- a/app/constants/file-uploader-size-limits.js +++ b/app/constants/file-uploader-size-limits.js @@ -9,7 +9,6 @@ export const PLANNING_UNIT_UPLOADER_MAX_SIZE = 1048576; // 1MiB export const PLANNING_AREA_UPLOADER_MAX_SIZE = 1048576; // 1MiB export const PLANNING_AREA_GRID_UPLOADER_MAX_SIZE = 10485760; // 10MiB export const PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE = 10485760; // 10MiB -export const PROTECTED_AREA_UPLOADER_CSV_MAX_SIZE = 10485760; // 10MiB export const FEATURES_UPLOADER_SHAPEFILE_MAX_SIZE = 20971520; // 20MiB export const FEATURES_UPLOADER_CSV_MAX_SIZE = 52428800; // 50MiB export const COST_SURFACE_UPLOADER_MAX_SIZE = 20971520; // 20MiB diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index b7131017c2..a2f557ebcb 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -213,33 +213,3 @@ export function useUploadWDPAsShapefile({ }, }); } - -export function useUploadWDPAsCSV({ - requestConfig = { - method: 'POST', - }, -}: { - requestConfig?: AxiosRequestConfig; -}) { - const queryClient = useQueryClient(); - const { data: session } = useSession(); - - const uploadWDPACSV = ({ id, data }: { id: Project['id']; data: FormData }) => { - return UPLOADS.request<{ success: true }>({ - url: `/projects/${id}/protected-areas/csv`, - data, - headers: { - Authorization: `Bearer ${session.accessToken}`, - 'Content-Type': 'multipart/form-data', - }, - ...requestConfig, - } as typeof requestConfig); - }; - - return useMutation(uploadWDPACSV, { - onSuccess: async (data, variables) => { - const { id: projectId } = variables; - await queryClient.invalidateQueries(['wdpas', projectId]); - }, - }); -} diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx index a5bdf3cecf..100ab4177e 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx @@ -8,10 +8,8 @@ import { useRouter } from 'next/router'; import { AxiosError, isAxiosError } from 'axios'; import { motion } from 'framer-motion'; -import { useUploadFeaturesCSV, useUploadFeaturesShapefile } from 'hooks/features'; -import { useDownloadShapefileTemplate } from 'hooks/projects'; import { useToasts } from 'hooks/toast'; -import { useUploadWDPAsCSV, useUploadWDPAsShapefile } from 'hooks/wdpa'; +import { useUploadWDPAsShapefile } from 'hooks/wdpa'; import Button from 'components/button'; import Field from 'components/forms/field'; @@ -22,11 +20,7 @@ import Icon from 'components/icon'; import InfoButton from 'components/info-button'; import Loading from 'components/loading'; import Modal from 'components/modal'; -import UploadTabs from 'components/upload-tabs'; -import { - PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE, - PROTECTED_AREA_UPLOADER_CSV_MAX_SIZE, -} from 'constants/file-uploader-size-limits'; +import { PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE } from 'constants/file-uploader-size-limits'; import UploadWDPAsInfoButtonContent from 'constants/info-button-content/upload-wdpas'; import { WDPA } from 'types/api/wdpa'; import { cn } from 'utils/cn'; @@ -50,7 +44,6 @@ export const WDPAUploadModal = ({ const [loading, setLoading] = useState(false); const [successFile, setSuccessFile] = useState<{ name: FormValues['name'] }>(null); - const [uploadMode, saveUploadMode] = useState<'shapefile' | 'csv'>('shapefile'); const { query } = useRouter(); const { pid } = query as { pid: string }; @@ -59,15 +52,6 @@ export const WDPAUploadModal = ({ const uploadWDPAsShapefileMutation = useUploadWDPAsShapefile({}); - const uploadWDPAsCSVMutation = useUploadWDPAsCSV({}); - - const downloadShapefileTemplateMutation = useDownloadShapefileTemplate(); - - const UPLOADER_MAX_SIZE = - uploadMode === 'shapefile' - ? PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE - : PROTECTED_AREA_UPLOADER_CSV_MAX_SIZE; - useEffect(() => { return () => { setSuccessFile(null); @@ -95,7 +79,9 @@ export const WDPAUploadModal = ({ return error.code === 'file-too-large' ? { ...error, - message: `File is larger than ${bytesToMegabytes(UPLOADER_MAX_SIZE)} MB`, + message: `File is larger than ${bytesToMegabytes( + PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE + )} MB`, } : error; }); @@ -173,52 +159,18 @@ export const WDPAUploadModal = ({ }, }; - if (uploadMode === 'shapefile') { - uploadWDPAsShapefileMutation.mutate({ data, id: `${pid}` }, mutationResponse); - } - - if (uploadMode === 'csv') { - uploadWDPAsCSVMutation.mutate({ data, id: `${pid}` }, mutationResponse); - } + uploadWDPAsShapefileMutation.mutate({ data, id: `${pid}` }, mutationResponse); }, - [ - pid, - addToast, - onClose, - uploadMode, - uploadWDPAsShapefileMutation, - uploadWDPAsCSVMutation, - successFile, - ] + [pid, addToast, onClose, uploadWDPAsShapefileMutation, successFile] ); const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({ multiple: false, - maxSize: UPLOADER_MAX_SIZE, + maxSize: PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE, onDropAccepted, onDropRejected, }); - const onDownloadTemplate = useCallback(() => { - downloadShapefileTemplateMutation.mutate( - { pid }, - { - onError: () => { - addToast( - 'download-error', - <> -

Error!

-
    Template not downloaded
- , - { - level: 'error', - } - ); - }, - } - ); - }, [pid, downloadShapefileTemplateMutation, addToast]); - return ( @@ -240,34 +192,18 @@ export const WDPAUploadModal = ({
- saveUploadMode(mode)} /> - {uploadMode === 'csv' && ( -

- Please download and fill in the{' '} - {' '} - before upload. -

- )} - - {uploadMode === 'shapefile' && ( -
- - {(fprops) => ( - - - - - )} - -
- )} +
+ + {(fprops) => ( + + + + + )} + +
{!successFile && (
@@ -292,16 +228,13 @@ export const WDPAUploadModal = ({

- Drag and drop your{' '} - {uploadMode === 'shapefile' - ? 'polygon data file' - : 'protected area file'} + Drag and drop your polygon data file
or click here to upload

{`Recommended file size < ${bytesToMegabytes( - UPLOADER_MAX_SIZE + PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE )} MB`}

Date: Tue, 29 Aug 2023 16:55:02 +0200 Subject: [PATCH 15/28] fix naming on toggle map --- .../project/sidebar/project/inventory-panel/wdpas/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index 18901b65fc..d1e4de5f33 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -65,10 +65,10 @@ const InventoryPanelProtectedAreas = ({ const handleSelectWDPA = useCallback((evt: ChangeEvent) => { if (evt.target.checked) { - setSelectedWDPAIds((prevSelectedFeatures) => [...prevSelectedFeatures, evt.target.value]); + setSelectedWDPAIds((prevSelectedWDPAs) => [...prevSelectedWDPAs, evt.target.value]); } else { - setSelectedWDPAIds((prevSelectedFeatures) => - prevSelectedFeatures.filter((featureId) => featureId !== evt.target.value) + setSelectedWDPAIds((prevSelectedWDPAs) => + prevSelectedWDPAs.filter((wdpaId) => wdpaId !== evt.target.value) ); } }, []); From fd77e57784c649ac8b0e6741f76fee2b44f1213f Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Wed, 30 Aug 2023 18:33:03 +0200 Subject: [PATCH 16/28] header item scalability --- .../components/inventory-table/index.tsx | 2 +- .../project/inventory-panel/features/index.tsx | 1 - .../sidebar/project/inventory-panel/wdpas/index.tsx | 11 +++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx index 70aaa7781d..bbd2031b44 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx @@ -51,7 +51,7 @@ const InventoryTable = ({ key={column.name} className={cn({ 'flex-1 pl-2': true, - [column.className]: !!column.className, + 'flex flex-1 justify-start py-2 pl-14': column.name === 'tag', })} > { const sort = filters.sort === _sortType ? `-${_sortType}` : _sortType; - setFilters((prevFilters) => ({ ...prevFilters, sort, diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index d1e4de5f33..effca09211 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -14,9 +14,12 @@ import InventoryTable, { type DataItem } from '../components/inventory-table'; import ActionsMenu from './actions-menu'; import WDPABulkActionMenu from './bulk-action-menu'; -const WDPA_TABLE_COLUMNS = { - name: 'fullName', -}; +const WDPA_TABLE_COLUMNS = [ + { + name: 'fullName', + text: 'Name', + }, +]; const InventoryPanelProtectedAreas = ({ noData: noDataMessage, @@ -33,7 +36,7 @@ const InventoryPanelProtectedAreas = ({ const [selectedWDPAIds, setSelectedWDPAIds] = useState([]); const [filters, setFilters] = useState[1]>({ - sort: WDPA_TABLE_COLUMNS.name, + sort: WDPA_TABLE_COLUMNS[0].name, }); const allProjectWDPAsQuery = useProjectWDPAs( From e01d8ca0fd0fd7a74ca0182bd893d9d5810f5091 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Thu, 31 Aug 2023 12:15:34 +0200 Subject: [PATCH 17/28] pass classname to column --- .../components/inventory-table/index.tsx | 2 +- .../inventory-panel/wdpas/modals/delete/index.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx index bbd2031b44..70aaa7781d 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx @@ -51,7 +51,7 @@ const InventoryTable = ({ key={column.name} className={cn({ 'flex-1 pl-2': true, - 'flex flex-1 justify-start py-2 pl-14': column.name === 'tag', + [column.className]: !!column.className, })} > { + const selectedWDPAs = useMemo(() => { return allProjectWDPAsQuery.data?.filter(({ id }) => selectedWDPAIds.includes(id)); }, [allProjectWDPAsQuery.data, selectedWDPAIds]); - const PAsNames = selectedPAs.map(({ fullName }) => fullName); + const WDPAsNames = selectedWDPAs.map(({ fullName }) => fullName); // ? the user will be able to delete the protected areas only if they are not being used by any scenario. - const haveScenarioAssociated = selectedPAs.some(({ scenarioUsageCount }) => + const haveScenarioAssociated = selectedWDPAs.some(({ scenarioUsageCount }) => Boolean(scenarioUsageCount) ); const handleBulkDelete = useCallback(() => { - const deletableWDPAsIds = selectedPAs.map(({ id }) => id); + const deletableWDPAsIds = selectedWDPAs.map(({ id }) => id); bulkDeleteWDPAFromProject(pid, deletableWDPAsIds, session) .then(async () => { @@ -75,7 +75,7 @@ const DeleteModal = ({ } ); }); - }, [selectedPAs, addToast, onDismiss, pid, queryClient, session]); + }, [selectedWDPAs, addToast, onDismiss, pid, queryClient, session]); return (
@@ -90,14 +90,14 @@ const DeleteModal = ({ This action cannot be undone.
    - {PAsNames.map((name) => ( + {WDPAsNames.map((name) => (
  • {name}
  • ))}
) : ( - Are you sure you want to delete "{PAsNames[0]}" protected area?
+ Are you sure you want to delete "{WDPAsNames[0]}" protected area?
This action cannot be undone.
)} From cb55062f1128ba588726ecaea06aeabd404f3edf Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Thu, 31 Aug 2023 15:38:12 +0200 Subject: [PATCH 18/28] remove promise all from edit --- .../wdpas/modals/edit/index.tsx | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx index 754a45fbd3..fbe0dfe0e1 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx @@ -36,42 +36,43 @@ const EditModal = ({ const onEditSubmit = useCallback( (values: FormValues) => { const { fullName } = values; - const editWDPAPromise = editWDPAMutation.mutateAsync({ - wdpaId: wdpaId, - projectId: pid, - data: { - fullName, + editWDPAMutation.mutate( + { + wdpaId: wdpaId, + projectId: pid, + data: { + fullName, + }, }, - }); - - Promise.all([editWDPAPromise]) - .then(async () => { - await queryClient.invalidateQueries(['wdpas', pid]); - handleModal('edit', false); - - addToast( - 'success-edit-wdpa', - <> -

Success!

-

Protected area edited

- , - { - level: 'success', - } - ); - }) - .catch(() => { - addToast( - 'error-edit-wdpa', - <> -

Error!

-

It is not possible to edit this protected area

- , - { - level: 'error', - } - ); - }); + { + onSuccess: async () => { + await queryClient.invalidateQueries(['wdpas', pid]); + handleModal('edit', false); + addToast( + 'success-edit-wdpa', + <> +

Success!

+

Protected area edited

+ , + { + level: 'success', + } + ); + }, + onError: () => { + addToast( + 'error-edit-wdpa', + <> +

Error!

+

It is not possible to edit this protected area

+ , + { + level: 'error', + } + ); + }, + } + ); }, [addToast, editWDPAMutation, wdpaId, handleModal, pid, queryClient] ); From 54839909ae43f442e2bf70511dc943681f232496 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 5 Sep 2023 09:39:32 +0200 Subject: [PATCH 19/28] update get protected areas response --- app/hooks/wdpa/index.ts | 43 +------------------ .../project/inventory-panel/wdpas/index.tsx | 7 ++- app/types/api/wdpa.ts | 17 +++++--- 3 files changed, 16 insertions(+), 51 deletions(-) diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index a2f557ebcb..664b6c1c59 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -90,56 +90,17 @@ export function useProjectWDPAs( ) { const { data: session } = useSession(); - const mockData: WDPA[] = [ - { - id: 'Not Reported', - wdpaId: 'Not Reported', - fullName: 'Not Reported', - iucnCategory: 'IUCN Category', - countryId: 'hgdfjkd', - shapeLength: 45, - shapeArea: 30, - status: 'done', - designation: 'd', - scenarioUsageCount: 3, - }, - { - id: 'IV', - wdpaId: 'IV', - fullName: 'IUCN IV', - iucnCategory: 'IUCN IV', - countryId: 'mdfgjf', - shapeLength: 45, - shapeArea: 30, - status: 'done', - designation: 'd', - scenarioUsageCount: 0, - }, - { - id: 'II', - wdpaId: 'II', - fullName: 'Florida scrub jay', - iucnCategory: 'IUCN II', - countryId: 'mdfgjf', - shapeLength: 45, - shapeArea: 30, - status: 'done', - designation: 'd', - scenarioUsageCount: 0, - }, - ]; - return useQuery({ queryKey: ['wdpas', pid], queryFn: async () => - API.request({ + API.request<{ data: WDPA[] }>({ method: 'GET', url: `/projects/${pid}/protected-areas`, headers: { Authorization: `Bearer ${session.accessToken}`, }, params, - }).then(({ data }) => mockData), + }).then(({ data }) => data.data), enabled: Boolean(pid), ...queryOptions, }); diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index effca09211..eca37c8033 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -49,8 +49,7 @@ const InventoryPanelProtectedAreas = ({ select: (data) => data?.map((wdpa) => ({ id: wdpa.id, - fullName: wdpa.fullName, - scenarioUsageCount: wdpa.scenarioUsageCount, + attributes: wdpa.attributes, })), keepPreviousData: true, placeholderData: [], @@ -110,8 +109,8 @@ const InventoryPanelProtectedAreas = ({ const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ ...wdpa, - name: wdpa.fullName, - scenarios: wdpa.scenarioUsageCount, + name: wdpa.attributes.fullName, + scenarios: wdpa.attributes.scenarioUsageCount, isVisibleOnMap: visibleWDPAs?.includes(wdpa.id), })); diff --git a/app/types/api/wdpa.ts b/app/types/api/wdpa.ts index 8baf21cf97..ccceb3a3a2 100644 --- a/app/types/api/wdpa.ts +++ b/app/types/api/wdpa.ts @@ -1,16 +1,21 @@ import { Job } from './job'; -export interface WDPA { - id: string; - wdpaId: string; +export interface WDPAAttributes { + countryId: string; + designation?: string; fullName: string; iucnCategory: string; - countryId: string; + scenarioUsageCount: number; shapeLength?: number; shapeArea?: number; status?: Job['status']; - designation?: string; - scenarioUsageCount: number; + wdpaId: string; +} + +export interface WDPA { + id: string; + type: string; + attributes: WDPAAttributes; } export interface WDPACategory { From 6094cf7cb12b9ebf4feafc7a5ecab4bf3a7c7754 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 5 Sep 2023 10:01:11 +0200 Subject: [PATCH 20/28] remove selector from wdpa categories hook --- app/hooks/wdpa/index.ts | 7 +++---- .../inventory-panel/components/inventory-table/types.ts | 3 +++ .../project/inventory-panel/wdpas/modals/delete/index.tsx | 6 +++--- .../project/inventory-panel/wdpas/modals/edit/index.tsx | 6 +++--- .../project/inventory-panel/wdpas/modals/upload/index.tsx | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 664b6c1c59..b2a768d0e8 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -5,7 +5,7 @@ import { useSession } from 'next-auth/react'; import { Project } from 'types/api/project'; import { Scenario } from 'types/api/scenario'; -import { WDPA, WDPACategory } from 'types/api/wdpa'; +import { WDPA } from 'types/api/wdpa'; import { API } from 'services/api'; import PROJECTS from 'services/projects'; @@ -26,9 +26,9 @@ export function useWDPACategories({ return useQuery( ['protected-areas', adminAreaId, customAreaId], async () => - API.request({ + API.request({ method: 'GET', - url: `/scenarios/${scenarioId}/protected-areas`, + url: `scenarios/${scenarioId}/protected-areas`, params: { ...(adminAreaId && { 'filter[adminAreaId]': adminAreaId, @@ -43,7 +43,6 @@ export function useWDPACategories({ }).then(({ data }) => data), { enabled: !!adminAreaId || !!customAreaId, - select: ({ data }) => data, } ); } diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts index 55942fbe87..0466740b2a 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/types.ts @@ -1,7 +1,10 @@ import { ChangeEvent } from 'react'; +import { WDPAAttributes } from 'types/api/wdpa'; + export type DataItem = { id: string; + attributes?: WDPAAttributes; name: string; scenarios: number; tag?: string; diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/delete/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/delete/index.tsx index f63007f188..731904ac51 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/delete/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/delete/index.tsx @@ -36,11 +36,11 @@ const DeleteModal = ({ return allProjectWDPAsQuery.data?.filter(({ id }) => selectedWDPAIds.includes(id)); }, [allProjectWDPAsQuery.data, selectedWDPAIds]); - const WDPAsNames = selectedWDPAs.map(({ fullName }) => fullName); + const WDPAsNames = selectedWDPAs.map(({ attributes }) => attributes.fullName); // ? the user will be able to delete the protected areas only if they are not being used by any scenario. - const haveScenarioAssociated = selectedWDPAs.some(({ scenarioUsageCount }) => - Boolean(scenarioUsageCount) + const haveScenarioAssociated = selectedWDPAs.some(({ attributes }) => + Boolean(attributes.scenarioUsageCount) ); const handleBulkDelete = useCallback(() => { diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx index fbe0dfe0e1..09c178d945 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx @@ -12,9 +12,9 @@ import Button from 'components/button'; import Field from 'components/forms/field'; import Label from 'components/forms/label'; import { composeValidators } from 'components/forms/validations'; -import { WDPA } from 'types/api/wdpa'; +import { WDPA, WDPAAttributes } from 'types/api/wdpa'; -export type FormValues = { fullName: WDPA['fullName'] }; +export type FormValues = { fullName: WDPAAttributes['fullName'] }; const EditModal = ({ wdpaId, @@ -80,7 +80,7 @@ const EditModal = ({ return ( initialValues={{ - fullName: allProjectWDPAsQuery.data?.[0]?.fullName, + fullName: allProjectWDPAsQuery.data?.[0]?.attributes.fullName, }} ref={formRef} onSubmit={onEditSubmit} diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx index 100ab4177e..0866bc2c24 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx @@ -22,14 +22,14 @@ import Loading from 'components/loading'; import Modal from 'components/modal'; import { PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE } from 'constants/file-uploader-size-limits'; import UploadWDPAsInfoButtonContent from 'constants/info-button-content/upload-wdpas'; -import { WDPA } from 'types/api/wdpa'; +import { WDPAAttributes } from 'types/api/wdpa'; import { cn } from 'utils/cn'; import { bytesToMegabytes } from 'utils/units'; import CLOSE_SVG from 'svgs/ui/close.svg?sprite'; export type FormValues = { - name: WDPA['fullName']; + name: WDPAAttributes['fullName']; file: File; }; From a15b06367a87fc48c3cdb4d967770fe97e2bab8a Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 5 Sep 2023 13:07:51 +0200 Subject: [PATCH 21/28] delete single item --- .../project/inventory-panel/wdpas/bulk-action-menu/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/utils.ts b/app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/utils.ts index 116bca93ea..0f0c3c0f56 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/utils.ts +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/bulk-action-menu/utils.ts @@ -3,7 +3,7 @@ import { Session } from 'next-auth'; import { Project } from 'types/api/project'; import { WDPA } from 'types/api/wdpa'; -import PROJECTS from 'services/projects'; +import { API } from 'services/api'; export function bulkDeleteWDPAFromProject( pid: Project['id'], @@ -17,7 +17,7 @@ export function bulkDeleteWDPAFromProject( pid: Project['id']; wdpaid: WDPA['id']; }) => { - return PROJECTS.delete(`/${pid}/protected-areas/${wdpaid}`, { + return API.delete(`/protected-areas/${wdpaid}`, { headers: { Authorization: `Bearer ${session.accessToken}`, }, From 217e3e47a7c692b1e8c904b4776be5642979df75 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 5 Sep 2023 13:19:27 +0200 Subject: [PATCH 22/28] edit name --- app/hooks/wdpa/index.ts | 26 +++---------------- .../wdpas/modals/edit/index.tsx | 3 +-- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index b2a768d0e8..62ab528d1e 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -8,7 +8,6 @@ import { Scenario } from 'types/api/scenario'; import { WDPA } from 'types/api/wdpa'; import { API } from 'services/api'; -import PROJECTS from 'services/projects'; import SCENARIOS from 'services/scenarios'; import UPLOADS from 'services/uploads'; @@ -110,21 +109,12 @@ export function useEditWDPA({ method: 'PATCH', }, }) { - const queryClient = useQueryClient(); const { data: session } = useSession(); - const saveProjectWDPA = ({ - projectId, - wdpaId, - data, - }: { - projectId: string; - wdpaId: string; - data: { fullName: string }; - }) => { - return PROJECTS.request({ + const saveProjectWDPA = ({ wdpaId, data }: { wdpaId: string; data: { name: string } }) => { + return API.request({ method: 'PATCH', - url: `/${projectId}/protected-areas/${wdpaId}`, + url: `/protected-areas/${wdpaId}`, data, headers: { Authorization: `Bearer ${session.accessToken}`, @@ -133,15 +123,7 @@ export function useEditWDPA({ }); }; - return useMutation(saveProjectWDPA, { - onSuccess: async (data, variables) => { - const { projectId } = variables; - await queryClient.invalidateQueries(['wdpas', projectId]); - }, - onError: (error, variables, context) => { - console.info('Error', error, variables, context); - }, - }); + return useMutation(saveProjectWDPA); } export function useUploadWDPAsShapefile({ diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx index 09c178d945..16e61017e4 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/edit/index.tsx @@ -39,9 +39,8 @@ const EditModal = ({ editWDPAMutation.mutate( { wdpaId: wdpaId, - projectId: pid, data: { - fullName, + name: fullName, }, }, { From 502a6ccae9e1f73d14263c070b9734efbda1222d Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Tue, 5 Sep 2023 13:37:16 +0200 Subject: [PATCH 23/28] disable header checkbox if there is no custom items --- .../inventory-panel/components/inventory-table/index.tsx | 3 +++ .../project/sidebar/project/inventory-panel/wdpas/index.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx index 70aaa7781d..c180110849 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/index.tsx @@ -22,6 +22,8 @@ const InventoryTable = ({ }: InventoryTable): JSX.Element => { const noData = !loading && data?.length === 0; + const noDataCustom = !loading && data?.every((item) => !item.isCustom); + return ( <> {loading && !data?.length && ( @@ -43,6 +45,7 @@ const InventoryTable = ({ theme="light" className="block h-4 w-4 checked:bg-blue-400" onChange={onSelectAll} + disabled={noDataCustom} /> {columns.map((column) => { diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index eca37c8033..940c57fb83 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, ChangeEvent } from 'react'; +import { useState, useCallback, useEffect, ChangeEvent, useMemo } from 'react'; import { useRouter } from 'next/router'; From 5ad8b4ae527777113ed3cb2de6977efdfcce952e Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Wed, 6 Sep 2023 18:45:53 +0200 Subject: [PATCH 24/28] update list --- .../project/sidebar/project/inventory-panel/wdpas/index.tsx | 5 +++-- app/types/api/wdpa.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index 940c57fb83..6c3b1fdae3 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, ChangeEvent, useMemo } from 'react'; +import { useState, useCallback, useEffect, ChangeEvent } from 'react'; import { useRouter } from 'next/router'; @@ -109,8 +109,9 @@ const InventoryPanelProtectedAreas = ({ const data: DataItem[] = allProjectWDPAsQuery.data?.map((wdpa) => ({ ...wdpa, - name: wdpa.attributes.fullName, + name: wdpa.attributes.isCustom ? wdpa.attributes.fullName : wdpa.attributes.iucnCategory, scenarios: wdpa.attributes.scenarioUsageCount, + isCustom: wdpa.attributes.isCustom, isVisibleOnMap: visibleWDPAs?.includes(wdpa.id), })); diff --git a/app/types/api/wdpa.ts b/app/types/api/wdpa.ts index ccceb3a3a2..2919c0d69d 100644 --- a/app/types/api/wdpa.ts +++ b/app/types/api/wdpa.ts @@ -10,6 +10,7 @@ export interface WDPAAttributes { shapeArea?: number; status?: Job['status']; wdpaId: string; + isCustom?: boolean; } export interface WDPA { From 36d3438c749f99d854cac2cc82e9d6747d8186e2 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Wed, 6 Sep 2023 18:57:08 +0200 Subject: [PATCH 25/28] invalidate query after upload --- .../project/sidebar/project/inventory-panel/wdpas/index.tsx | 1 + .../project/inventory-panel/wdpas/modals/upload/index.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index 6c3b1fdae3..a294b17074 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -27,6 +27,7 @@ const InventoryPanelProtectedAreas = ({ noData: string; }): JSX.Element => { const dispatch = useAppDispatch(); + const { selectedWDPAs: visibleWDPAs, search } = useAppSelector( (state) => state['/projects/[id]'] ); diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx index 0866bc2c24..c5a57ef0ad 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDropzone, DropzoneProps } from 'react-dropzone'; import { Form as FormRFF, Field as FieldRFF, FormProps } from 'react-final-form'; +import { useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; @@ -49,6 +50,7 @@ export const WDPAUploadModal = ({ const { pid } = query as { pid: string }; const { addToast } = useToasts(); + const queryClient = useQueryClient(); const uploadWDPAsShapefileMutation = useUploadWDPAsShapefile({}); @@ -126,6 +128,7 @@ export const WDPAUploadModal = ({ level: 'success', } ); + queryClient.invalidateQueries(['wdpas', pid]); }, onError: (error: AxiosError | Error) => { let errors: { status: number; title: string }[] = []; From 58c08dda7b5dee2b07611621677b0da574cec5f1 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Wed, 6 Sep 2023 19:29:02 +0200 Subject: [PATCH 26/28] ui --- .../inventory-table/row-item/index.tsx | 8 ++++- .../project/inventory-panel/wdpas/index.tsx | 30 ++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx index e9c4302064..dde7fb3c31 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx @@ -70,7 +70,13 @@ const RowItem = ({ {isCustom && ( - diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index a294b17074..111df2a181 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -117,20 +117,22 @@ const InventoryPanelProtectedAreas = ({ })); return ( -
- +
+
+ +
{displayBulkActions && }
); From d0919e57607bcd939cdf4c43fe58efc5321ce0c9 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Wed, 6 Sep 2023 19:36:14 +0200 Subject: [PATCH 27/28] custom filtering --- .../components/inventory-table/row-item/index.tsx | 2 +- .../project/sidebar/project/inventory-panel/wdpas/index.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx index dde7fb3c31..437cf1fee4 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx @@ -28,7 +28,7 @@ const RowItem = ({ className="block h-4 w-4 checked:bg-blue-400" onChange={onSelectRow} value={id} - checked={selectedIds.includes(id)} + checked={isCustom && selectedIds.includes(id)} disabled={!isCustom} /> diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx index 111df2a181..1bf1e1d8b7 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/index.tsx @@ -57,7 +57,9 @@ const InventoryPanelProtectedAreas = ({ } ); - const WDPAIds = allProjectWDPAsQuery.data?.map((wdpa) => wdpa.id); + const WDPAIds = allProjectWDPAsQuery.data + ?.filter((wdpa) => wdpa.attributes.isCustom) + .map((wdpa) => wdpa.id); const handleSelectAll = useCallback( (evt: ChangeEvent) => { From 90ea9d3d577602eabebda3bde13e152b1d6888e1 Mon Sep 17 00:00:00 2001 From: anamontiaga Date: Mon, 11 Sep 2023 14:23:51 +0200 Subject: [PATCH 28/28] fix upload info btn content --- .../project/inventory-panel/wdpas/modals/upload/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx index c5a57ef0ad..22009a83b2 100644 --- a/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/wdpas/modals/upload/index.tsx @@ -22,7 +22,7 @@ import InfoButton from 'components/info-button'; import Loading from 'components/loading'; import Modal from 'components/modal'; import { PROTECTED_AREA_UPLOADER_SHAPEFILE_MAX_SIZE } from 'constants/file-uploader-size-limits'; -import UploadWDPAsInfoButtonContent from 'constants/info-button-content/upload-wdpas'; +import UploadWDPAsInfoButtonContent from 'layout/info/upload-wdpas'; import { WDPAAttributes } from 'types/api/wdpa'; import { cn } from 'utils/cn'; import { bytesToMegabytes } from 'utils/units';