From f64f308c0508a0a934659319ed7a37b87a455e37 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 25 Sep 2024 21:29:31 +0200 Subject: [PATCH 01/14] split vessel-groups lists and modal edition --- apps/fishing-map/features/modals/Modals.tsx | 2 +- .../features/modals/modals.selectors.ts | 2 +- .../vessels/ReportVesselsTableFooter.tsx | 2 +- .../vessel-groups/VesselGroupReportTitle.tsx | 2 +- .../features/search/SearchActions.tsx | 2 +- .../features/user/UserVesselGroups.tsx | 18 +- .../selectors/user.permissions.selectors.ts | 11 +- .../vessel-groups/VesselGroupAddButton.tsx | 2 +- .../vessel-groups/VesselGroupModal.tsx | 36 +- .../vessel-groups/VesselGroupModalSearch.tsx | 4 +- .../vessel-groups/VesselGroupModalVessels.tsx | 8 +- .../vessel-groups-modal.slice.ts | 354 ++++++++++++++++++ .../vessel-groups/vessel-groups.hooks.ts | 21 +- .../vessel-groups/vessel-groups.selectors.ts | 20 +- .../vessel-groups/vessel-groups.slice.ts | 348 +---------------- .../workspace/common/LayerFilters.tsx | 2 +- .../vessel-groups/VesselGroupsLayerPanel.tsx | 5 +- .../vessel-groups/VesselGroupsSection.tsx | 2 +- .../workspace/vessels/VesselsSection.tsx | 6 +- apps/fishing-map/reducers.ts | 2 + 20 files changed, 444 insertions(+), 405 deletions(-) create mode 100644 apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts diff --git a/apps/fishing-map/features/modals/Modals.tsx b/apps/fishing-map/features/modals/Modals.tsx index 86c2ee2501..8502f340ff 100644 --- a/apps/fishing-map/features/modals/Modals.tsx +++ b/apps/fishing-map/features/modals/Modals.tsx @@ -12,7 +12,7 @@ import { ROOT_DOM_ELEMENT } from 'data/config' import useSecretMenu, { useSecretKeyboardCombo } from 'hooks/secret-menu.hooks' import { selectBigQueryActive, toggleBigQueryMenu } from 'features/bigquery/bigquery.slice' import { selectDownloadActivityAreaKey } from 'features/download/downloadActivity.slice' -import { selectVesselGroupModalOpen } from 'features/vessel-groups/vessel-groups.slice' +import { selectVesselGroupModalOpen } from 'features/vessel-groups/vessel-groups-modal.slice' import GFWOnly from 'features/user/GFWOnly' import { useAppDispatch } from 'features/app/app.hooks' import { selectDatasetUploadModalOpen, setModalOpen } from 'features/modals/modals.slice' diff --git a/apps/fishing-map/features/modals/modals.selectors.ts b/apps/fishing-map/features/modals/modals.selectors.ts index 2584b1ba59..9333e744d7 100644 --- a/apps/fishing-map/features/modals/modals.selectors.ts +++ b/apps/fishing-map/features/modals/modals.selectors.ts @@ -10,7 +10,7 @@ import { selectLayerLibraryModalOpen, selectScreenshotModalOpen, } from 'features/modals/modals.slice' -import { selectVesselGroupModalOpen } from 'features/vessel-groups/vessel-groups.slice' +import { selectVesselGroupModalOpen } from 'features/vessel-groups/vessel-groups-modal.slice' import { WelcomeContentKey } from 'features/welcome/welcome.content' import { selectLocationCategory, diff --git a/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx b/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx index f3bf986ed7..e58f11fd3a 100644 --- a/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx +++ b/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx @@ -14,7 +14,7 @@ import { useAppDispatch } from 'features/app/app.hooks' import { setVesselGroupConfirmationMode, setVesselGroupCurrentDataviewIds, -} from 'features/vessel-groups/vessel-groups.slice' +} from 'features/vessel-groups/vessel-groups-modal.slice' import { selectActiveActivityAndDetectionsDataviews } from 'features/dataviews/selectors/dataviews.selectors' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { selectReportVesselFilter } from 'features/reports/areas/area-reports.config.selectors' diff --git a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx index e409d096c0..a3a4482e1a 100644 --- a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx @@ -11,7 +11,7 @@ import { setNewVesselGroupSearchVessels, setVesselGroupEditId, setVesselGroupsModalOpen, -} from 'features/vessel-groups/vessel-groups.slice' +} from 'features/vessel-groups/vessel-groups-modal.slice' import { formatInfoField } from 'utils/info' import { useLocationConnect } from 'routes/routes.hook' import { selectHasOtherVesselGroupDataviews } from 'features/dataviews/selectors/dataviews.selectors' diff --git a/apps/fishing-map/features/search/SearchActions.tsx b/apps/fishing-map/features/search/SearchActions.tsx index 81d163b105..aa919437aa 100644 --- a/apps/fishing-map/features/search/SearchActions.tsx +++ b/apps/fishing-map/features/search/SearchActions.tsx @@ -17,7 +17,7 @@ import { selectActiveActivityAndDetectionsDataviews } from 'features/dataviews/s import { setVesselGroupConfirmationMode, setVesselGroupCurrentDataviewIds, -} from 'features/vessel-groups/vessel-groups.slice' +} from 'features/vessel-groups/vessel-groups-modal.slice' import { HOME, WORKSPACE } from 'routes/routes' import { EMPTY_FILTERS } from 'features/search/search.config' import { getRelatedIdentityVesselIds } from 'features/vessel/vessel.utils' diff --git a/apps/fishing-map/features/user/UserVesselGroups.tsx b/apps/fishing-map/features/user/UserVesselGroups.tsx index 1d5c364294..24aed3f339 100644 --- a/apps/fishing-map/features/user/UserVesselGroups.tsx +++ b/apps/fishing-map/features/user/UserVesselGroups.tsx @@ -6,18 +6,20 @@ import { Spinner, IconButton, Button } from '@globalfishingwatch/ui-components' import { VesselGroup } from '@globalfishingwatch/api-types' import { AsyncReducerStatus } from 'utils/async-slice' import { - setVesselGroupsModalOpen, selectVesselGroupsStatus, selectVesselGroupsStatusId, deleteVesselGroupThunk, - setVesselGroupEditId, - selectVesselGroupEditId, } from 'features/vessel-groups/vessel-groups.slice' import { useAppDispatch } from 'features/app/app.hooks' import { selectDatasetsStatus } from 'features/datasets/datasets.slice' import { getVesselGroupLabel } from 'features/vessel-groups/vessel-groups.utils' import { sortByCreationDate } from 'utils/dates' import VesselGroupReportLink from 'features/reports/vessel-groups/VesselGroupReportLink' +import { + selectVesselGroupEditId, + setVesselGroupEditId, + setVesselGroupsModalOpen, +} from 'features/vessel-groups/vessel-groups-modal.slice' import { selectUserVesselGroups } from './selectors/user.permissions.selectors' import styles from './User.module.css' @@ -88,14 +90,20 @@ function UserVesselGroups() {
onEditClick(vesselGroup)} /> onDeleteClick(vesselGroup)} /> diff --git a/apps/fishing-map/features/user/selectors/user.permissions.selectors.ts b/apps/fishing-map/features/user/selectors/user.permissions.selectors.ts index 7cc148dad7..3b10b7041b 100644 --- a/apps/fishing-map/features/user/selectors/user.permissions.selectors.ts +++ b/apps/fishing-map/features/user/selectors/user.permissions.selectors.ts @@ -5,10 +5,7 @@ import { DatasetStatus, DatasetCategory, UserPermission } from '@globalfishingwa import { selectAllDatasets } from 'features/datasets/datasets.slice' import { selectWorkspaces } from 'features/workspaces-list/workspaces-list.slice' import { AUTO_GENERATED_FEEDBACK_WORKSPACE_PREFIX, PRIVATE_SUFIX, USER_SUFIX } from 'data/config' -import { - selectAllVesselGroups, - selectWorkspaceVesselGroups, -} from 'features/vessel-groups/vessel-groups.slice' +import { selectAllVesselGroups } from 'features/vessel-groups/vessel-groups.slice' import { selectAllReports } from 'features/reports/areas/area-reports.slice' import { selectUserData } from 'features/user/selectors/user.selectors' import { DEFAULT_GROUP_ID } from 'features/user/user.config' @@ -121,8 +118,8 @@ export const selectUserVesselGroups = createSelector( ) export const selectAllVisibleVesselGroups = createSelector( - [selectUserVesselGroups, selectWorkspaceVesselGroups], - (vesselGroups = [], workspaceVesselGroups = []) => { - return uniqBy([...vesselGroups, ...(workspaceVesselGroups || [])], (v) => v.id) + [selectUserVesselGroups], + (vesselGroups = []) => { + return uniqBy([...vesselGroups], (v) => v.id) } ) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx index 247aa11e7b..ca92ff8196 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import React from 'react' import { useSelector } from 'react-redux' import { Button, ButtonType, ButtonSize } from '@globalfishingwatch/ui-components' -import { MAX_VESSEL_GROUP_VESSELS } from 'features/vessel-groups/vessel-groups.slice' +import { MAX_VESSEL_GROUP_VESSELS } from 'features/vessel-groups/vessel-groups-modal.slice' import { selectIsGuestUser } from 'features/user/selectors/user.selectors' import styles from './VesselGroupListTooltip.module.css' import VesselGroupListTooltip from './VesselGroupListTooltip' diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx index fa7f641a81..87aa8ce029 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx @@ -17,7 +17,7 @@ import VesselGroupVessels from 'features/vessel-groups/VesselGroupModalVessels' import { useAppDispatch } from 'features/app/app.hooks' import { selectAllVesselGroupSearchVessels, - selectHasVesselGroupSearchVessels, + selectHasVesselGroupVessels, selectHasVesselGroupVesselsOverflow, selectVesselGroupWorkspaceToNavigate, selectWorkspaceVessselGroupsIds, @@ -40,27 +40,29 @@ import { selectVesselsDataviews } from 'features/dataviews/selectors/dataviews.i import { getVesselGroupDataviewInstance } from 'features/reports/vessel-groups/vessel-group-report.dataviews' import { IdField, - resetVesselGroup, createVesselGroupThunk, selectVesselGroupById, + selectVesselGroupsStatus, + VesselGroupConfirmationMode, + updateVesselGroupVesselsThunk, +} from './vessel-groups.slice' +import styles from './VesselGroupModal.module.css' +import { + getVesselInVesselGroupThunk, + MAX_VESSEL_GROUP_VESSELS, + MAX_VESSEL_GROUP_SEARCH_VESSELS, + resetVesselGroupModal, + resetVesselGroupModalStatus, + searchVesselGroupsVesselsThunk, + selectVesselGroupConfirmationMode, selectVesselGroupEditId, selectVesselGroupModalOpen, selectVesselGroupSearchId, selectVesselGroupSearchStatus, - selectVesselGroupsStatus, selectVesselGroupsVessels, setVesselGroupSearchId, - resetVesselGroupStatus, setVesselGroupSearchVessels, - searchVesselGroupsVesselsThunk, - MAX_VESSEL_GROUP_SEARCH_VESSELS, - MAX_VESSEL_GROUP_VESSELS, - getVesselInVesselGroupThunk, - selectVesselGroupConfirmationMode, - VesselGroupConfirmationMode, - updateVesselGroupVesselsThunk, -} from './vessel-groups.slice' -import styles from './VesselGroupModal.module.css' +} from './vessel-groups-modal.slice' function VesselGroupModal(): React.ReactElement { const { t } = useTranslation() @@ -92,7 +94,7 @@ function VesselGroupModal(): React.ReactElement { const [createAsPublic, setCreateAsPublic] = useState(true) const vesselGroupSearchVessels = useSelector(selectAllVesselGroupSearchVessels) const hasVesselsOverflow = useSelector(selectHasVesselGroupVesselsOverflow) - const hasVesselGroupsVessels = useSelector(selectHasVesselGroupSearchVessels) + const hasVesselGroupsVessels = useSelector(selectHasVesselGroupVessels) const vesselGroupsInWorkspace = useSelector(selectWorkspaceVessselGroupsIds) const { upsertDataviewInstance } = useDataviewInstancesConnect() const searchVesselGroupsVesselsRef = useRef() @@ -141,7 +143,7 @@ function VesselGroupModal(): React.ReactElement { const close = useCallback(() => { setError('') setGroupName('') - dispatch(resetVesselGroup('')) + dispatch(resetVesselGroupModal()) abortSearch() }, [abortSearch, dispatch]) @@ -158,8 +160,8 @@ function VesselGroupModal(): React.ReactElement { if (confirmed) { if (action === 'back') { setError('') - dispatch(setVesselGroupSearchVessels(undefined)) - dispatch(resetVesselGroupStatus('')) + dispatch(setVesselGroupSearchVessels(null)) + dispatch(resetVesselGroupModalStatus()) abortSearch() setShowBackButton(false) } else { diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx index 96265e5891..4e61517bf2 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx @@ -13,7 +13,7 @@ import { selectVesselGroupsVessels, setVesselGroupSearchId, setVesselGroupVessels, -} from './vessel-groups.slice' +} from './vessel-groups-modal.slice' import styles from './VesselGroupModal.module.css' function VesselGroupSearch({ onError }: { onError: (string: any) => void }) { @@ -30,7 +30,7 @@ function VesselGroupSearch({ onError }: { onError: (string: any) => void }) { const vesselIds = debouncedSearchText?.split(/[\s|,]+/).filter(Boolean) dispatch(setVesselGroupVessels(vesselIds.map((v) => ({ vesselId: v, dataset: '' })))) } else { - dispatch(setVesselGroupVessels(undefined)) + dispatch(setVesselGroupVessels(null)) } }, [dispatch, debouncedSearchText]) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx index e5b44efda9..a994ee3116 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx @@ -16,13 +16,13 @@ import { } from 'features/vessel/vessel.utils' import { VesselDataIdentity } from 'features/vessel/vessel.slice' import VesselIdentityFieldLogin from 'features/vessel/identity/VesselIdentityFieldLogin' +import styles from './VesselGroupModal.module.css' import { - setVesselGroupSearchVessels, - selectVesselGroupSearchVessels, selectNewVesselGroupSearchVessels, + selectVesselGroupSearchVessels, setNewVesselGroupSearchVessels, -} from './vessel-groups.slice' -import styles from './VesselGroupModal.module.css' + setVesselGroupSearchVessels, +} from './vessel-groups-modal.slice' type VesselGroupVesselRowProps = { vessel: VesselDataIdentity diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts new file mode 100644 index 0000000000..cb803ae7b5 --- /dev/null +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -0,0 +1,354 @@ +import { createAsyncThunk, PayloadAction, createSlice } from '@reduxjs/toolkit' +import { uniq, uniqBy } from 'es-toolkit' +import { RootState } from 'reducers' +import { + APIPagination, + APIVesselSearchPagination, + DatasetStatus, + DataviewDatasetConfig, + EndpointId, + IdentityVessel, + VesselGroup, + VesselGroupVessel, +} from '@globalfishingwatch/api-types' +import { GFWAPI, parseAPIError, ParsedAPIError } from '@globalfishingwatch/api-client' +import { resolveEndpoint } from '@globalfishingwatch/datasets-client' +import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' +import { AsyncReducerStatus } from 'utils/async-slice' +import { getVesselId } from 'features/vessel/vessel.utils' +import { fetchDatasetByIdThunk, selectDatasetById } from '../datasets/datasets.slice' + +export const MAX_VESSEL_GROUP_VESSELS = 1000 + +// Limitation of the API as we have a limitation of 2000 characters in GET requests +export const MAX_VESSEL_GROUP_SEARCH_VESSELS = 400 + +export type IdField = 'vesselId' | 'mmsi' +export type VesselGroupConfirmationMode = 'save' | 'saveAndSeeInWorkspace' | 'saveAndDeleteVessels' + +interface VesselGroupModalState { + isModalOpen: boolean + vesselGroupEditId: string | null + currentDataviewIds: string[] | null + confirmationMode: VesselGroupConfirmationMode + groupVessels: VesselGroupVessel[] | null + search: { + id: IdField + status: AsyncReducerStatus + error: ParsedAPIError | null + vessels: IdentityVessel[] | null + } + newSearchVessels: IdentityVessel[] | null +} + +const fetchSearchVessels = async ( + url: string, + { signal, token }: { signal?: AbortSignal; token?: string } +) => { + const searchResponse = await GFWAPI.fetch>( + `${url}${token ? `&since=${token}` : ''}`, + { + signal, + } + ) + return searchResponse +} + +const SEARCH_PAGINATION = 25 +const fetchAllSearchVessels = async (url: string, signal: AbortSignal) => { + let searchResults = [] as IdentityVessel[] + let pendingResults = true + let paginationToken = '' + while (pendingResults) { + const searchResponse = await fetchSearchVessels(url, { signal, token: paginationToken }) + searchResults = searchResults.concat(searchResponse.entries) + if (searchResponse.since && searchResults!?.length < searchResponse.total) { + paginationToken = searchResponse.since + } else { + pendingResults = false + } + } + return searchResults +} + +const initialState: VesselGroupModalState = { + isModalOpen: false, + vesselGroupEditId: null, + currentDataviewIds: null, + confirmationMode: 'save', + groupVessels: null, + search: { + id: 'mmsi', + status: AsyncReducerStatus.Idle, + vessels: null, + error: null, + }, + newSearchVessels: null, +} + +export const searchVesselGroupsVesselsThunk = createAsyncThunk( + 'vessel-groups/searchVessels', + async ( + { vessels, idField }: { vessels: VesselGroupVessel[]; idField: IdField }, + { signal, rejectWithValue, getState } + ) => { + const state = getState() as any + const vesselGroupDatasets = uniq(vessels?.flatMap((v) => v.dataset || [])) + const allVesselDatasets = (selectVesselsDatasets(state) || []).filter( + (d) => + d.status !== DatasetStatus.Deleted && d.configuration?.apiSupportedVersions?.includes('v3') + /*&& d.alias?.some((alias) => alias.includes(':latest'))*/ + ) + + const searchDatasets = vesselGroupDatasets?.length + ? allVesselDatasets.filter((dataset) => vesselGroupDatasets.includes(dataset.id)) + : allVesselDatasets + + if (searchDatasets?.length) { + const dataset = searchDatasets[0] + const datasets = searchDatasets.map((d) => d.id) + const uniqVesselIds = uniq(vessels.map(({ vesselId }) => vesselId)) + const isVesselByIdSearch = idField === 'vesselId' + const datasetConfig: DataviewDatasetConfig = { + endpoint: isVesselByIdSearch ? EndpointId.VesselList : EndpointId.VesselSearch, + datasetId: searchDatasets[0].id, + params: [], + query: [ + { + id: 'datasets', + value: datasets, + }, + isVesselByIdSearch + ? { id: 'ids', value: uniqVesselIds } + : { + id: 'where', + value: encodeURIComponent( + `${uniqVesselIds.map((ssvid) => `ssvid='${ssvid}'`).join(' OR ')}` + ), + }, + ], + } + if (!isVesselByIdSearch) { + datasetConfig.query?.push({ + id: 'limit', + value: SEARCH_PAGINATION, + }) + } + try { + const url = resolveEndpoint(dataset, datasetConfig) + if (!url) { + console.warn('Missing search url') + return rejectWithValue({ + code: 0, + message: 'Missing search url', + }) + } + const searchResults = await fetchAllSearchVessels(`${url}&cache=false`, signal) + // API returns multiple instances of the same vessel with the same id and dataset + const uniqSearchResults = uniqBy(searchResults, (vessel) => + [getVesselId(vessel), vessel.dataset].join(',') + ) + // Searching could return same vessel id from different datasets so we need to choose the original one + const searchResultsFiltered = isVesselByIdSearch + ? uniqSearchResults.filter((vessel) => { + const vesselId = getVesselId(vessel) + return ( + vessels.find((v) => { + const isSameVesselid = v.vesselId === vesselId + const isSameDataset = v.dataset ? v.dataset === vessel.dataset : true + return isSameVesselid && isSameDataset + }) !== undefined + ) + }) + : uniqSearchResults + + return searchResultsFiltered + } catch (e: any) { + console.warn(e) + return rejectWithValue(parseAPIError(e)) + } + } else { + console.warn('No search datasets found') + return rejectWithValue({ + code: 0, + message: 'No search datasets found', + }) + } + }, + { + condition: (_, { getState }) => { + const workspaceVesselGroupsStatus = (getState() as RootState).vesselGroupModal.search.status + // Fetched already in progress, don't need to re-fetch + return workspaceVesselGroupsStatus !== AsyncReducerStatus.Loading + }, + } +) + +export const getVesselInVesselGroupThunk = createAsyncThunk( + 'vessel-groups/getVessels', + async ( + { vesselGroup }: { vesselGroup: VesselGroup }, + { signal, rejectWithValue, getState, dispatch } + ) => { + const state = getState() as any + const datasets = uniq(vesselGroup.vessels.flatMap((v) => v.dataset || [])) + const datasetId = datasets[0] + let dataset = selectDatasetById(datasetId)(state) + if (!dataset) { + const action = await dispatch(fetchDatasetByIdThunk(datasetId)) + if (fetchDatasetByIdThunk.fulfilled.match(action)) { + dataset = action.payload + } + } + if (vesselGroup.id && dataset) { + const datasetConfig: DataviewDatasetConfig = { + endpoint: EndpointId.VesselList, + datasetId: '', + params: [], + query: [ + { + id: 'vessel-groups', + value: vesselGroup.id, + }, + { + id: 'cache', + value: false, + }, + ], + } + try { + const url = resolveEndpoint(dataset, datasetConfig) + if (!url) { + console.warn('Missing search url') + return rejectWithValue({ + code: 0, + message: 'Missing search url', + }) + } + const vessels = await GFWAPI.fetch>(url, { + signal, + cache: 'reload', + }) + return vessels.entries + } catch (e: any) { + console.warn(e) + return rejectWithValue(parseAPIError(e)) + } + } else { + console.warn('No search datasets found') + return rejectWithValue({ + code: 0, + message: 'No search datasets found', + }) + } + }, + { + condition: (_, { getState }) => { + const workspaceVesselGroupsStatus = (getState() as RootState).vesselGroupModal.search.status + // Fetched already in progress, don't need to re-fetch + return workspaceVesselGroupsStatus !== AsyncReducerStatus.Loading + }, + } +) + +export const vesselGroupModalSlice = createSlice({ + name: 'vesselGroupModal', + initialState, + reducers: { + setVesselGroupsModalOpen: (state, action: PayloadAction) => { + state.isModalOpen = action.payload + }, + setVesselGroupSearchId: (state, action: PayloadAction) => { + state.search.id = action.payload + }, + resetVesselGroupModalStatus: (state) => { + state.search.status = AsyncReducerStatus.Idle + }, + setVesselGroupSearchVessels: (state, action: PayloadAction) => { + state.search.vessels = action.payload + }, + setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { + state.newSearchVessels = action.payload + }, + setVesselGroupVessels: (state, action: PayloadAction) => { + state.groupVessels = action.payload + }, + setVesselGroupEditId: (state, action: PayloadAction) => { + state.vesselGroupEditId = action.payload + }, + setVesselGroupConfirmationMode: (state, action: PayloadAction) => { + state.confirmationMode = action.payload + }, + setVesselGroupCurrentDataviewIds: (state, action: PayloadAction) => { + state.currentDataviewIds = action.payload + }, + resetVesselGroupModal: (state) => { + return { ...initialState } + }, + }, + extraReducers(builder) { + builder.addCase(searchVesselGroupsVesselsThunk.pending, (state) => { + state.search.status = AsyncReducerStatus.Loading + state.search.vessels = null + }) + builder.addCase(searchVesselGroupsVesselsThunk.fulfilled, (state, action) => { + state.search.status = AsyncReducerStatus.Finished + state.search.vessels = action.payload + }) + builder.addCase(searchVesselGroupsVesselsThunk.rejected, (state, action) => { + if (action.error.message === 'Aborted') { + state.search.status = AsyncReducerStatus.Idle + } else { + state.search.status = AsyncReducerStatus.Error + state.search.error = action.payload as ParsedAPIError + } + }) + builder.addCase(getVesselInVesselGroupThunk.pending, (state) => { + state.search.status = AsyncReducerStatus.Loading + state.search.vessels = null + }) + builder.addCase(getVesselInVesselGroupThunk.fulfilled, (state, action) => { + state.search.status = AsyncReducerStatus.Finished + state.search.vessels = action.payload + }) + builder.addCase(getVesselInVesselGroupThunk.rejected, (state, action) => { + if (action.error.message === 'Aborted') { + state.search.status = AsyncReducerStatus.Idle + } else { + state.search.status = AsyncReducerStatus.Error + state.search.error = action.payload as ParsedAPIError + } + }) + }, +}) + +export const { + resetVesselGroupModal, + resetVesselGroupModalStatus, + setVesselGroupEditId, + setVesselGroupVessels, + setVesselGroupSearchId, + setVesselGroupsModalOpen, + setVesselGroupSearchVessels, + setNewVesselGroupSearchVessels, + setVesselGroupCurrentDataviewIds, + setVesselGroupConfirmationMode, +} = vesselGroupModalSlice.actions + +export const selectVesselGroupModalOpen = (state: RootState) => state.vesselGroupModal.isModalOpen +export const selectVesselGroupSearchId = (state: RootState) => state.vesselGroupModal.search.id +export const selectVesselGroupSearchStatus = (state: RootState) => + state.vesselGroupModal.search.status +export const selectVesselGroupSearchVessels = (state: RootState) => + state.vesselGroupModal.search.vessels +export const selectVesselGroupsVessels = (state: RootState) => state.vesselGroupModal.groupVessels +export const selectNewVesselGroupSearchVessels = (state: RootState) => + state.vesselGroupModal.newSearchVessels +export const selectCurrentDataviewIds = (state: RootState) => + state.vesselGroupModal.currentDataviewIds +export const selectVesselGroupEditId = (state: RootState) => + state.vesselGroupModal.vesselGroupEditId +export const selectVesselGroupConfirmationMode = (state: RootState) => + state.vesselGroupModal.confirmationMode + +export default vesselGroupModalSlice.reducer diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts index 8b801669c7..f364df1760 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts @@ -13,12 +13,14 @@ import { useAppDispatch } from 'features/app/app.hooks' import { sortByCreationDate } from 'utils/dates' import { selectVesselGroupsStatusId, - setNewVesselGroupSearchVessels, - setVesselGroupEditId, - setVesselGroupsModalOpen, UpdateVesselGroupThunkParams, updateVesselGroupVesselsThunk, } from './vessel-groups.slice' +import { + setNewVesselGroupSearchVessels, + setVesselGroupEditId, + setVesselGroupsModalOpen, +} from './vessel-groups-modal.slice' export const NEW_VESSEL_GROUP_ID = 'new-vessel-group' @@ -82,17 +84,18 @@ export const useVesselGroupsModal = () => { async (vesselGroupId: string, vessels: AddVesselGroupVessel[]) => { const vesselsWithDataset = vessels.map((vessel) => ({ ...vessel, - id: (vessel as VesselLastIdentity)?.id || (vessel as ReportVesselWithDatasets)?.vesselId, - dataset: - typeof vessel?.dataset === 'string' - ? vessel.dataset - : vessel.dataset?.id || (vessel as ReportVesselWithDatasets)?.infoDataset?.id, + vesselId: + (vessel as VesselLastIdentity)?.id || (vessel as ReportVesselWithDatasets)?.vesselId, + dataset: (typeof vessel?.dataset === 'string' + ? vessel.dataset + : vessel.dataset?.id || (vessel as ReportVesselWithDatasets)?.infoDataset?.id) as string, })) if (vesselsWithDataset?.length) { if (vesselGroupId && vesselGroupId !== NEW_VESSEL_GROUP_ID) { dispatch(setVesselGroupEditId(vesselGroupId)) } - dispatch(setNewVesselGroupSearchVessels(vesselsWithDataset)) + // TODO:VV3 remove this any + dispatch(setNewVesselGroupSearchVessels(vesselsWithDataset as any)) dispatch(setVesselGroupsModalOpen(true)) } else { console.warn('No related activity datasets founds for', vesselsWithDataset) diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts index d54c4d796a..772df490ca 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts @@ -3,9 +3,8 @@ import { isAdvancedSearchAllowed } from 'features/search/search.selectors' import { selectLocationQuery, selectUrlDataviewInstances } from 'routes/routes.selectors' import { MAX_VESSEL_GROUP_VESSELS, - selectNewVesselGroupSearchVessels, - selectVesselGroupSearchVessels, -} from 'features/vessel-groups/vessel-groups.slice' + selectVesselGroupsVessels, +} from 'features/vessel-groups/vessel-groups-modal.slice' import { selectLastVisitedWorkspace, selectWorkspace, @@ -16,6 +15,10 @@ import { LastWorkspaceVisited } from 'features/workspace/workspace.slice' import { WORKSPACE } from 'routes/routes' import { DEFAULT_WORKSPACE_CATEGORY, DEFAULT_WORKSPACE_ID } from 'data/workspaces' import { getVesselGroupsInDataviews } from 'features/datasets/datasets.utils' +import { + selectNewVesselGroupSearchVessels, + selectVesselGroupSearchVessels, +} from './vessel-groups-modal.slice' export const selectAllVesselGroupSearchVessels = createSelector( [selectVesselGroupSearchVessels, selectNewVesselGroupSearchVessels], @@ -31,10 +34,13 @@ export const selectHasVesselGroupVesselsOverflow = createSelector( } ) -export const selectHasVesselGroupSearchVessels = createSelector( - [selectAllVesselGroupSearchVessels], - (vessels = []) => { - return vessels.length > 0 +export const selectHasVesselGroupVessels = createSelector( + [selectVesselGroupsVessels, selectAllVesselGroupSearchVessels], + (vessels = [], searchVessels = []) => { + return ( + (vessels !== null && vessels.length > 0) || + (searchVessels !== null && searchVessels.length > 0) + ) } ) diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts index 9157b6ad25..85907bdb13 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts @@ -1,21 +1,14 @@ -import { createAsyncThunk, PayloadAction, createSelector } from '@reduxjs/toolkit' +import { createAsyncThunk, createSelector, PayloadAction } from '@reduxjs/toolkit' import { stringify } from 'qs' -import { uniq, uniqBy } from 'es-toolkit' +import { uniqBy } from 'es-toolkit' import memoize from 'lodash/memoize' import { APIPagination, - APIVesselSearchPagination, - DatasetStatus, - DataviewDatasetConfig, - EndpointId, - IdentityVessel, VesselGroup, VesselGroupUpsert, VesselGroupVessel, } from '@globalfishingwatch/api-types' import { GFWAPI, FetchOptions, parseAPIError, ParsedAPIError } from '@globalfishingwatch/api-client' -import { resolveEndpoint } from '@globalfishingwatch/datasets-client' -import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' import { AsyncError, asyncInitialState, @@ -24,257 +17,27 @@ import { createAsyncSlice, } from 'utils/async-slice' import { DEFAULT_PAGINATION_PARAMS } from 'data/config' -import { getVesselId } from 'features/vessel/vessel.utils' import { RootState } from 'store' -import { fetchDatasetByIdThunk, selectDatasetById } from '../datasets/datasets.slice' - -export const MAX_VESSEL_GROUP_VESSELS = 1000 - -// Limitation of the API as we have a limitation of 2000 characters in GET requests -export const MAX_VESSEL_GROUP_SEARCH_VESSELS = 400 export type IdField = 'vesselId' | 'mmsi' export type VesselGroupConfirmationMode = 'save' | 'saveAndSeeInWorkspace' | 'saveAndDeleteVessels' interface VesselGroupsState extends AsyncReducer { - isModalOpen: boolean - vesselGroupEditId: string | null - currentDataviewIds: string[] | null - confirmationMode: VesselGroupConfirmationMode - groupVessels: VesselGroupVessel[] | null - search: { - id: IdField - status: AsyncReducerStatus - error: ParsedAPIError | null - vessels: IdentityVessel[] | null - } - newSearchVessels: IdentityVessel[] | null workspace: { status: AsyncReducerStatus error: ParsedAPIError | null - vesselGroups: VesselGroup[] | null } } -const fetchSearchVessels = async ( - url: string, - { signal, token }: { signal?: AbortSignal; token?: string } -) => { - const searchResponse = await GFWAPI.fetch>( - `${url}${token ? `&since=${token}` : ''}`, - { - signal, - } - ) - return searchResponse -} - -const SEARCH_PAGINATION = 25 -const fetchAllSearchVessels = async (url: string, signal: AbortSignal) => { - let searchResults = [] as IdentityVessel[] - let pendingResults = true - let paginationToken = '' - while (pendingResults) { - const searchResponse = await fetchSearchVessels(url, { signal, token: paginationToken }) - searchResults = searchResults.concat(searchResponse.entries) - if (searchResponse.since && searchResults!?.length < searchResponse.total) { - paginationToken = searchResponse.since - } else { - pendingResults = false - } - } - return searchResults -} - const initialState: VesselGroupsState = { ...asyncInitialState, - isModalOpen: false, - vesselGroupEditId: null, - currentDataviewIds: null, - confirmationMode: 'save', - groupVessels: null, - search: { - id: 'mmsi', - status: AsyncReducerStatus.Idle, - vessels: null, - error: null, - }, - newSearchVessels: null, workspace: { status: AsyncReducerStatus.Idle, error: null, - vesselGroups: null, }, } type VesselGroupSliceState = { vesselGroups: VesselGroupsState } -export const searchVesselGroupsVesselsThunk = createAsyncThunk( - 'vessel-groups/searchVessels', - async ( - { vessels, idField }: { vessels: VesselGroupVessel[]; idField: IdField }, - { signal, rejectWithValue, getState } - ) => { - const state = getState() as any - const vesselGroupDatasets = uniq(vessels?.flatMap((v) => v.dataset || [])) - const allVesselDatasets = (selectVesselsDatasets(state) || []).filter( - (d) => - d.status !== DatasetStatus.Deleted && d.configuration?.apiSupportedVersions?.includes('v3') - /*&& d.alias?.some((alias) => alias.includes(':latest'))*/ - ) - - const searchDatasets = vesselGroupDatasets?.length - ? allVesselDatasets.filter((dataset) => vesselGroupDatasets.includes(dataset.id)) - : allVesselDatasets - - if (searchDatasets?.length) { - const dataset = searchDatasets[0] - const datasets = searchDatasets.map((d) => d.id) - const uniqVesselIds = uniq(vessels.map(({ vesselId }) => vesselId)) - const isVesselByIdSearch = idField === 'vesselId' - const datasetConfig: DataviewDatasetConfig = { - endpoint: isVesselByIdSearch ? EndpointId.VesselList : EndpointId.VesselSearch, - datasetId: searchDatasets[0].id, - params: [], - query: [ - { - id: 'datasets', - value: datasets, - }, - isVesselByIdSearch - ? { id: 'ids', value: uniqVesselIds } - : { - id: 'where', - value: encodeURIComponent( - `${uniqVesselIds.map((ssvid) => `ssvid='${ssvid}'`).join(' OR ')}` - ), - }, - ], - } - if (!isVesselByIdSearch) { - datasetConfig.query?.push({ - id: 'limit', - value: SEARCH_PAGINATION, - }) - } - try { - const url = resolveEndpoint(dataset, datasetConfig) - if (!url) { - console.warn('Missing search url') - return rejectWithValue({ - code: 0, - message: 'Missing search url', - }) - } - const searchResults = await fetchAllSearchVessels(`${url}&cache=false`, signal) - // API returns multiple instances of the same vessel with the same id and dataset - const uniqSearchResults = uniqBy(searchResults, (vessel) => - [getVesselId(vessel), vessel.dataset].join(',') - ) - // Searching could return same vessel id from different datasets so we need to choose the original one - const searchResultsFiltered = isVesselByIdSearch - ? uniqSearchResults.filter((vessel) => { - const vesselId = getVesselId(vessel) - return ( - vessels.find((v) => { - const isSameVesselid = v.vesselId === vesselId - const isSameDataset = v.dataset ? v.dataset === vessel.dataset : true - return isSameVesselid && isSameDataset - }) !== undefined - ) - }) - : uniqSearchResults - - return searchResultsFiltered - } catch (e: any) { - console.warn(e) - return rejectWithValue(parseAPIError(e)) - } - } else { - console.warn('No search datasets found') - return rejectWithValue({ - code: 0, - message: 'No search datasets found', - }) - } - }, - { - condition: (_, { getState }) => { - const workspaceVesselGroupsStatus = (getState() as VesselGroupSliceState).vesselGroups.search - .status - // Fetched already in progress, don't need to re-fetch - return workspaceVesselGroupsStatus !== AsyncReducerStatus.Loading - }, - } -) - -export const getVesselInVesselGroupThunk = createAsyncThunk( - 'vessel-groups/getVessels', - async ( - { vesselGroup }: { vesselGroup: VesselGroup }, - { signal, rejectWithValue, getState, dispatch } - ) => { - const state = getState() as any - const datasets = uniq(vesselGroup.vessels.flatMap((v) => v.dataset || [])) - const datasetId = datasets[0] - let dataset = selectDatasetById(datasetId)(state) - if (!dataset) { - const action = await dispatch(fetchDatasetByIdThunk(datasetId)) - if (fetchDatasetByIdThunk.fulfilled.match(action)) { - dataset = action.payload - } - } - if (vesselGroup.id && dataset) { - const datasetConfig: DataviewDatasetConfig = { - endpoint: EndpointId.VesselList, - datasetId: '', - params: [], - query: [ - { - id: 'vessel-groups', - value: vesselGroup.id, - }, - { - id: 'cache', - value: false, - }, - ], - } - try { - const url = resolveEndpoint(dataset, datasetConfig) - if (!url) { - console.warn('Missing search url') - return rejectWithValue({ - code: 0, - message: 'Missing search url', - }) - } - const vessels = await GFWAPI.fetch>(url, { - signal, - cache: 'reload', - }) - return vessels.entries - } catch (e: any) { - console.warn(e) - return rejectWithValue(parseAPIError(e)) - } - } else { - console.warn('No search datasets found') - return rejectWithValue({ - code: 0, - message: 'No search datasets found', - }) - } - }, - { - condition: (_, { getState }) => { - const workspaceVesselGroupsStatus = (getState() as VesselGroupSliceState).vesselGroups.search - .status - // Fetched already in progress, don't need to re-fetch - return workspaceVesselGroupsStatus !== AsyncReducerStatus.Loading - }, - } -) - export const fetchWorkspaceVesselGroupsThunk = createAsyncThunk( 'workspace-vessel-groups/fetch', async (ids: string[] = [], { signal, rejectWithValue, getState }) => { @@ -305,8 +68,7 @@ export const fetchWorkspaceVesselGroupsThunk = createAsyncThunk( }, { condition: (_, { getState }) => { - const workspaceVesselGroupsStatus = (getState() as VesselGroupSliceState).vesselGroups - .workspace.status + const workspaceVesselGroupsStatus = (getState() as VesselGroupSliceState).vesselGroups.status // Fetched already in progress, don't need to re-fetch return workspaceVesselGroupsStatus !== AsyncReducerStatus.Loading }, @@ -448,82 +210,20 @@ export const { slice: vesselGroupsSlice, entityAdapter } = createAsyncSlice< name: 'vesselGroups', initialState, reducers: { - setVesselGroupsModalOpen: (state, action: PayloadAction) => { - state.isModalOpen = action.payload - }, - setVesselGroupSearchId: (state, action: PayloadAction) => { - state.search.id = action.payload - }, - resetVesselGroupStatus: (state) => { - state.status = AsyncReducerStatus.Idle - state.search.status = AsyncReducerStatus.Idle - }, - setVesselGroupSearchVessels: (state, action: PayloadAction) => { - state.search.vessels = action.payload - }, - setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { - state.newSearchVessels = action.payload - }, - setVesselGroupVessels: (state, action: PayloadAction) => { - state.groupVessels = action.payload - }, - setVesselGroupEditId: (state, action: PayloadAction) => { - state.vesselGroupEditId = action.payload - }, - setVesselGroupConfirmationMode: (state, action: PayloadAction) => { - state.confirmationMode = action.payload - }, - setVesselGroupCurrentDataviewIds: (state, action: PayloadAction) => { - state.currentDataviewIds = action.payload - }, - resetVesselGroup: (state) => { - // Dont reset async reducer properties as it contains the list of existing vessel gruops - const { status, statusId, error, ids, currentRequestIds, entities } = state - return { ...initialState, ids, entities, status, statusId, error, currentRequestIds } + resetVesselGroup: () => { + return { ...initialState } }, }, + extraReducers(builder) { - builder.addCase(searchVesselGroupsVesselsThunk.pending, (state) => { - state.search.status = AsyncReducerStatus.Loading - state.search.vessels = null - }) - builder.addCase(searchVesselGroupsVesselsThunk.fulfilled, (state, action) => { - state.search.status = AsyncReducerStatus.Finished - state.search.vessels = action.payload - }) - builder.addCase(searchVesselGroupsVesselsThunk.rejected, (state, action) => { - if (action.error.message === 'Aborted') { - state.search.status = AsyncReducerStatus.Idle - } else { - state.search.status = AsyncReducerStatus.Error - state.search.error = action.payload as ParsedAPIError - } - }) - builder.addCase(getVesselInVesselGroupThunk.pending, (state) => { - state.search.status = AsyncReducerStatus.Loading - state.search.vessels = null - }) - builder.addCase(getVesselInVesselGroupThunk.fulfilled, (state, action) => { - state.search.status = AsyncReducerStatus.Finished - state.search.vessels = action.payload - }) - builder.addCase(getVesselInVesselGroupThunk.rejected, (state, action) => { - if (action.error.message === 'Aborted') { - state.search.status = AsyncReducerStatus.Idle - } else { - state.search.status = AsyncReducerStatus.Error - state.search.error = action.payload as ParsedAPIError - } - }) builder.addCase(fetchWorkspaceVesselGroupsThunk.pending, (state) => { state.workspace.status = AsyncReducerStatus.Loading - state.workspace.vesselGroups = null }) builder.addCase( fetchWorkspaceVesselGroupsThunk.fulfilled, (state, action: PayloadAction) => { state.workspace.status = AsyncReducerStatus.Finished - state.workspace.vesselGroups = action.payload + entityAdapter.addMany(state, action.payload) } ) builder.addCase(fetchWorkspaceVesselGroupsThunk.rejected, (state, action) => { @@ -544,19 +244,6 @@ export const { slice: vesselGroupsSlice, entityAdapter } = createAsyncSlice< }, }) -export const { - resetVesselGroup, - resetVesselGroupStatus, - setVesselGroupEditId, - setVesselGroupVessels, - setVesselGroupSearchId, - setVesselGroupsModalOpen, - setVesselGroupSearchVessels, - setNewVesselGroupSearchVessels, - setVesselGroupCurrentDataviewIds, - setVesselGroupConfirmationMode, -} = vesselGroupsSlice.actions - export const { selectAll: selectAllVesselGroups, selectById } = entityAdapter.getSelectors((state) => state.vesselGroups) @@ -564,32 +251,13 @@ export const selectVesselGroupById = memoize((id: string) => createSelector([(state: VesselGroupSliceState) => state], (state) => selectById(state, id)) ) -export const selectVesselGroupModalOpen = (state: VesselGroupSliceState) => - state.vesselGroups.isModalOpen export const selectVesselGroupsStatus = (state: VesselGroupSliceState) => state.vesselGroups.status export const selectWorkspaceVesselGroupsStatus = (state: VesselGroupSliceState) => state.vesselGroups.workspace.status + export const selectWorkspaceVesselGroupsError = (state: VesselGroupSliceState) => state.vesselGroups.workspace.error -export const selectVesselGroupsVessels = (state: VesselGroupSliceState) => - state.vesselGroups.groupVessels -export const selectWorkspaceVesselGroups = (state: VesselGroupSliceState) => - state.vesselGroups.workspace.vesselGroups -export const selectVesselGroupSearchId = (state: VesselGroupSliceState) => - state.vesselGroups.search.id -export const selectVesselGroupSearchStatus = (state: VesselGroupSliceState) => - state.vesselGroups.search.status -export const selectVesselGroupSearchVessels = (state: VesselGroupSliceState) => - state.vesselGroups.search.vessels -export const selectNewVesselGroupSearchVessels = (state: VesselGroupSliceState) => - state.vesselGroups.newSearchVessels export const selectVesselGroupsStatusId = (state: VesselGroupSliceState) => state.vesselGroups.statusId -export const selectCurrentDataviewIds = (state: VesselGroupSliceState) => - state.vesselGroups.currentDataviewIds -export const selectVesselGroupEditId = (state: VesselGroupSliceState) => - state.vesselGroups.vesselGroupEditId -export const selectVesselGroupConfirmationMode = (state: VesselGroupSliceState) => - state.vesselGroups.confirmationMode export default vesselGroupsSlice.reducer diff --git a/apps/fishing-map/features/workspace/common/LayerFilters.tsx b/apps/fishing-map/features/workspace/common/LayerFilters.tsx index 73e2e92f7b..ed4ec4e30e 100644 --- a/apps/fishing-map/features/workspace/common/LayerFilters.tsx +++ b/apps/fishing-map/features/workspace/common/LayerFilters.tsx @@ -34,7 +34,7 @@ import { useAppDispatch } from 'features/app/app.hooks' import { setVesselGroupCurrentDataviewIds, setVesselGroupsModalOpen, -} from 'features/vessel-groups/vessel-groups.slice' +} from 'features/vessel-groups/vessel-groups-modal.slice' import { trackEvent, TrackCategory } from 'features/app/analytics.hooks' import { listAsSentence } from 'utils/shared' import UserGuideLink from 'features/help/UserGuideLink' diff --git a/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx b/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx index c051861529..ecc43c3501 100644 --- a/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx +++ b/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx @@ -16,7 +16,7 @@ import { setNewVesselGroupSearchVessels, setVesselGroupEditId, setVesselGroupsModalOpen, -} from 'features/vessel-groups/vessel-groups.slice' +} from 'features/vessel-groups/vessel-groups-modal.slice' import { selectIsGFWUser } from 'features/user/selectors/user.selectors' import { selectReadOnly } from 'features/app/selectors/app.selectors' import Color from '../common/Color' @@ -66,7 +66,8 @@ function VesselGroupLayerPanel({ const onEditClick = () => { if (vesselGroup && (vesselGroup?.id || !vesselGroup?.vessels?.length)) { dispatch(setVesselGroupEditId(vesselGroup.id)) - dispatch(setNewVesselGroupSearchVessels(vesselGroup.vessels)) + // TODO:VV3 remove this any + dispatch(setNewVesselGroupSearchVessels(vesselGroup.vessels as any)) dispatch(setVesselGroupsModalOpen(true)) } } diff --git a/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsSection.tsx b/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsSection.tsx index c26039d785..cc3a32d294 100644 --- a/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsSection.tsx +++ b/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsSection.tsx @@ -13,12 +13,12 @@ import { useAppDispatch } from 'features/app/app.hooks' import { selectVesselGroupsStatusId, selectWorkspaceVesselGroupsStatus, - setVesselGroupsModalOpen, } from 'features/vessel-groups/vessel-groups.slice' import UserLoggedIconButton from 'features/user/UserLoggedIconButton' import { NEW_VESSEL_GROUP_ID } from 'features/vessel-groups/vessel-groups.hooks' import { AsyncReducerStatus } from 'utils/async-slice' import { getVesselGroupDataviewInstance } from 'features/reports/vessel-groups/vessel-group-report.dataviews' +import { setVesselGroupsModalOpen } from 'features/vessel-groups/vessel-groups-modal.slice' import VesselGroupLayerPanel from './VesselGroupsLayerPanel' function VesselGroupSection(): React.ReactElement { diff --git a/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx b/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx index a7df1e4709..fd0d98cad3 100644 --- a/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx +++ b/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx @@ -29,14 +29,12 @@ import VesselGroupAddButton from 'features/vessel-groups/VesselGroupAddButton' import { selectWorkspaceVessselGroupsIds } from 'features/vessel-groups/vessel-groups.selectors' import { NEW_VESSEL_GROUP_ID } from 'features/vessel-groups/vessel-groups.hooks' import UserLoggedIconButton from 'features/user/UserLoggedIconButton' -import { - selectVesselGroupsStatus, - setVesselGroupConfirmationMode, -} from 'features/vessel-groups/vessel-groups.slice' +import { selectVesselGroupsStatus } from 'features/vessel-groups/vessel-groups.slice' import { AsyncReducerStatus } from 'utils/async-slice' import { useAppDispatch } from 'features/app/app.hooks' import { getVesselGroupDataviewInstance } from 'features/reports/vessel-groups/vessel-group-report.dataviews' import { selectActiveVesselsDataviews } from 'features/dataviews/selectors/dataviews.categories.selectors' +import { setVesselGroupConfirmationMode } from 'features/vessel-groups/vessel-groups-modal.slice' import VesselEventsLegend from './VesselEventsLegend' import VesselLayerPanel from './VesselLayerPanel' import VesselsFromPositions from './VesselsFromPositions' diff --git a/apps/fishing-map/reducers.ts b/apps/fishing-map/reducers.ts index 5cf15acb8a..cbd25285ff 100644 --- a/apps/fishing-map/reducers.ts +++ b/apps/fishing-map/reducers.ts @@ -27,6 +27,7 @@ import timebarReducer from 'features/timebar/timebar.slice' import titleReducer from 'routes/title.reducer' import userReducer from 'features/user/user.slice' import vesselGroupReportReducer from 'features/reports/vessel-groups/vessel-group-report.slice' +import vesselGroupsModalReducer from 'features/vessel-groups/vessel-groups-modal.slice' import vesselGroupsReducer from 'features/vessel-groups/vessel-groups.slice' import vesselReducer from 'features/vessel/vessel.slice' import workspaceReducer from 'features/workspace/workspace.slice' @@ -62,6 +63,7 @@ export const rootReducer = combineReducers({ user: userReducer, vessel: vesselReducer, vesselGroupReport: vesselGroupReportReducer, + vesselGroupModal: vesselGroupsModalReducer, vesselGroups: vesselGroupsReducer, workspace: workspaceReducer, workspaces: workspacesReducer, From 816905f3308d4ec18ef4140419b0999a902d1559 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 25 Sep 2024 22:45:47 +0200 Subject: [PATCH 02/14] search vessel group vessels using POST --- .../vessel-groups/VesselGroupModal.tsx | 9 ++- .../vessel-groups-modal.slice.ts | 64 +++++++++++-------- .../vessel-groups/vessel-groups.selectors.ts | 16 ++--- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx index 87aa8ce029..8711fc78df 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx @@ -17,7 +17,7 @@ import VesselGroupVessels from 'features/vessel-groups/VesselGroupModalVessels' import { useAppDispatch } from 'features/app/app.hooks' import { selectAllVesselGroupSearchVessels, - selectHasVesselGroupVessels, + selectHasVesselGroupSearchVessels, selectHasVesselGroupVesselsOverflow, selectVesselGroupWorkspaceToNavigate, selectWorkspaceVessselGroupsIds, @@ -50,7 +50,6 @@ import styles from './VesselGroupModal.module.css' import { getVesselInVesselGroupThunk, MAX_VESSEL_GROUP_VESSELS, - MAX_VESSEL_GROUP_SEARCH_VESSELS, resetVesselGroupModal, resetVesselGroupModalStatus, searchVesselGroupsVesselsThunk, @@ -94,12 +93,12 @@ function VesselGroupModal(): React.ReactElement { const [createAsPublic, setCreateAsPublic] = useState(true) const vesselGroupSearchVessels = useSelector(selectAllVesselGroupSearchVessels) const hasVesselsOverflow = useSelector(selectHasVesselGroupVesselsOverflow) - const hasVesselGroupsVessels = useSelector(selectHasVesselGroupVessels) + const hasVesselGroupsVessels = useSelector(selectHasVesselGroupSearchVessels) const vesselGroupsInWorkspace = useSelector(selectWorkspaceVessselGroupsIds) const { upsertDataviewInstance } = useDataviewInstancesConnect() const searchVesselGroupsVesselsRef = useRef() const searchVesselGroupsVesselsAllowed = vesselGroupVessels - ? vesselGroupVessels?.length < MAX_VESSEL_GROUP_SEARCH_VESSELS + ? vesselGroupVessels?.length < MAX_VESSEL_GROUP_VESSELS : true const dispatchSearchVesselsGroupsThunk = useCallback( @@ -367,7 +366,7 @@ function VesselGroupModal(): React.ReactElement { {t('vesselGroup.searchLimit', { defaultValue: 'Search is limited up to {{limit}} vessels', - limit: MAX_VESSEL_GROUP_SEARCH_VESSELS, + limit: MAX_VESSEL_GROUP_VESSELS, })} )} diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index cb803ae7b5..2d8a7af406 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -1,6 +1,7 @@ import { createAsyncThunk, PayloadAction, createSlice } from '@reduxjs/toolkit' import { uniq, uniqBy } from 'es-toolkit' import { RootState } from 'reducers' +import { cache } from 'react' import { APIPagination, APIVesselSearchPagination, @@ -20,9 +21,6 @@ import { fetchDatasetByIdThunk, selectDatasetById } from '../datasets/datasets.s export const MAX_VESSEL_GROUP_VESSELS = 1000 -// Limitation of the API as we have a limitation of 2000 characters in GET requests -export const MAX_VESSEL_GROUP_SEARCH_VESSELS = 400 - export type IdField = 'vesselId' | 'mmsi' export type VesselGroupConfirmationMode = 'save' | 'saveAndSeeInWorkspace' | 'saveAndDeleteVessels' @@ -41,26 +39,35 @@ interface VesselGroupModalState { newSearchVessels: IdentityVessel[] | null } -const fetchSearchVessels = async ( - url: string, - { signal, token }: { signal?: AbortSignal; token?: string } -) => { +type SearchVesselsBody = { datasets: string[]; where?: string; ids?: string[] } +type FetchSearchVessels = { url: string; body?: SearchVesselsBody; signal?: AbortSignal } + +const fetchSearchVessels = async ({ + url, + body, + signal, + token, +}: FetchSearchVessels & { token?: string }) => { const searchResponse = await GFWAPI.fetch>( `${url}${token ? `&since=${token}` : ''}`, { signal, + ...(body && { + method: 'POST', + body, + }), } ) return searchResponse } const SEARCH_PAGINATION = 25 -const fetchAllSearchVessels = async (url: string, signal: AbortSignal) => { +const fetchAllSearchVessels = async (params: FetchSearchVessels) => { let searchResults = [] as IdentityVessel[] let pendingResults = true let paginationToken = '' while (pendingResults) { - const searchResponse = await fetchSearchVessels(url, { signal, token: paginationToken }) + const searchResponse = await fetchSearchVessels({ ...params, token: paginationToken }) searchResults = searchResults.concat(searchResponse.entries) if (searchResponse.since && searchResults!?.length < searchResponse.total) { paginationToken = searchResponse.since @@ -111,24 +118,17 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( const isVesselByIdSearch = idField === 'vesselId' const datasetConfig: DataviewDatasetConfig = { endpoint: isVesselByIdSearch ? EndpointId.VesselList : EndpointId.VesselSearch, - datasetId: searchDatasets[0].id, + datasetId: '', params: [], - query: [ - { - id: 'datasets', - value: datasets, - }, - isVesselByIdSearch - ? { id: 'ids', value: uniqVesselIds } - : { - id: 'where', - value: encodeURIComponent( - `${uniqVesselIds.map((ssvid) => `ssvid='${ssvid}'`).join(' OR ')}` - ), - }, - ], + query: [{ id: 'cache', value: false }], } - if (!isVesselByIdSearch) { + if (isVesselByIdSearch) { + datasetConfig.query?.push({ id: 'ids', value: uniqVesselIds }) + datasetConfig.query?.push({ + id: 'datasets', + value: datasets, + }) + } else { datasetConfig.query?.push({ id: 'limit', value: SEARCH_PAGINATION, @@ -143,7 +143,19 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( message: 'Missing search url', }) } - const searchResults = await fetchAllSearchVessels(`${url}&cache=false`, signal) + const fetchBody = isVesselByIdSearch + ? undefined + : { + datasets, + where: encodeURIComponent( + `${uniqVesselIds.map((ssvid) => `ssvid='${ssvid}'`).join(' OR ')}` + ), + } + const searchResults = await fetchAllSearchVessels({ + url: `${url}`, + body: fetchBody, + signal, + }) // API returns multiple instances of the same vessel with the same id and dataset const uniqSearchResults = uniqBy(searchResults, (vessel) => [getVesselId(vessel), vessel.dataset].join(',') diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts index 772df490ca..c961d00d83 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts @@ -1,10 +1,7 @@ import { createSelector } from '@reduxjs/toolkit' import { isAdvancedSearchAllowed } from 'features/search/search.selectors' import { selectLocationQuery, selectUrlDataviewInstances } from 'routes/routes.selectors' -import { - MAX_VESSEL_GROUP_VESSELS, - selectVesselGroupsVessels, -} from 'features/vessel-groups/vessel-groups-modal.slice' +import { MAX_VESSEL_GROUP_VESSELS } from 'features/vessel-groups/vessel-groups-modal.slice' import { selectLastVisitedWorkspace, selectWorkspace, @@ -34,13 +31,10 @@ export const selectHasVesselGroupVesselsOverflow = createSelector( } ) -export const selectHasVesselGroupVessels = createSelector( - [selectVesselGroupsVessels, selectAllVesselGroupSearchVessels], - (vessels = [], searchVessels = []) => { - return ( - (vessels !== null && vessels.length > 0) || - (searchVessels !== null && searchVessels.length > 0) - ) +export const selectHasVesselGroupSearchVessels = createSelector( + [selectAllVesselGroupSearchVessels], + (vessels = []) => { + return vessels.length > 0 } ) From 37fa500d4efb7ba17e20e4860baca1b9c3d61a68 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 25 Sep 2024 22:59:29 +0200 Subject: [PATCH 03/14] fix remove vessels in vessel group --- .../features/vessel-groups/VesselGroupModal.tsx | 4 +++- .../features/vessel-groups/vessel-groups.slice.ts | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx index 8711fc78df..269f5c3783 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx @@ -45,6 +45,7 @@ import { selectVesselGroupsStatus, VesselGroupConfirmationMode, updateVesselGroupVesselsThunk, + UpdateVesselGroupThunkParams, } from './vessel-groups.slice' import styles from './VesselGroupModal.module.css' import { @@ -192,10 +193,11 @@ function VesselGroupModal(): React.ReactElement { }) let dispatchedAction if (editingVesselGroupId) { - const vesselGroup = { + const vesselGroup: UpdateVesselGroupThunkParams = { id: editingVesselGroupId, name: groupName, vessels, + override: true, } dispatchedAction = await dispatch(updateVesselGroupVesselsThunk(vesselGroup)) } else { diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts index 85907bdb13..2de516a38e 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts @@ -147,6 +147,7 @@ export const createVesselGroupThunk = createAsyncThunk( export type UpdateVesselGroupThunkParams = VesselGroupUpsert & { id: string + override?: boolean } export const updateVesselGroupThunk = createAsyncThunk( 'vessel-groups/update', @@ -166,9 +167,12 @@ export const updateVesselGroupThunk = createAsyncThunk( export const updateVesselGroupVesselsThunk = createAsyncThunk( 'vessel-groups/update-vessels', - async ({ id, vessels = [] }: UpdateVesselGroupThunkParams, { getState, dispatch }) => { + async ( + { id, vessels = [], override = false }: UpdateVesselGroupThunkParams, + { getState, dispatch } + ) => { let vesselGroup = selectVesselGroupById(id)(getState() as any) - if (!vesselGroup) { + if (!override && !vesselGroup) { const action = await dispatch(fetchVesselGroupByIdThunk(id)) if (fetchVesselGroupByIdThunk.fulfilled.match(action) && action.payload) { vesselGroup = action.payload @@ -178,7 +182,7 @@ export const updateVesselGroupVesselsThunk = createAsyncThunk( return dispatch( updateVesselGroupThunk({ id: vesselGroup.id, - vessels: [...vesselGroup.vessels, ...vessels], + vessels: override ? vessels : [...vesselGroup.vessels, ...vessels], }) ) } From 30cedd22a2fda74c8fe283deb027217a85aa6684 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 25 Sep 2024 22:59:41 +0200 Subject: [PATCH 04/14] remove not used vessel-group-modal state --- .../reports/activity/vessels/ReportVesselsTableFooter.tsx | 8 +------- .../reports/events/VGREventsVesselsTableFooter.tsx | 3 --- .../vessels/VesselGroupReportVesselsTableFooter.tsx | 3 --- apps/fishing-map/features/search/SearchActions.tsx | 8 +------- .../features/vessel-groups/vessel-groups-modal.slice.ts | 8 -------- .../features/workspace/common/LayerFilters.tsx | 6 +----- 6 files changed, 3 insertions(+), 33 deletions(-) diff --git a/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx b/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx index e58f11fd3a..85fc3b4db3 100644 --- a/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx +++ b/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx @@ -11,10 +11,7 @@ import VesselGroupAddButton from 'features/vessel-groups/VesselGroupAddButton' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import { REPORT_SHOW_MORE_VESSELS_PER_PAGE, REPORT_VESSELS_PER_PAGE } from 'data/config' import { useAppDispatch } from 'features/app/app.hooks' -import { - setVesselGroupConfirmationMode, - setVesselGroupCurrentDataviewIds, -} from 'features/vessel-groups/vessel-groups-modal.slice' +import { setVesselGroupConfirmationMode } from 'features/vessel-groups/vessel-groups-modal.slice' import { selectActiveActivityAndDetectionsDataviews } from 'features/dataviews/selectors/dataviews.selectors' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { selectReportVesselFilter } from 'features/reports/areas/area-reports.config.selectors' @@ -101,9 +98,6 @@ export default function ReportVesselsTableFooter({ reportName }: ReportVesselsTa const onAddToVesselGroup = () => { const dataviewIds = heatmapDataviews.map(({ id }) => id) dispatch(setVesselGroupConfirmationMode('saveAndSeeInWorkspace')) - if (dataviewIds?.length) { - dispatch(setVesselGroupCurrentDataviewIds(dataviewIds)) - } trackEvent({ category: TrackCategory.VesselGroups, action: 'add_to_vessel_group', diff --git a/apps/fishing-map/features/reports/events/VGREventsVesselsTableFooter.tsx b/apps/fishing-map/features/reports/events/VGREventsVesselsTableFooter.tsx index 08c45fdda0..d93b358b90 100644 --- a/apps/fishing-map/features/reports/events/VGREventsVesselsTableFooter.tsx +++ b/apps/fishing-map/features/reports/events/VGREventsVesselsTableFooter.tsx @@ -75,9 +75,6 @@ export default function VesselGroupReportVesselsTableFooter() { // const onAddToVesselGroup = () => { // const dataviewIds = heatmapDataviews.map(({ id }) => id) // dispatch(setVesselGroupConfirmationMode('saveAndNavigate')) - // if (dataviewIds?.length) { - // dispatch(setVesselGroupCurrentDataviewIds(dataviewIds)) - // } // trackEvent({ // category: TrackCategory.VesselGroups, // action: 'add_to_vessel_group', diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx index 514f9f8fbb..e3a05dd61f 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx @@ -87,9 +87,6 @@ export default function VesselGroupReportVesselsTableFooter() { // const onAddToVesselGroup = () => { // const dataviewIds = heatmapDataviews.map(({ id }) => id) // dispatch(setVesselGroupConfirmationMode('saveAndNavigate')) - // if (dataviewIds?.length) { - // dispatch(setVesselGroupCurrentDataviewIds(dataviewIds)) - // } // trackEvent({ // category: TrackCategory.VesselGroups, // action: 'add_to_vessel_group', diff --git a/apps/fishing-map/features/search/SearchActions.tsx b/apps/fishing-map/features/search/SearchActions.tsx index aa919437aa..d7eb40f9ef 100644 --- a/apps/fishing-map/features/search/SearchActions.tsx +++ b/apps/fishing-map/features/search/SearchActions.tsx @@ -14,10 +14,7 @@ import VesselGroupAddButton, { VesselGroupAddActionButton, } from 'features/vessel-groups/VesselGroupAddButton' import { selectActiveActivityAndDetectionsDataviews } from 'features/dataviews/selectors/dataviews.selectors' -import { - setVesselGroupConfirmationMode, - setVesselGroupCurrentDataviewIds, -} from 'features/vessel-groups/vessel-groups-modal.slice' +import { setVesselGroupConfirmationMode } from 'features/vessel-groups/vessel-groups-modal.slice' import { HOME, WORKSPACE } from 'routes/routes' import { EMPTY_FILTERS } from 'features/search/search.config' import { getRelatedIdentityVesselIds } from 'features/vessel/vessel.utils' @@ -76,9 +73,6 @@ function SearchActions() { const onAddToVesselGroup = () => { const dataviewIds = heatmapDataviews.map(({ id }) => id) dispatch(setVesselGroupConfirmationMode('saveAndSeeInWorkspace')) - if (dataviewIds?.length) { - dispatch(setVesselGroupCurrentDataviewIds(dataviewIds)) - } trackEvent({ category: TrackCategory.SearchVessel, action: 'Click add to vessel group', diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index 2d8a7af406..015488c52c 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -27,7 +27,6 @@ export type VesselGroupConfirmationMode = 'save' | 'saveAndSeeInWorkspace' | 'sa interface VesselGroupModalState { isModalOpen: boolean vesselGroupEditId: string | null - currentDataviewIds: string[] | null confirmationMode: VesselGroupConfirmationMode groupVessels: VesselGroupVessel[] | null search: { @@ -81,7 +80,6 @@ const fetchAllSearchVessels = async (params: FetchSearchVessels) => { const initialState: VesselGroupModalState = { isModalOpen: false, vesselGroupEditId: null, - currentDataviewIds: null, confirmationMode: 'save', groupVessels: null, search: { @@ -291,9 +289,6 @@ export const vesselGroupModalSlice = createSlice({ setVesselGroupConfirmationMode: (state, action: PayloadAction) => { state.confirmationMode = action.payload }, - setVesselGroupCurrentDataviewIds: (state, action: PayloadAction) => { - state.currentDataviewIds = action.payload - }, resetVesselGroupModal: (state) => { return { ...initialState } }, @@ -343,7 +338,6 @@ export const { setVesselGroupsModalOpen, setVesselGroupSearchVessels, setNewVesselGroupSearchVessels, - setVesselGroupCurrentDataviewIds, setVesselGroupConfirmationMode, } = vesselGroupModalSlice.actions @@ -356,8 +350,6 @@ export const selectVesselGroupSearchVessels = (state: RootState) => export const selectVesselGroupsVessels = (state: RootState) => state.vesselGroupModal.groupVessels export const selectNewVesselGroupSearchVessels = (state: RootState) => state.vesselGroupModal.newSearchVessels -export const selectCurrentDataviewIds = (state: RootState) => - state.vesselGroupModal.currentDataviewIds export const selectVesselGroupEditId = (state: RootState) => state.vesselGroupModal.vesselGroupEditId export const selectVesselGroupConfirmationMode = (state: RootState) => diff --git a/apps/fishing-map/features/workspace/common/LayerFilters.tsx b/apps/fishing-map/features/workspace/common/LayerFilters.tsx index ed4ec4e30e..bd0bf9e0cd 100644 --- a/apps/fishing-map/features/workspace/common/LayerFilters.tsx +++ b/apps/fishing-map/features/workspace/common/LayerFilters.tsx @@ -31,10 +31,7 @@ import HistogramRangeFilter from 'features/workspace/environmental/HistogramRang import { useVesselGroupsOptions } from 'features/vessel-groups/vessel-groups.hooks' import { selectVessselGroupsAllowed } from 'features/vessel-groups/vessel-groups.selectors' import { useAppDispatch } from 'features/app/app.hooks' -import { - setVesselGroupCurrentDataviewIds, - setVesselGroupsModalOpen, -} from 'features/vessel-groups/vessel-groups-modal.slice' +import { setVesselGroupsModalOpen } from 'features/vessel-groups/vessel-groups-modal.slice' import { trackEvent, TrackCategory } from 'features/app/analytics.hooks' import { listAsSentence } from 'utils/shared' import UserGuideLink from 'features/help/UserGuideLink' @@ -252,7 +249,6 @@ function LayerFilters({ }: OnSelectFilterArgs) => { if ((selection as MultiSelectOption)?.id === VESSEL_GROUPS_MODAL_ID) { dispatch(setVesselGroupsModalOpen(true)) - dispatch(setVesselGroupCurrentDataviewIds([dataview.id])) return } let filterValues: number | string[] From c908afaa6f40d99ae226f569514bc70636a1ba57 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 26 Sep 2024 09:00:16 +0200 Subject: [PATCH 05/14] show warning on outdated user vessel groups --- .../features/user/UserVesselGroups.tsx | 34 ++++++++++++++----- .../vessel-groups/vessel-groups.config.ts | 3 ++ .../vessel-groups/vessel-groups.utils.ts | 5 +++ .../public/locales/source/translations.json | 1 + libs/api-types/src/vesselGroups.ts | 1 + 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/fishing-map/features/user/UserVesselGroups.tsx b/apps/fishing-map/features/user/UserVesselGroups.tsx index 24aed3f339..67aedde949 100644 --- a/apps/fishing-map/features/user/UserVesselGroups.tsx +++ b/apps/fishing-map/features/user/UserVesselGroups.tsx @@ -1,7 +1,6 @@ import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { useCallback } from 'react' -import Link from 'redux-first-router-link' import { Spinner, IconButton, Button } from '@globalfishingwatch/ui-components' import { VesselGroup } from '@globalfishingwatch/api-types' import { AsyncReducerStatus } from 'utils/async-slice' @@ -12,7 +11,10 @@ import { } from 'features/vessel-groups/vessel-groups.slice' import { useAppDispatch } from 'features/app/app.hooks' import { selectDatasetsStatus } from 'features/datasets/datasets.slice' -import { getVesselGroupLabel } from 'features/vessel-groups/vessel-groups.utils' +import { + getVesselGroupLabel, + isOutdatedVesselGroup, +} from 'features/vessel-groups/vessel-groups.utils' import { sortByCreationDate } from 'utils/dates' import VesselGroupReportLink from 'features/reports/vessel-groups/VesselGroupReportLink' import { @@ -78,23 +80,39 @@ function UserVesselGroups() {
    {vesselGroups && vesselGroups.length > 0 ? ( sortByCreationDate(vesselGroups).map((vesselGroup) => { + const isOutdated = isOutdatedVesselGroup(vesselGroup) return (
  • - - + {isOutdated ? ( + {getVesselGroupLabel(vesselGroup)}{' '} ({vesselGroup.vessels.length}) - - + ) : ( + + + {getVesselGroupLabel(vesselGroup)}{' '} + ({vesselGroup.vessels.length}) + + + + )}
    onEditClick(vesselGroup)} /> id: key, label: key.toUpperCase(), })) + +// TODO:VV3 set the proper date +export const VESSEL_GROUPS_REPORT_RELEASE_DATE = '2024-09-25' diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts index 52d87dd24e..6f5df32990 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts @@ -1,7 +1,12 @@ import { VesselGroup } from '@globalfishingwatch/api-types' import { PUBLIC_SUFIX } from 'data/config' +import { VESSEL_GROUPS_REPORT_RELEASE_DATE } from './vessel-groups.config' export const getVesselGroupLabel = (vesselGroup: VesselGroup) => { const isPrivate = !vesselGroup.id.endsWith(`-${PUBLIC_SUFIX}`) return `${isPrivate ? '🔒 ' : ''}${vesselGroup.name}` } + +export const isOutdatedVesselGroup = (vesselGroup: VesselGroup) => { + return vesselGroup?.updatedAt && vesselGroup?.updatedAt < VESSEL_GROUPS_REPORT_RELEASE_DATE +} diff --git a/apps/fishing-map/public/locales/source/translations.json b/apps/fishing-map/public/locales/source/translations.json index 9a39ddcd3d..1c5766d219 100644 --- a/apps/fishing-map/public/locales/source/translations.json +++ b/apps/fishing-map/public/locales/source/translations.json @@ -1013,6 +1013,7 @@ "addToWorkspace": "Add vessel group to workspace", "addVessels": "Add vessels to vessel group", "addVisibleVessels": "Add visible vessels to vessel group", + "clickToUpdate": "Click to migrate your vessel group to latest available data", "confirmAbort": "You will lose any changes made in this vessel group. Are you sure?", "confirmRemove": "Are you sure you want to permanently delete this vessel group?", "createNewGroup": "Create new group", diff --git a/libs/api-types/src/vesselGroups.ts b/libs/api-types/src/vesselGroups.ts index f1481a1589..979f1dadaa 100644 --- a/libs/api-types/src/vesselGroups.ts +++ b/libs/api-types/src/vesselGroups.ts @@ -13,6 +13,7 @@ export interface VesselGroup { ownerId?: number ownerType?: string createdAt?: string + updatedAt?: string } export type VesselGroupUpsert = Partial> From 5fe3510e0d734066048e56d6d2704d289f165e87 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 26 Sep 2024 09:05:42 +0200 Subject: [PATCH 06/14] fetch related and flat vessel identities in vessel groups --- .../vessel-groups/VesselGroupReportTitle.tsx | 3 +- .../search/advanced/SearchAdvancedResults.tsx | 2 +- .../features/user/UserVesselGroups.tsx | 2 + .../vessel-groups/VesselGroupModal.tsx | 11 ++-- .../vessel-groups/VesselGroupModalVessels.tsx | 58 ++++++------------- .../vessel-groups-modal.slice.ts | 36 ++++++++---- .../vessel-groups/vessel-groups.hooks.ts | 2 +- .../features/vessel/vessel.slice.ts | 5 ++ .../features/vessel/vessel.utils.ts | 55 ++++++++++++++---- 9 files changed, 106 insertions(+), 68 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx index a3a4482e1a..266df12ea9 100644 --- a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx @@ -8,6 +8,7 @@ import { useAppDispatch } from 'features/app/app.hooks' import ReportTitlePlaceholder from 'features/reports/areas/placeholders/ReportTitlePlaceholder' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { + getVesselsGroupIdentities, setNewVesselGroupSearchVessels, setVesselGroupEditId, setVesselGroupsModalOpen, @@ -35,7 +36,7 @@ export default function VesselGroupReportTitle({ vesselGroup, loading }: ReportT const onEditClick = useCallback(() => { if (vesselGroup?.id || !vesselGroup?.vessels?.length) { dispatch(setVesselGroupEditId(vesselGroup.id)) - dispatch(setNewVesselGroupSearchVessels(vesselGroup.vessels)) + dispatch(setNewVesselGroupSearchVessels(getVesselsGroupIdentities(vesselGroup.vessels))) dispatch(setVesselGroupsModalOpen(true)) } }, [dispatch, vesselGroup?.id, vesselGroup?.vessels]) diff --git a/apps/fishing-map/features/search/advanced/SearchAdvancedResults.tsx b/apps/fishing-map/features/search/advanced/SearchAdvancedResults.tsx index 47d9bffcd8..6d253c24e0 100644 --- a/apps/fishing-map/features/search/advanced/SearchAdvancedResults.tsx +++ b/apps/fishing-map/features/search/advanced/SearchAdvancedResults.tsx @@ -202,7 +202,7 @@ function SearchAdvancedResults({ fetchResults, fetchMoreResults }: SearchCompone return ( onVesselClick(e, vesselData)} query={vesselQuery} diff --git a/apps/fishing-map/features/user/UserVesselGroups.tsx b/apps/fishing-map/features/user/UserVesselGroups.tsx index 67aedde949..2f64bceb86 100644 --- a/apps/fishing-map/features/user/UserVesselGroups.tsx +++ b/apps/fishing-map/features/user/UserVesselGroups.tsx @@ -19,6 +19,7 @@ import { sortByCreationDate } from 'utils/dates' import VesselGroupReportLink from 'features/reports/vessel-groups/VesselGroupReportLink' import { selectVesselGroupEditId, + setVesselGroupConfirmationMode, setVesselGroupEditId, setVesselGroupsModalOpen, } from 'features/vessel-groups/vessel-groups-modal.slice' @@ -45,6 +46,7 @@ function UserVesselGroups() { async (vesselGroup: VesselGroup) => { dispatch(setVesselGroupEditId(vesselGroup.id)) dispatch(setVesselGroupsModalOpen(true)) + dispatch(setVesselGroupConfirmationMode('update')) }, [dispatch] ) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx index 269f5c3783..9ff1e1f0a9 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx @@ -34,7 +34,6 @@ import { resetSidebarScroll } from 'features/sidebar/sidebar.utils' import { selectSearchQuery } from 'features/search/search.config.selectors' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import UserGuideLink from 'features/help/UserGuideLink' -import { getVesselId } from 'features/vessel/vessel.utils' import { ID_COLUMNS_OPTIONS } from 'features/vessel-groups/vessel-groups.config' import { selectVesselsDataviews } from 'features/dataviews/selectors/dataviews.instances.selectors' import { getVesselGroupDataviewInstance } from 'features/reports/vessel-groups/vessel-group-report.dataviews' @@ -187,7 +186,7 @@ function VesselGroupModal(): React.ReactElement { setButtonLoading(navigateToWorkspace ? 'saveAndSeeInWorkspace' : 'save') const vessels: VesselGroupVessel[] = vesselGroupSearchVessels.map((vessel) => { return { - vesselId: getVesselId(vessel), + vesselId: vessel.id, dataset: vessel.dataset as string, } }) @@ -391,7 +390,9 @@ function VesselGroupModal(): React.ReactElement { )} {!fullModalLoading && - (confirmationMode === 'save' || confirmationMode === 'saveAndDeleteVessels' ? ( + (confirmationMode === 'save' || + confirmationMode === 'update' || + confirmationMode === 'saveAndDeleteVessels' ? ( ) : ( diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx index a994ee3116..c54e265589 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx @@ -4,16 +4,12 @@ import { useTranslation } from 'react-i18next' import { groupBy } from 'es-toolkit' import { ActionCreatorWithPayload } from '@reduxjs/toolkit' import { IconButton, Tooltip, TransmissionsTimeline } from '@globalfishingwatch/ui-components' -import { IdentityVessel, Locale, VesselRegistryInfo } from '@globalfishingwatch/api-types' +import { Locale } from '@globalfishingwatch/api-types' import { EMPTY_FIELD_PLACEHOLDER, formatInfoField, getVesselGearTypeLabel } from 'utils/info' import { FIRST_YEAR_OF_DATA } from 'data/config' import I18nDate from 'features/i18n/i18nDate' import { useAppDispatch } from 'features/app/app.hooks' -import { - getLatestIdentityPrioritised, - getVesselProperty, - isFieldLoginRequired, -} from 'features/vessel/vessel.utils' +import { isFieldLoginRequired } from 'features/vessel/vessel.utils' import { VesselDataIdentity } from 'features/vessel/vessel.slice' import VesselIdentityFieldLogin from 'features/vessel/identity/VesselIdentityFieldLogin' import styles from './VesselGroupModal.module.css' @@ -36,7 +32,7 @@ function VesselGroupVesselRow({ }: VesselGroupVesselRowProps) { const { t, i18n } = useTranslation() const { shipname, flag, ssvid, transmissionDateFrom, transmissionDateTo } = - vessel || ({} as VesselRegistryInfo) + vessel || ({} as VesselDataIdentity) const vesselName = formatInfoField(shipname, 'name') const vesselGearType = getVesselGearTypeLabel(vessel) @@ -45,15 +41,7 @@ function VesselGroupVesselRow({ {ssvid} {vesselName} - - - {isFieldLoginRequired(vesselGearType) ? ( - - ) : ( - vesselGearType || EMPTY_FIELD_PLACEHOLDER - )} - - + {t(`flags:${flag as string}` as any) || EMPTY_FIELD_PLACEHOLDER} {isFieldLoginRequired(vesselGearType) ? : vesselGearType} @@ -96,20 +84,14 @@ function VesselGroupVesselRow({ } type VesselGroupDataIdentity = VesselDataIdentity & { dataset: string } -function groupSearchVesselsIdentityBy(vessels: IdentityVessel[] | null, groupByKey: string) { +function groupSearchVesselsIdentityBy( + vessels: VesselDataIdentity[] | null, + groupByKey: 'id' | 'ssvid' +) { if (!vessels?.length) { return {} } - return groupBy( - vessels.map( - (v) => - ({ - dataset: v.dataset, - ...getLatestIdentityPrioritised(v), - } as VesselGroupDataIdentity) - ), - (v) => (v as any)[groupByKey] - ) + return groupBy(vessels, (v) => v[groupByKey]) } function VesselGroupVessels() { @@ -120,7 +102,7 @@ function VesselGroupVessels() { ...(vesselGroupSearchVessels || []), ...(newVesselGroupSearchVessels || []), ].some((vessel) => { - const ssvid = getVesselProperty(vessel, 'ssvid') + const ssvid = vessel.ssvid return ssvid !== undefined && ssvid !== '' }) ? 'ssvid' @@ -137,10 +119,8 @@ function VesselGroupVessels() { const vessels = list === 'search' ? vesselGroupSearchVessels : newVesselGroupSearchVessels const action = ( list === 'search' ? setVesselGroupSearchVessels : setNewVesselGroupSearchVessels - ) as ActionCreatorWithPayload - const index = vessels!.findIndex( - (v) => getLatestIdentityPrioritised(v).id === vessel?.id && v.dataset === vessel.dataset - ) + ) as ActionCreatorWithPayload + const index = vessels!.findIndex((v) => v.id === vessel?.id && v.dataset === vessel.dataset) if (index > -1) { dispatch(action([...vessels!.slice(0, index), ...vessels!.slice(index + 1)])) } @@ -165,11 +145,11 @@ function VesselGroupVessels() { {Object.keys(newSearchVesselsGrouped)?.length > 0 && - Object.keys(newSearchVesselsGrouped).map((mmsi) => { - if (!mmsi || mmsi === 'undefined') { + Object.keys(newSearchVesselsGrouped).map((key) => { + if (!key || key === 'undefined') { return null } - const vessels = newSearchVesselsGrouped[mmsi] + const vessels = newSearchVesselsGrouped[key] return vessels.map((vessel) => ( 0 && - Object.keys(searchVesselsGrouped).map((mmsi) => { - if (newSearchVesselsGrouped[mmsi]) { + Object.keys(searchVesselsGrouped).map((key) => { + if (newSearchVesselsGrouped[key]) { return null } - const vessels = searchVesselsGrouped[mmsi] + const vessels = searchVesselsGrouped[key] return ( - + {vessels.map((vessel, i) => ( { + return getVesselIdentities(vessel, { + identitySource: VesselIdentitySourceEnum.SelfReported, + }) + }) +} + export const searchVesselGroupsVesselsThunk = createAsyncThunk( 'vessel-groups/searchVessels', async ( @@ -171,8 +184,7 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( ) }) : uniqSearchResults - - return searchResultsFiltered + return getVesselsGroupIdentities(searchResultsFiltered) } catch (e: any) { console.warn(e) return rejectWithValue(parseAPIError(e)) @@ -224,6 +236,10 @@ export const getVesselInVesselGroupThunk = createAsyncThunk( id: 'cache', value: false, }, + { + id: 'includes', + value: ['POTENTIAL_RELATED_SELF_REPORTED_INFO'], + }, ], } try { @@ -239,7 +255,7 @@ export const getVesselInVesselGroupThunk = createAsyncThunk( signal, cache: 'reload', }) - return vessels.entries + return getVesselsGroupIdentities(vessels.entries) } catch (e: any) { console.warn(e) return rejectWithValue(parseAPIError(e)) @@ -274,10 +290,10 @@ export const vesselGroupModalSlice = createSlice({ resetVesselGroupModalStatus: (state) => { state.search.status = AsyncReducerStatus.Idle }, - setVesselGroupSearchVessels: (state, action: PayloadAction) => { + setVesselGroupSearchVessels: (state, action: PayloadAction) => { state.search.vessels = action.payload }, - setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { + setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { state.newSearchVessels = action.payload }, setVesselGroupVessels: (state, action: PayloadAction) => { diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts index f364df1760..708352b97c 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts @@ -61,7 +61,7 @@ export const useVesselGroupsUpdate = () => { } return { vesselId: id, - dataset: typeof dataset === 'string' ? dataset : dataset.id, + dataset: dataset, } }), } diff --git a/apps/fishing-map/features/vessel/vessel.slice.ts b/apps/fishing-map/features/vessel/vessel.slice.ts index 6354fc3572..ca6dab7b95 100644 --- a/apps/fishing-map/features/vessel/vessel.slice.ts +++ b/apps/fishing-map/features/vessel/vessel.slice.ts @@ -6,12 +6,14 @@ import { ApiEvent, Dataset, DatasetTypes, + GearType, IdentityVessel, Resource, ResourceStatus, SelfReportedInfo, VesselCombinedSourcesInfo, VesselRegistryInfo, + VesselType, } from '@globalfishingwatch/api-types' import { setResource } from '@globalfishingwatch/dataviews-client' import { resolveEndpoint } from '@globalfishingwatch/datasets-client' @@ -42,6 +44,9 @@ export type VesselDataIdentity = (SelfReportedInfo | VesselRegistryInfo) & { identitySource: VesselIdentitySourceEnum combinedSourcesInfo?: VesselCombinedSourcesInfo positionsCounter?: number + dataset?: string + geartypes?: GearType[] + shiptypes?: VesselType[] } // Merges and plain all the identities of a vessel export type IdentityVesselData = { diff --git a/apps/fishing-map/features/vessel/vessel.utils.ts b/apps/fishing-map/features/vessel/vessel.utils.ts index c9f0a30d7c..71a6ff9f20 100644 --- a/apps/fishing-map/features/vessel/vessel.utils.ts +++ b/apps/fishing-map/features/vessel/vessel.utils.ts @@ -18,6 +18,33 @@ import { IdentityVesselData, VesselDataIdentity } from 'features/vessel/vessel.s import { TimeRange } from 'features/timebar/timebar.slice' type GetVesselIdentityParams = { identityId?: string; identitySource?: VesselIdentitySourceEnum } + +const getVesselIdentitiesBySource = ( + vessel: IdentityVessel, + { identitySource } = {} as Pick +): VesselDataIdentity[] => { + if (!identitySource) { + return [] as VesselDataIdentity[] + } + return (vessel?.[identitySource] || []).map((identity) => { + const geartypes = getVesselCombinedSourceProperty(vessel, { + vesselId: identity.id, + property: 'geartypes', + })?.map((i) => i.name as GearType) + const shiptypes = getVesselCombinedSourceProperty(vessel, { + vesselId: identity.id, + property: 'shiptypes', + })?.map((i) => i.name as VesselType) + return { + ...identity, + identitySource, + geartypes, + shiptypes, + dataset: vessel.dataset, + } as VesselDataIdentity + }) +} + export const getVesselIdentities = ( vessel: IdentityVessel | IdentityVesselData, { identitySource } = {} as Pick @@ -25,18 +52,22 @@ export const getVesselIdentities = ( if (!vessel) { return [] as VesselDataIdentity[] } - const identities = (vessel as IdentityVesselData).identities?.length - ? (vessel as IdentityVesselData).identities - : [ - ...((vessel as IdentityVessel).registryInfo || []).map((i) => ({ - ...i, - identitySource: VesselIdentitySourceEnum.Registry, - })), - ...((vessel as IdentityVessel).selfReportedInfo || []).map((i) => ({ - ...i, - identitySource: VesselIdentitySourceEnum.SelfReported, - })), - ].sort((a, b) => (a.transmissionDateTo > b.transmissionDateTo ? -1 : 1)) + + if ((vessel as IdentityVesselData).identities) { + return (vessel as IdentityVesselData).identities.sort((a, b) => + a.transmissionDateTo > b.transmissionDateTo ? -1 : 1 + ) + } + + const identities = [ + ...getVesselIdentitiesBySource(vessel as IdentityVessel, { + identitySource: VesselIdentitySourceEnum.Registry, + }), + ...getVesselIdentitiesBySource(vessel as IdentityVessel, { + identitySource: VesselIdentitySourceEnum.SelfReported, + }), + ].sort((a, b) => (a.transmissionDateTo > b.transmissionDateTo ? -1 : 1)) + return identitySource ? identities.filter((i) => i.identitySource === identitySource) : identities } From 8eb18ed821114bf12b11f9a1f12b56a505a93a90 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 26 Sep 2024 09:40:34 +0200 Subject: [PATCH 07/14] fix POST request --- .../features/vessel-groups/vessel-groups-modal.slice.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index cfe321478e..75f4c621ec 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -158,9 +158,7 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( ? undefined : { datasets, - where: encodeURIComponent( - `${uniqVesselIds.map((ssvid) => `ssvid='${ssvid}'`).join(' OR ')}` - ), + where: `${uniqVesselIds.map((ssvid) => `ssvid='${ssvid}'`).join(' OR ')}`, } const searchResults = await fetchAllSearchVessels({ url: `${url}`, From d215b3b12293e85cb30856b7d99841de70128552 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 26 Sep 2024 20:53:28 +0200 Subject: [PATCH 08/14] include all vessel identities in vessel-groups --- .../features/dataviews/dataviews.utils.ts | 3 +- apps/fishing-map/features/map/map.slice.ts | 3 +- .../reports/areas/area-reports.utils.ts | 3 +- .../reports/events/vgr-events.selectors.ts | 6 +- .../vessel-groups/VesselGroupReportTitle.tsx | 3 +- .../vessel-group-report-insights.selectors.ts | 6 +- .../vessel-group-report.slice.ts | 23 ++-- .../vessels/VesselGroupReportVesselsTable.tsx | 10 +- .../vessel-group-report-vessels.selectors.ts | 43 ++++--- .../features/user/UserVesselGroups.tsx | 9 +- .../vessel-groups/VesselGroupModal.tsx | 8 +- .../vessel-groups/VesselGroupModalSearch.tsx | 4 +- .../vessel-groups/VesselGroupModalVessels.tsx | 120 ++++++------------ .../vessel-groups-modal.slice.ts | 40 +++--- .../vessel-groups/vessel-groups.hooks.ts | 2 + .../vessel-groups/vessel-groups.slice.ts | 4 +- .../vessel-groups/vessel-groups.utils.ts | 70 +++++++++- .../features/vessel/vessel.config.ts | 1 + .../features/vessel/vessel.utils.ts | 24 ++-- libs/api-types/src/vesselGroups.ts | 1 + 20 files changed, 215 insertions(+), 168 deletions(-) diff --git a/apps/fishing-map/features/dataviews/dataviews.utils.ts b/apps/fishing-map/features/dataviews/dataviews.utils.ts index dbcc8300b8..4390951f41 100644 --- a/apps/fishing-map/features/dataviews/dataviews.utils.ts +++ b/apps/fishing-map/features/dataviews/dataviews.utils.ts @@ -25,6 +25,7 @@ import { getActiveDatasetsInDataview, isPrivateDataset, } from 'features/datasets/datasets.utils' +import { INCLUDES_RELATED_SELF_REPORTED_INFO_ID } from 'features/vessel/vessel.config' // used in workspaces with encounter events layers export const ENCOUNTER_EVENTS_SOURCE_ID = 'encounter' @@ -57,7 +58,7 @@ export const getVesselInfoDataviewInstanceDatasetConfig = ( { id: 'dataset', value: info }, { id: 'includes', - value: ['POTENTIAL_RELATED_SELF_REPORTED_INFO'], + value: [INCLUDES_RELATED_SELF_REPORTED_INFO_ID], }, ], endpoint: EndpointId.Vessel, diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index a1b34596ba..7ff4896177 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -47,6 +47,7 @@ import { selectEventsDataviews, selectVesselGroupDataviews, } from 'features/dataviews/selectors/dataviews.categories.selectors' +import { INCLUDES_RELATED_SELF_REPORTED_INFO_ID } from 'features/vessel/vessel.config' export const MAX_TOOLTIP_LIST = 5 @@ -197,7 +198,7 @@ const getVesselInfoEndpoint = (vesselDatasets: Dataset[], vesselIds: string[]) = }, { id: 'includes', - value: ['POTENTIAL_RELATED_SELF_REPORTED_INFO'], + value: [INCLUDES_RELATED_SELF_REPORTED_INFO_ID], }, ], } diff --git a/apps/fishing-map/features/reports/areas/area-reports.utils.ts b/apps/fishing-map/features/reports/areas/area-reports.utils.ts index 5d2c916cfa..185a8b25c7 100644 --- a/apps/fishing-map/features/reports/areas/area-reports.utils.ts +++ b/apps/fishing-map/features/reports/areas/area-reports.utils.ts @@ -22,6 +22,7 @@ import { Bbox, BufferOperation, BufferUnit } from 'types' import { Area, AreaGeometry } from 'features/areas/areas.slice' import { IdentityVesselData, VesselDataIdentity } from 'features/vessel/vessel.slice' import { VesselGroupReportVesselParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.types' +import { VesselGroupVesselTableParsed } from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' import { DEFAULT_BUFFER_OPERATION, DEFAULT_POINT_BUFFER_UNIT, @@ -320,7 +321,7 @@ export const FILTER_PROPERTIES: Record = { } export function getVesselsFiltered< - Vessel = ReportVesselWithDatasets | VesselGroupReportVesselParsed + Vessel = ReportVesselWithDatasets | VesselGroupReportVesselParsed | VesselGroupVesselTableParsed >(vessels: Vessel[], filter: string, filterProperties = FILTER_PROPERTIES) { if (!filter || !filter.length) { return vessels diff --git a/apps/fishing-map/features/reports/events/vgr-events.selectors.ts b/apps/fishing-map/features/reports/events/vgr-events.selectors.ts index f8f25a0444..1704c81239 100644 --- a/apps/fishing-map/features/reports/events/vgr-events.selectors.ts +++ b/apps/fishing-map/features/reports/events/vgr-events.selectors.ts @@ -5,7 +5,7 @@ import { VesselGroupEventsVesselsParams, } from 'queries/vessel-group-events-stats-api' import { selectVGRData } from 'features/reports/vessel-groups/vessel-group-report.slice' -import { getSearchIdentityResolved, getVesselId } from 'features/vessel/vessel.utils' +import { getSearchIdentityResolved } from 'features/vessel/vessel.utils' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import { selectReportVesselGroupId } from 'routes/routes.selectors' import { @@ -49,11 +49,11 @@ export const selectVGREventsVessels = createSelector( return } const insightVessels = vesselGroup?.vessels?.flatMap((vessel) => { - const vesselWithEvents = data?.find((v) => v.vesselId === getVesselId(vessel)) + const vesselWithEvents = data?.find((v) => v.vesselId === vessel.vesselId) if (!vesselWithEvents) { return [] } - const identity = getSearchIdentityResolved(vessel) + const identity = getSearchIdentityResolved(vessel.identity!) return { ...vesselWithEvents, ...identity, diff --git a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx index 266df12ea9..a3a4482e1a 100644 --- a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx @@ -8,7 +8,6 @@ import { useAppDispatch } from 'features/app/app.hooks' import ReportTitlePlaceholder from 'features/reports/areas/placeholders/ReportTitlePlaceholder' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { - getVesselsGroupIdentities, setNewVesselGroupSearchVessels, setVesselGroupEditId, setVesselGroupsModalOpen, @@ -36,7 +35,7 @@ export default function VesselGroupReportTitle({ vesselGroup, loading }: ReportT const onEditClick = useCallback(() => { if (vesselGroup?.id || !vesselGroup?.vessels?.length) { dispatch(setVesselGroupEditId(vesselGroup.id)) - dispatch(setNewVesselGroupSearchVessels(getVesselsGroupIdentities(vesselGroup.vessels))) + dispatch(setNewVesselGroupSearchVessels(vesselGroup.vessels)) dispatch(setVesselGroupsModalOpen(true)) } }, [dispatch, vesselGroup?.id, vesselGroup?.vessels]) diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts index ce7b28d287..1f55db3344 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts @@ -37,13 +37,11 @@ export const selectVGRVesselsByInsight = ( return [] } const insightVessels = vesselGroup?.vessels?.flatMap((vessel) => { - const vesselWithInsight = data?.[insightProperty]?.find( - (v) => v.vesselId === getVesselId(vessel) - ) + const vesselWithInsight = data?.[insightProperty]?.find((v) => v.vesselId === vessel.vesselId) if (!vesselWithInsight || (insightCounter && get(vesselWithInsight, insightCounter) === 0)) { return [] } - return { ...vesselWithInsight, identity: getSearchIdentityResolved(vessel) } + return { ...vesselWithInsight, identity: getSearchIdentityResolved(vessel.identity!) } }) return insightVessels.sort((a, b) => { if (insightCounter) { diff --git a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.slice.ts b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.slice.ts index ad3405aac6..5cd12a691e 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.slice.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.slice.ts @@ -3,9 +3,13 @@ import { stringify } from 'qs' import { GFWAPI } from '@globalfishingwatch/api-client' import { APIPagination, IdentityVessel, VesselGroup } from '@globalfishingwatch/api-types' import { AsyncError, AsyncReducerStatus } from 'utils/async-slice' -import { getVesselProperty } from 'features/vessel/vessel.utils' +import { mergeVesselGroupVesselIdentities } from 'features/vessel-groups/vessel-groups.utils' +import { VesselGroupVesselIdentity } from 'features/vessel-groups/vessel-groups-modal.slice' +import { INCLUDES_RELATED_SELF_REPORTED_INFO_ID } from 'features/vessel/vessel.config' -export type VesselGroupReport = Omit & { vessels: IdentityVessel[] } +export type VesselGroupReport = Omit & { + vessels: VesselGroupVesselIdentity[] +} interface ReportState { status: AsyncReducerStatus @@ -30,20 +34,17 @@ export const fetchVesselGroupReportThunk = createAsyncThunk( async ({ vesselGroupId }: FetchVesselGroupReportThunkParams, { rejectWithValue, signal }) => { try { const vesselGroup = await GFWAPI.fetch(`/vessel-groups/${vesselGroupId}`) + const params = { + 'vessel-groups': [vesselGroupId], + includes: [INCLUDES_RELATED_SELF_REPORTED_INFO_ID], + } const vesselGroupVessels = await GFWAPI.fetch>( - `/vessels?${stringify({ 'vessel-groups': [vesselGroupId] })}`, + `/vessels?${stringify(params)}`, { cache: 'reload', signal } ) return { ...vesselGroup, - vessels: vesselGroupVessels.entries.toSorted((a, b) => { - const aValue = getVesselProperty(a, 'shipname') - const bValue = getVesselProperty(b, 'shipname') - if (aValue === bValue) { - return 0 - } - return aValue > bValue ? 1 : -1 - }), + vessels: mergeVesselGroupVesselIdentities(vesselGroup.vessels, vesselGroupVessels.entries), } } catch (e) { console.warn(e) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTable.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTable.tsx index 9112fd45c8..9d7b8711ef 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTable.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTable.tsx @@ -18,11 +18,11 @@ import { selectVGRVesselsOrderDirection, selectVGRVesselsOrderProperty, } from 'features/reports/vessel-groups/vessel-group.config.selectors' -import { selectVGRVessels } from 'features/reports/vessel-groups/vessel-group-report.slice' import { VGRVesselsOrderProperty, VGRVesselsOrderDirection, } from 'features/vessel-groups/vessel-groups.types' +import { getSearchIdentityResolved } from 'features/vessel/vessel.utils' import styles from './VesselGroupReportVesselsTable.module.css' import { selectVGRVesselsPaginated } from './vessel-group-report-vessels.selectors' import VesselGroupReportVesselsTableFooter from './VesselGroupReportVesselsTableFooter' @@ -30,7 +30,6 @@ import VesselGroupReportVesselsTableFooter from './VesselGroupReportVesselsTable export default function VesselGroupReportVesselsTable() { const { t } = useTranslation() const { dispatchQueryParams } = useLocationConnect() - const vesselsRaw = useSelector(selectVGRVessels) const vessels = useSelector(selectVGRVesselsPaginated) const userData = useSelector(selectUserData) const dataviews = useSelector(selectActiveReportDataviews) @@ -107,7 +106,8 @@ export default function VesselGroupReportVesselsTable() { />
    {vessels?.map((vessel, i) => { - const { id, shipName, flag, flagTranslatedClean, flagTranslated, mmsi, index } = vessel + const { shipName, flagTranslated, flagTranslatedClean, identity } = vessel + const { id, flag, ssvid } = getSearchIdentityResolved(identity!) const isLastRow = i === vessels.length - 1 const flagInteractionEnabled = !EMPTY_API_VALUES.includes(flagTranslated) const type = vessel.vesselType @@ -117,7 +117,7 @@ export default function VesselGroupReportVesselsTable() {
    @@ -132,7 +132,7 @@ export default function VesselGroupReportVesselsTable() { )}
    - {mmsi || EMPTY_FIELD_PLACEHOLDER} + {ssvid || EMPTY_FIELD_PLACEHOLDER}
    { return source } +export type VesselGroupVesselTableParsed = VesselGroupVesselIdentity & VesselGroupReportVesselParsed + export const selectVGRVesselsParsed = createSelector([selectVGRVessels], (vessels) => { - if (!vessels?.length) return null - return vessels.map((vessel, index) => { - const { ssvid, ...vesselData } = getSearchIdentityResolved(vessel) - const source = getVesselSource(vessel) + if (!vessels?.length) { + return + } + return getVesselGroupUniqVessels(vessels).flatMap((vessel) => { + if (!vessel.identity) { + return [] + } + const { shipname, flag, ...vesselData } = getSearchIdentityResolved(vessel.identity!) + const source = getVesselSource(vessel.identity) return { - ...vesselData, - index: index, - shipName: formatInfoField(vesselData.shipname, 'name'), + ...vessel, + shipName: formatInfoField(shipname, 'name') as string, vesselType: getVesselShipTypeLabel(vesselData), geartype: getVesselGearTypeLabel(vesselData), - flagTranslated: t(`flags:${vesselData.flag as string}` as any), - flagTranslatedClean: cleanFlagState(t(`flags:${vesselData.flag as string}` as any)), + flagTranslated: t(`flags:${flag as string}` as any), + flagTranslatedClean: cleanFlagState(t(`flags:${flag as string}` as any)), source: t(`common.sourceOptions.${source}`, source), - mmsi: ssvid, - dataset: vessel.dataset, - } - }) as VesselGroupReportVesselParsed[] + } as VesselGroupVesselTableParsed + }) }) type ReportFilterProperty = FilterProperty | 'source' @@ -67,11 +73,12 @@ export const selectVGRVesselsTimeRange = createSelector([selectVGRVesselsParsed] let start: string = '' let end: string = '' vessels.forEach((vessel) => { - if (!start || vessel.transmissionDateFrom < start) { - start = vessel.transmissionDateFrom + const { transmissionDateFrom, transmissionDateTo } = getSearchIdentityResolved(vessel.identity!) + if (!start || transmissionDateFrom < start) { + start = transmissionDateFrom } - if (!end || vessel.transmissionDateTo > end) { - end = vessel.transmissionDateTo + if (!end || transmissionDateTo > end) { + end = transmissionDateTo } }) return { start, end } @@ -92,7 +99,7 @@ export const selectVGRVesselsFiltered = createSelector( [selectVGRVesselsParsed, selectVGRVesselFilter], (vessels, filter) => { if (!vessels?.length) return null - return getVesselsFiltered( + return getVesselsFiltered( vessels, filter, REPORT_FILTER_PROPERTIES diff --git a/apps/fishing-map/features/user/UserVesselGroups.tsx b/apps/fishing-map/features/user/UserVesselGroups.tsx index 2f64bceb86..7dda3da769 100644 --- a/apps/fishing-map/features/user/UserVesselGroups.tsx +++ b/apps/fishing-map/features/user/UserVesselGroups.tsx @@ -13,6 +13,7 @@ import { useAppDispatch } from 'features/app/app.hooks' import { selectDatasetsStatus } from 'features/datasets/datasets.slice' import { getVesselGroupLabel, + getVesselGroupVesselsCount, isOutdatedVesselGroup, } from 'features/vessel-groups/vessel-groups.utils' import { sortByCreationDate } from 'utils/dates' @@ -88,13 +89,17 @@ function UserVesselGroups() { {isOutdated ? ( {getVesselGroupLabel(vesselGroup)}{' '} - ({vesselGroup.vessels.length}) + + ({getVesselGroupVesselsCount(vesselGroup)}) + ) : ( {getVesselGroupLabel(vesselGroup)}{' '} - ({vesselGroup.vessels.length}) + + ({getVesselGroupVesselsCount(vesselGroup)}) + diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx index 9ff1e1f0a9..44b1b8bd73 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx @@ -62,6 +62,7 @@ import { setVesselGroupSearchId, setVesselGroupSearchVessels, } from './vessel-groups-modal.slice' +import { getVesselGroupUniqVessels } from './vessel-groups.utils' function VesselGroupModal(): React.ReactElement { const { t } = useTranslation() @@ -184,12 +185,7 @@ function VesselGroupModal(): React.ReactElement { { addToDataviews = true, removeVessels = false, navigateToWorkspace = false } = {} ) => { setButtonLoading(navigateToWorkspace ? 'saveAndSeeInWorkspace' : 'save') - const vessels: VesselGroupVessel[] = vesselGroupSearchVessels.map((vessel) => { - return { - vesselId: vessel.id, - dataset: vessel.dataset as string, - } - }) + const vessels: VesselGroupVessel[] = getVesselGroupUniqVessels(vesselGroupSearchVessels) let dispatchedAction if (editingVesselGroupId) { const vesselGroup: UpdateVesselGroupThunkParams = { diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx index 4e61517bf2..241cccf8a2 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalSearch.tsx @@ -28,7 +28,9 @@ function VesselGroupSearch({ onError }: { onError: (string: any) => void }) { useEffect(() => { if (debouncedSearchText) { const vesselIds = debouncedSearchText?.split(/[\s|,]+/).filter(Boolean) - dispatch(setVesselGroupVessels(vesselIds.map((v) => ({ vesselId: v, dataset: '' })))) + dispatch( + setVesselGroupVessels(vesselIds.map((v) => ({ vesselId: v, dataset: '', relationId: '' }))) + ) } else { dispatch(setVesselGroupVessels(null)) } diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx index c54e265589..b99eb9d455 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx @@ -1,7 +1,6 @@ -import { Fragment, useCallback } from 'react' +import { useCallback } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { groupBy } from 'es-toolkit' import { ActionCreatorWithPayload } from '@reduxjs/toolkit' import { IconButton, Tooltip, TransmissionsTimeline } from '@globalfishingwatch/ui-components' import { Locale } from '@globalfishingwatch/api-types' @@ -9,11 +8,11 @@ import { EMPTY_FIELD_PLACEHOLDER, formatInfoField, getVesselGearTypeLabel } from import { FIRST_YEAR_OF_DATA } from 'data/config' import I18nDate from 'features/i18n/i18nDate' import { useAppDispatch } from 'features/app/app.hooks' -import { isFieldLoginRequired } from 'features/vessel/vessel.utils' -import { VesselDataIdentity } from 'features/vessel/vessel.slice' +import { getSearchIdentityResolved, isFieldLoginRequired } from 'features/vessel/vessel.utils' import VesselIdentityFieldLogin from 'features/vessel/identity/VesselIdentityFieldLogin' import styles from './VesselGroupModal.module.css' import { + VesselGroupVesselIdentity, selectNewVesselGroupSearchVessels, selectVesselGroupSearchVessels, setNewVesselGroupSearchVessels, @@ -21,9 +20,9 @@ import { } from './vessel-groups-modal.slice' type VesselGroupVesselRowProps = { - vessel: VesselDataIdentity + vessel: VesselGroupVesselIdentity className?: string - onRemoveClick: (vessel: VesselDataIdentity) => void + onRemoveClick: (vessel: VesselGroupVesselIdentity) => void } function VesselGroupVesselRow({ vessel, @@ -31,10 +30,10 @@ function VesselGroupVesselRow({ className = '', }: VesselGroupVesselRowProps) { const { t, i18n } = useTranslation() - const { shipname, flag, ssvid, transmissionDateFrom, transmissionDateTo } = - vessel || ({} as VesselDataIdentity) + const { shipname, flag, ssvid, transmissionDateFrom, transmissionDateTo, geartypes } = + getSearchIdentityResolved(vessel.identity!) const vesselName = formatInfoField(shipname, 'name') - const vesselGearType = getVesselGearTypeLabel(vessel) + const vesselGearType = getVesselGearTypeLabel({ geartypes }) return ( @@ -48,7 +47,7 @@ function VesselGroupVesselRow({ {transmissionDateFrom && transmissionDateTo && ( - // TODO tooltip not working + // TODO:VV3 tooltip not working @@ -83,44 +82,20 @@ function VesselGroupVesselRow({ ) } -type VesselGroupDataIdentity = VesselDataIdentity & { dataset: string } -function groupSearchVesselsIdentityBy( - vessels: VesselDataIdentity[] | null, - groupByKey: 'id' | 'ssvid' -) { - if (!vessels?.length) { - return {} - } - return groupBy(vessels, (v) => v[groupByKey]) -} - function VesselGroupVessels() { const { t } = useTranslation() + const dispatch = useAppDispatch() const vesselGroupSearchVessels = useSelector(selectVesselGroupSearchVessels) const newVesselGroupSearchVessels = useSelector(selectNewVesselGroupSearchVessels) - const groupByKey = [ - ...(vesselGroupSearchVessels || []), - ...(newVesselGroupSearchVessels || []), - ].some((vessel) => { - const ssvid = vessel.ssvid - return ssvid !== undefined && ssvid !== '' - }) - ? 'ssvid' - : 'id' - const searchVesselsGrouped = groupSearchVesselsIdentityBy(vesselGroupSearchVessels, groupByKey) - const newSearchVesselsGrouped = groupSearchVesselsIdentityBy( - newVesselGroupSearchVessels, - groupByKey - ) - const dispatch = useAppDispatch() - const onVesselRemoveClick = useCallback( - (vessel: VesselGroupDataIdentity, list: 'search' | 'new' = 'search') => { + (vessel: VesselGroupVesselIdentity, list: 'search' | 'new' = 'search') => { const vessels = list === 'search' ? vesselGroupSearchVessels : newVesselGroupSearchVessels const action = ( list === 'search' ? setVesselGroupSearchVessels : setNewVesselGroupSearchVessels - ) as ActionCreatorWithPayload - const index = vessels!.findIndex((v) => v.id === vessel?.id && v.dataset === vessel.dataset) + ) as ActionCreatorWithPayload + const index = vessels!.findIndex( + (v) => v.vesselId === vessel?.vesselId && v.dataset === vessel.dataset + ) if (index > -1) { dispatch(action([...vessels!.slice(0, index), ...vessels!.slice(index + 1)])) } @@ -131,6 +106,7 @@ function VesselGroupVessels() { if (!vesselGroupSearchVessels?.length && !newVesselGroupSearchVessels?.length) { return null } + return ( @@ -144,44 +120,32 @@ function VesselGroupVessels() { - {Object.keys(newSearchVesselsGrouped)?.length > 0 && - Object.keys(newSearchVesselsGrouped).map((key) => { - if (!key || key === 'undefined') { - return null - } - const vessels = newSearchVesselsGrouped[key] - return vessels.map((vessel) => ( - - onVesselRemoveClick(vessel as VesselGroupDataIdentity, 'new') - } - /> - )) - })} - {Object.keys(searchVesselsGrouped)?.length > 0 && - Object.keys(searchVesselsGrouped).map((key) => { - if (newSearchVesselsGrouped[key]) { - return null - } - const vessels = searchVesselsGrouped[key] - return ( - - {vessels.map((vessel, i) => ( - - onVesselRemoveClick(vessel as VesselGroupDataIdentity) - } - className={i === vessels.length - 1 ? styles.border : ''} - /> - ))} - - ) - })} + {newVesselGroupSearchVessels?.map((vessel) => { + if (!vessel.identity) { + return null + } + return ( + onVesselRemoveClick(vessel, 'new')} + /> + ) + })} + {vesselGroupSearchVessels?.map((vessel) => { + if (!vessel.identity) { + return null + } + return ( + onVesselRemoveClick(vessel, 'new')} + /> + ) + })}
    ) diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index 75f4c621ec..0cb5b17679 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -10,15 +10,15 @@ import { IdentityVessel, VesselGroup, VesselGroupVessel, - VesselIdentitySourceEnum, } from '@globalfishingwatch/api-types' import { GFWAPI, parseAPIError, ParsedAPIError } from '@globalfishingwatch/api-client' import { resolveEndpoint } from '@globalfishingwatch/datasets-client' import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' import { AsyncReducerStatus } from 'utils/async-slice' -import { getVesselId, getVesselIdentities } from 'features/vessel/vessel.utils' -import { VesselDataIdentity } from 'features/vessel/vessel.slice' +import { getVesselId } from 'features/vessel/vessel.utils' +import { INCLUDES_RELATED_SELF_REPORTED_INFO_ID } from 'features/vessel/vessel.config' import { fetchDatasetByIdThunk, selectDatasetById } from '../datasets/datasets.slice' +import { mergeVesselGroupVesselIdentities } from './vessel-groups.utils' export const MAX_VESSEL_GROUP_VESSELS = 1000 @@ -29,6 +29,8 @@ export type VesselGroupConfirmationMode = | 'saveAndSeeInWorkspace' | 'saveAndDeleteVessels' +export type VesselGroupVesselIdentity = VesselGroupVessel & { identity?: IdentityVessel } + interface VesselGroupModalState { isModalOpen: boolean vesselGroupEditId: string | null @@ -38,9 +40,9 @@ interface VesselGroupModalState { id: IdField status: AsyncReducerStatus error: ParsedAPIError | null - vessels: VesselDataIdentity[] | null + vessels: VesselGroupVesselIdentity[] | null } - newSearchVessels: VesselDataIdentity[] | null + newSearchVessels: VesselGroupVesselIdentity[] | null } type SearchVesselsBody = { datasets: string[]; where?: string; ids?: string[] } @@ -96,14 +98,6 @@ const initialState: VesselGroupModalState = { newSearchVessels: null, } -export function getVesselsGroupIdentities(vessels: IdentityVessel[]): VesselDataIdentity[] { - return vessels.flatMap((vessel) => { - return getVesselIdentities(vessel, { - identitySource: VesselIdentitySourceEnum.SelfReported, - }) - }) -} - export const searchVesselGroupsVesselsThunk = createAsyncThunk( 'vessel-groups/searchVessels', async ( @@ -182,7 +176,7 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( ) }) : uniqSearchResults - return getVesselsGroupIdentities(searchResultsFiltered) + return mergeVesselGroupVesselIdentities(vessels, searchResultsFiltered) } catch (e: any) { console.warn(e) return rejectWithValue(parseAPIError(e)) @@ -236,7 +230,7 @@ export const getVesselInVesselGroupThunk = createAsyncThunk( }, { id: 'includes', - value: ['POTENTIAL_RELATED_SELF_REPORTED_INFO'], + value: [INCLUDES_RELATED_SELF_REPORTED_INFO_ID], }, ], } @@ -249,11 +243,16 @@ export const getVesselInVesselGroupThunk = createAsyncThunk( message: 'Missing search url', }) } - const vessels = await GFWAPI.fetch>(url, { + const vesselsIdentities = await GFWAPI.fetch>(url, { signal, cache: 'reload', }) - return getVesselsGroupIdentities(vessels.entries) + const vesselGroupVessels = mergeVesselGroupVesselIdentities( + vesselGroup.vessels, + vesselsIdentities.entries + ) + console.log('🚀 ~ vesselGroupVessels:', vesselGroupVessels) + return vesselGroupVessels } catch (e: any) { console.warn(e) return rejectWithValue(parseAPIError(e)) @@ -288,10 +287,13 @@ export const vesselGroupModalSlice = createSlice({ resetVesselGroupModalStatus: (state) => { state.search.status = AsyncReducerStatus.Idle }, - setVesselGroupSearchVessels: (state, action: PayloadAction) => { + setVesselGroupSearchVessels: ( + state, + action: PayloadAction + ) => { state.search.vessels = action.payload }, - setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { + setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { state.newSearchVessels = action.payload }, setVesselGroupVessels: (state, action: PayloadAction) => { diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts index 708352b97c..7363bd3ea4 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts @@ -62,6 +62,8 @@ export const useVesselGroupsUpdate = () => { return { vesselId: id, dataset: dataset, + // TODO:VV3 insert vessels with its relationships + relationId: '', } }), } diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts index 2de516a38e..aac19df367 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts @@ -182,7 +182,9 @@ export const updateVesselGroupVesselsThunk = createAsyncThunk( return dispatch( updateVesselGroupThunk({ id: vesselGroup.id, - vessels: override ? vessels : [...vesselGroup.vessels, ...vessels], + vessels: override + ? vessels + : uniqBy([...vesselGroup.vessels, ...vessels], (v) => v.vesselId), }) ) } diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts index 6f5df32990..c6f5f20eb8 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts @@ -1,6 +1,14 @@ -import { VesselGroup } from '@globalfishingwatch/api-types' +import { uniq, uniqBy } from 'es-toolkit' +import { + IdentityVessel, + VesselGroup, + VesselGroupVessel, + VesselIdentitySourceEnum, +} from '@globalfishingwatch/api-types' import { PUBLIC_SUFIX } from 'data/config' +import { getVesselId, getVesselIdentities, getVesselProperty } from 'features/vessel/vessel.utils' import { VESSEL_GROUPS_REPORT_RELEASE_DATE } from './vessel-groups.config' +import { VesselGroupVesselIdentity } from './vessel-groups-modal.slice' export const getVesselGroupLabel = (vesselGroup: VesselGroup) => { const isPrivate = !vesselGroup.id.endsWith(`-${PUBLIC_SUFIX}`) @@ -10,3 +18,63 @@ export const getVesselGroupLabel = (vesselGroup: VesselGroup) => { export const isOutdatedVesselGroup = (vesselGroup: VesselGroup) => { return vesselGroup?.updatedAt && vesselGroup?.updatedAt < VESSEL_GROUPS_REPORT_RELEASE_DATE } + +export const getVesselGroupVesselsCount = (vesselGroup: VesselGroup) => { + return uniq(vesselGroup.vessels.map((v) => v.relationId || v.vesselId)).filter(Boolean).length +} + +export const getVesselGroupUniqVessels = ( + vessels: VesselGroupVesselIdentity[] +): VesselGroupVesselIdentity[] => { + return uniqBy( + vessels.flatMap((vessel) => { + const identities = getVesselIdentities(vessel.identity!, { + identitySource: VesselIdentitySourceEnum.SelfReported, + }) + if (!identities.length) { + return [] + } + return identities.map((identity) => ({ + vesselId: identity.id, + dataset: identity.dataset as string, + relationId: vessel.relationId as string, + identity: (vessel.relationId === identity.id + ? vessel.identity + : undefined) as IdentityVessel, + })) + }), + (v) => v.vesselId + ) +} + +export const mergeVesselGroupVesselIdentities = ( + vesselGroupVessels: VesselGroupVessel[], + vesselIdentities: IdentityVessel[] +): VesselGroupVesselIdentity[] => { + return vesselGroupVessels + .flatMap((v) => { + const vesselIdentity = vesselIdentities.find((vesselIdentity) => { + const selftReportedIds = getVesselIdentities(vesselIdentity, { + identitySource: VesselIdentitySourceEnum.SelfReported, + })?.map((v) => v.id) + return selftReportedIds?.includes(v.vesselId) + }) + if (!vesselIdentity) { + return [] + } + const relationId = getVesselId(vesselIdentity) + return { + ...v, + relationId, + identity: (relationId === v.vesselId ? vesselIdentity : undefined) as IdentityVessel, + } + }) + .toSorted((a, b) => { + const aValue = getVesselProperty(a.identity!, 'shipname') + const bValue = getVesselProperty(b.identity!, 'shipname') + if (aValue === bValue) { + return 0 + } + return aValue > bValue ? 1 : -1 + }) +} diff --git a/apps/fishing-map/features/vessel/vessel.config.ts b/apps/fishing-map/features/vessel/vessel.config.ts index 45ce2cf80b..f0940510c9 100644 --- a/apps/fishing-map/features/vessel/vessel.config.ts +++ b/apps/fishing-map/features/vessel/vessel.config.ts @@ -7,6 +7,7 @@ import { VesselProfileState } from './vessel.types' export const DEFAULT_VESSEL_IDENTITY_DATASET = 'public-global-vessel-identity' export const DEFAULT_VESSEL_IDENTITY_VERSION = 'v3.0' export const DEFAULT_VESSEL_IDENTITY_ID = `${DEFAULT_VESSEL_IDENTITY_DATASET}:${DEFAULT_VESSEL_IDENTITY_VERSION}` +export const INCLUDES_RELATED_SELF_REPORTED_INFO_ID = 'POTENTIAL_RELATED_SELF_REPORTED_INFO' export const CACHE_FALSE_PARAM = { id: 'cache', value: 'false' } export const DEFAULT_VESSEL_STATE: VesselProfileState = { diff --git a/apps/fishing-map/features/vessel/vessel.utils.ts b/apps/fishing-map/features/vessel/vessel.utils.ts index 71a6ff9f20..1ae5946751 100644 --- a/apps/fishing-map/features/vessel/vessel.utils.ts +++ b/apps/fishing-map/features/vessel/vessel.utils.ts @@ -53,20 +53,16 @@ export const getVesselIdentities = ( return [] as VesselDataIdentity[] } - if ((vessel as IdentityVesselData).identities) { - return (vessel as IdentityVesselData).identities.sort((a, b) => - a.transmissionDateTo > b.transmissionDateTo ? -1 : 1 - ) - } - - const identities = [ - ...getVesselIdentitiesBySource(vessel as IdentityVessel, { - identitySource: VesselIdentitySourceEnum.Registry, - }), - ...getVesselIdentitiesBySource(vessel as IdentityVessel, { - identitySource: VesselIdentitySourceEnum.SelfReported, - }), - ].sort((a, b) => (a.transmissionDateTo > b.transmissionDateTo ? -1 : 1)) + const identities = (vessel as IdentityVesselData).identities?.length + ? (vessel as IdentityVesselData).identities + : [ + ...getVesselIdentitiesBySource(vessel as IdentityVessel, { + identitySource: VesselIdentitySourceEnum.Registry, + }), + ...getVesselIdentitiesBySource(vessel as IdentityVessel, { + identitySource: VesselIdentitySourceEnum.SelfReported, + }), + ].sort((a, b) => (a.transmissionDateTo > b.transmissionDateTo ? -1 : 1)) return identitySource ? identities.filter((i) => i.identitySource === identitySource) : identities } diff --git a/libs/api-types/src/vesselGroups.ts b/libs/api-types/src/vesselGroups.ts index 979f1dadaa..36a62890c8 100644 --- a/libs/api-types/src/vesselGroups.ts +++ b/libs/api-types/src/vesselGroups.ts @@ -1,6 +1,7 @@ export interface VesselGroupVessel { dataset: string vesselId: string + relationId: string flag?: string vesselType?: string } From be0e1c541324dedab6b2131168d516411da352d3 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 27 Sep 2024 10:10:36 +0200 Subject: [PATCH 09/14] fix vessel group update and delete --- .../vessel-groups/VesselGroupModalVessels.tsx | 18 +++++++++--------- .../vessel-groups/vessel-groups.slice.ts | 16 ++++------------ .../vessel-groups/vessel-groups.utils.ts | 10 ++++++++++ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx index b99eb9d455..fbcfab251a 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx @@ -90,14 +90,14 @@ function VesselGroupVessels() { const onVesselRemoveClick = useCallback( (vessel: VesselGroupVesselIdentity, list: 'search' | 'new' = 'search') => { const vessels = list === 'search' ? vesselGroupSearchVessels : newVesselGroupSearchVessels - const action = ( - list === 'search' ? setVesselGroupSearchVessels : setNewVesselGroupSearchVessels - ) as ActionCreatorWithPayload - const index = vessels!.findIndex( - (v) => v.vesselId === vessel?.vesselId && v.dataset === vessel.dataset - ) - if (index > -1) { - dispatch(action([...vessels!.slice(0, index), ...vessels!.slice(index + 1)])) + if (vessels) { + const action = ( + list === 'search' ? setVesselGroupSearchVessels : setNewVesselGroupSearchVessels + ) as ActionCreatorWithPayload + const filteredVessels = vessels.filter( + (v) => v.vesselId !== vessel.vesselId && v.relationId !== vessel.vesselId + ) + dispatch(action(filteredVessels)) } }, [dispatch, newVesselGroupSearchVessels, vesselGroupSearchVessels] @@ -142,7 +142,7 @@ function VesselGroupVessels() { key={`${vessel?.vesselId}-${vessel.dataset}`} className={styles.new} vessel={vessel} - onRemoveClick={(vessel) => onVesselRemoveClick(vessel, 'new')} + onRemoveClick={(vessel) => onVesselRemoveClick(vessel, 'search')} /> ) })} diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts index aac19df367..1ad3392b0d 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.slice.ts @@ -2,12 +2,7 @@ import { createAsyncThunk, createSelector, PayloadAction } from '@reduxjs/toolki import { stringify } from 'qs' import { uniqBy } from 'es-toolkit' import memoize from 'lodash/memoize' -import { - APIPagination, - VesselGroup, - VesselGroupUpsert, - VesselGroupVessel, -} from '@globalfishingwatch/api-types' +import { APIPagination, VesselGroup, VesselGroupUpsert } from '@globalfishingwatch/api-types' import { GFWAPI, FetchOptions, parseAPIError, ParsedAPIError } from '@globalfishingwatch/api-client' import { AsyncError, @@ -18,6 +13,7 @@ import { } from 'utils/async-slice' import { DEFAULT_PAGINATION_PARAMS } from 'data/config' import { RootState } from 'store' +import { prepareVesselGroupVesselsUpdate } from './vessel-groups.utils' export type IdField = 'vesselId' | 'mmsi' export type VesselGroupConfirmationMode = 'save' | 'saveAndSeeInWorkspace' | 'saveAndDeleteVessels' @@ -100,10 +96,6 @@ export const fetchVesselGroupsThunk = createAsyncThunk< } ) -const removeDuplicatedVesselGroupvessels = (vessels: VesselGroupVessel[]) => { - return uniqBy(vessels, (vessel) => [vessel.vesselId, vessel.dataset].join(',')) -} - export const fetchVesselGroupByIdThunk = createAsyncThunk( 'vessel-groups/fetchById', async (vesselGroupId: string) => { @@ -119,7 +111,7 @@ export const createVesselGroupThunk = createAsyncThunk( async (vesselGroupCreate: VesselGroupUpsert, { dispatch, getState }) => { const vesselGroupUpsert: VesselGroupUpsert = { ...vesselGroupCreate, - vessels: removeDuplicatedVesselGroupvessels(vesselGroupCreate.vessels || []), + vessels: prepareVesselGroupVesselsUpdate(vesselGroupCreate.vessels || []), } const saveVesselGroup: any = async (vesselGroup: VesselGroupUpsert, tries = 0) => { let vesselGroupUpdated: VesselGroup @@ -155,7 +147,7 @@ export const updateVesselGroupThunk = createAsyncThunk( const { id, ...rest } = vesselGroupUpsert const vesselGroup: VesselGroupUpsert = { ...rest, - vessels: removeDuplicatedVesselGroupvessels(rest.vessels || []), + vessels: prepareVesselGroupVesselsUpdate(rest.vessels || []), } const vesselGroupUpdated = await GFWAPI.fetch(`/vessel-groups/${id}`, { method: 'PATCH', diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts index c6f5f20eb8..984716027f 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts @@ -23,6 +23,16 @@ export const getVesselGroupVesselsCount = (vesselGroup: VesselGroup) => { return uniq(vesselGroup.vessels.map((v) => v.relationId || v.vesselId)).filter(Boolean).length } +export const removeDuplicatedVesselGroupvessels = (vessels: VesselGroupVesselIdentity[]) => { + return uniqBy(vessels, (vessel) => [vessel.vesselId, vessel.dataset].join(',')) +} +export const removeVesselGroupvesselIdentity = (vessels: VesselGroupVesselIdentity[]) => { + return vessels.map(({ identity, ...vessel }) => vessel) +} +export const prepareVesselGroupVesselsUpdate = (vessels: VesselGroupVesselIdentity[]) => { + return removeDuplicatedVesselGroupvessels(removeVesselGroupvesselIdentity(vessels)) +} + export const getVesselGroupUniqVessels = ( vessels: VesselGroupVesselIdentity[] ): VesselGroupVesselIdentity[] => { From 9b4c243fdaf31ba7509d08aca750d5fa81166577 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 27 Sep 2024 12:59:35 +0200 Subject: [PATCH 10/14] simplify vessel-group-modal.slice and fix creation --- .../vessel-groups/VesselGroupReportTitle.tsx | 4 +- .../vessel-groups/VesselGroupModal.tsx | 46 ++++---- .../vessel-groups/VesselGroupModalSearch.tsx | 27 ++--- .../vessel-groups/VesselGroupModalVessels.tsx | 46 +++----- .../vessel-groups-modal.slice.ts | 107 +++++++----------- .../vessel-groups/vessel-groups.hooks.ts | 14 ++- .../vessel-groups/vessel-groups.selectors.ts | 20 +--- .../vessel-groups/vessel-groups.utils.ts | 34 +++++- .../vessel-groups/VesselGroupsLayerPanel.tsx | 5 +- 9 files changed, 146 insertions(+), 157 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx index a3a4482e1a..3d2a6ebf2c 100644 --- a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx @@ -8,8 +8,8 @@ import { useAppDispatch } from 'features/app/app.hooks' import ReportTitlePlaceholder from 'features/reports/areas/placeholders/ReportTitlePlaceholder' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { - setNewVesselGroupSearchVessels, setVesselGroupEditId, + setVesselGroupModalVessels, setVesselGroupsModalOpen, } from 'features/vessel-groups/vessel-groups-modal.slice' import { formatInfoField } from 'utils/info' @@ -35,7 +35,7 @@ export default function VesselGroupReportTitle({ vesselGroup, loading }: ReportT const onEditClick = useCallback(() => { if (vesselGroup?.id || !vesselGroup?.vessels?.length) { dispatch(setVesselGroupEditId(vesselGroup.id)) - dispatch(setNewVesselGroupSearchVessels(vesselGroup.vessels)) + dispatch(setVesselGroupModalVessels(vesselGroup.vessels)) dispatch(setVesselGroupsModalOpen(true)) } }, [dispatch, vesselGroup?.id, vesselGroup?.vessels]) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx index 44b1b8bd73..9053621f99 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModal.tsx @@ -16,7 +16,6 @@ import VesselGroupSearch from 'features/vessel-groups/VesselGroupModalSearch' import VesselGroupVessels from 'features/vessel-groups/VesselGroupModalVessels' import { useAppDispatch } from 'features/app/app.hooks' import { - selectAllVesselGroupSearchVessels, selectHasVesselGroupSearchVessels, selectHasVesselGroupVesselsOverflow, selectVesselGroupWorkspaceToNavigate, @@ -51,16 +50,17 @@ import { getVesselInVesselGroupThunk, MAX_VESSEL_GROUP_VESSELS, resetVesselGroupModal, - resetVesselGroupModalStatus, + resetVesselGroupModalSearchStatus, searchVesselGroupsVesselsThunk, selectVesselGroupConfirmationMode, selectVesselGroupEditId, selectVesselGroupModalOpen, - selectVesselGroupSearchId, + selectVesselGroupModalSearchIdField, + selectVesselGroupModalVessels, selectVesselGroupSearchStatus, - selectVesselGroupsVessels, - setVesselGroupSearchId, - setVesselGroupSearchVessels, + selectVesselGroupsModalSearchIds, + setVesselGroupModalVessels, + setVesselGroupSearchIdField, } from './vessel-groups-modal.slice' import { getVesselGroupUniqVessels } from './vessel-groups.utils' @@ -71,9 +71,9 @@ function VesselGroupModal(): React.ReactElement { const vesselDataviews = useSelector(selectVesselsDataviews) const isModalOpen = useSelector(selectVesselGroupModalOpen) const confirmationMode = useSelector(selectVesselGroupConfirmationMode) - const searchIdField = useSelector(selectVesselGroupSearchId) + const searchIdField = useSelector(selectVesselGroupModalSearchIdField) const editingVesselGroupId = useSelector(selectVesselGroupEditId) - const vesselGroupVessels = useSelector(selectVesselGroupsVessels) + const vesselGroupVesselsToSearch = useSelector(selectVesselGroupsModalSearchIds) const editingVesselGroup = useSelector(selectVesselGroupById(editingVesselGroupId as string)) const searchVesselStatus = useSelector(selectVesselGroupSearchStatus) const vesselGroupsStatus = useSelector(selectVesselGroupsStatus) @@ -92,20 +92,20 @@ function VesselGroupModal(): React.ReactElement { const [groupName, setGroupName] = useState(editingVesselGroup?.name || '') const [showBackButton, setShowBackButton] = useState(false) const [createAsPublic, setCreateAsPublic] = useState(true) - const vesselGroupSearchVessels = useSelector(selectAllVesselGroupSearchVessels) + const vesselGroupVessels = useSelector(selectVesselGroupModalVessels) const hasVesselsOverflow = useSelector(selectHasVesselGroupVesselsOverflow) const hasVesselGroupsVessels = useSelector(selectHasVesselGroupSearchVessels) const vesselGroupsInWorkspace = useSelector(selectWorkspaceVessselGroupsIds) const { upsertDataviewInstance } = useDataviewInstancesConnect() const searchVesselGroupsVesselsRef = useRef() - const searchVesselGroupsVesselsAllowed = vesselGroupVessels - ? vesselGroupVessels?.length < MAX_VESSEL_GROUP_VESSELS + const searchVesselGroupsVesselsAllowed = vesselGroupVesselsToSearch + ? vesselGroupVesselsToSearch?.length < MAX_VESSEL_GROUP_VESSELS : true const dispatchSearchVesselsGroupsThunk = useCallback( - async (vessels: VesselGroupVessel[], idField: IdField = 'vesselId') => { + async (ids: string[], idField: IdField = 'vesselId') => { searchVesselGroupsVesselsRef.current = dispatch( - searchVesselGroupsVesselsThunk({ vessels, idField }) + searchVesselGroupsVesselsThunk({ ids, idField }) ) const action = await searchVesselGroupsVesselsRef.current if (searchVesselGroupsVesselsThunk.fulfilled.match(action)) { @@ -129,7 +129,7 @@ function VesselGroupModal(): React.ReactElement { const onIdFieldChange = useCallback( (option: SelectOption) => { - dispatch(setVesselGroupSearchId(option.id)) + dispatch(setVesselGroupSearchIdField(option.id)) }, [dispatch] ) @@ -160,8 +160,8 @@ function VesselGroupModal(): React.ReactElement { if (confirmed) { if (action === 'back') { setError('') - dispatch(setVesselGroupSearchVessels(null)) - dispatch(resetVesselGroupModalStatus()) + dispatch(setVesselGroupModalVessels(null)) + dispatch(resetVesselGroupModalSearchStatus()) abortSearch() setShowBackButton(false) } else { @@ -174,10 +174,10 @@ function VesselGroupModal(): React.ReactElement { const onSearchVesselsClick = useCallback(async () => { setShowBackButton(true) - if (vesselGroupVessels) { - dispatchSearchVesselsGroupsThunk(vesselGroupVessels, searchIdField) + if (vesselGroupVesselsToSearch) { + dispatchSearchVesselsGroupsThunk(vesselGroupVesselsToSearch, searchIdField) } - }, [dispatchSearchVesselsGroupsThunk, vesselGroupVessels, searchIdField]) + }, [dispatchSearchVesselsGroupsThunk, vesselGroupVesselsToSearch, searchIdField]) const onCreateGroupClick = useCallback( async ( @@ -185,7 +185,7 @@ function VesselGroupModal(): React.ReactElement { { addToDataviews = true, removeVessels = false, navigateToWorkspace = false } = {} ) => { setButtonLoading(navigateToWorkspace ? 'saveAndSeeInWorkspace' : 'save') - const vessels: VesselGroupVessel[] = getVesselGroupUniqVessels(vesselGroupSearchVessels) + const vessels: VesselGroupVessel[] = getVesselGroupUniqVessels(vesselGroupVessels) let dispatchedAction if (editingVesselGroupId) { const vesselGroup: UpdateVesselGroupThunkParams = { @@ -260,7 +260,7 @@ function VesselGroupModal(): React.ReactElement { }) }, [ - vesselGroupSearchVessels, + vesselGroupVessels, editingVesselGroupId, groupName, dispatch, @@ -339,9 +339,9 @@ function VesselGroupModal(): React.ReactElement {
    {!editingVesselGroup && (
    - {vesselGroupSearchVessels?.length > 0 && ( + {vesselGroupVessels && vesselGroupVessels?.length > 0 && ( )} void }) { const dispatch = useAppDispatch() const [searchText, setSearchText] = useState('') const debouncedSearchText = useDebounce(searchText, 200) - const searchIdField = useSelector(selectVesselGroupSearchId) - const vesselGroupVessels = useSelector(selectVesselGroupsVessels) - const hasGroupVessels = vesselGroupVessels && vesselGroupVessels.length > 0 + const searchIdField = useSelector(selectVesselGroupModalSearchIdField) + const vesselGroupVesselsToSearch = useSelector(selectVesselGroupsModalSearchIds) + const hasGroupVesselsToSearch = + vesselGroupVesselsToSearch && vesselGroupVesselsToSearch.length > 0 useEffect(() => { if (debouncedSearchText) { const vesselIds = debouncedSearchText?.split(/[\s|,]+/).filter(Boolean) - dispatch( - setVesselGroupVessels(vesselIds.map((v) => ({ vesselId: v, dataset: '', relationId: '' }))) - ) + dispatch(setVesselGroupModalSearchIds(vesselIds)) } else { - dispatch(setVesselGroupVessels(null)) + dispatch(setVesselGroupModalSearchIds(null)) } }, [dispatch, debouncedSearchText]) @@ -56,7 +55,7 @@ function VesselGroupSearch({ onError }: { onError: (string: any) => void }) { const presetColumn = ID_COLUMN_LOOKUP[i] foundIdColumn = columns.find((c) => c.toLowerCase() === presetColumn) if (foundIdColumn) { - dispatch(setVesselGroupSearchId(presetColumn)) + dispatch(setVesselGroupSearchIdField(presetColumn)) break } } @@ -88,7 +87,9 @@ function VesselGroupSearch({ onError }: { onError: (string: any) => void }) { className={styles.idsArea} value={searchText} label={ - hasGroupVessels ? `${searchIdField} (${vesselGroupVessels?.length})` : searchIdField + hasGroupVesselsToSearch + ? `${searchIdField} (${vesselGroupVesselsToSearch?.length})` + : searchIdField } placeholder={t('vesselGroup.idsPlaceholder', { field: searchIdField, diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx index fbcfab251a..255d4383bf 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupModalVessels.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { ActionCreatorWithPayload } from '@reduxjs/toolkit' import { IconButton, Tooltip, TransmissionsTimeline } from '@globalfishingwatch/ui-components' import { Locale } from '@globalfishingwatch/api-types' import { EMPTY_FIELD_PLACEHOLDER, formatInfoField, getVesselGearTypeLabel } from 'utils/info' @@ -13,10 +12,8 @@ import VesselIdentityFieldLogin from 'features/vessel/identity/VesselIdentityFie import styles from './VesselGroupModal.module.css' import { VesselGroupVesselIdentity, - selectNewVesselGroupSearchVessels, - selectVesselGroupSearchVessels, - setNewVesselGroupSearchVessels, - setVesselGroupSearchVessels, + selectVesselGroupModalVessels, + setVesselGroupModalVessels, } from './vessel-groups-modal.slice' type VesselGroupVesselRowProps = { @@ -40,7 +37,7 @@ function VesselGroupVesselRow({ {ssvid} {vesselName} - {t(`flags:${flag as string}` as any) || EMPTY_FIELD_PLACEHOLDER} + {flag ? t(`flags:${flag as string}` as any) : EMPTY_FIELD_PLACEHOLDER} {isFieldLoginRequired(vesselGearType) ? : vesselGearType} @@ -85,25 +82,21 @@ function VesselGroupVesselRow({ function VesselGroupVessels() { const { t } = useTranslation() const dispatch = useAppDispatch() - const vesselGroupSearchVessels = useSelector(selectVesselGroupSearchVessels) - const newVesselGroupSearchVessels = useSelector(selectNewVesselGroupSearchVessels) + const vesselGroupVessels = useSelector(selectVesselGroupModalVessels) + const onVesselRemoveClick = useCallback( - (vessel: VesselGroupVesselIdentity, list: 'search' | 'new' = 'search') => { - const vessels = list === 'search' ? vesselGroupSearchVessels : newVesselGroupSearchVessels - if (vessels) { - const action = ( - list === 'search' ? setVesselGroupSearchVessels : setNewVesselGroupSearchVessels - ) as ActionCreatorWithPayload - const filteredVessels = vessels.filter( + (vessel: VesselGroupVesselIdentity) => { + if (vesselGroupVessels) { + const filteredVessels = vesselGroupVessels.filter( (v) => v.vesselId !== vessel.vesselId && v.relationId !== vessel.vesselId ) - dispatch(action(filteredVessels)) + dispatch(setVesselGroupModalVessels(filteredVessels)) } }, - [dispatch, newVesselGroupSearchVessels, vesselGroupSearchVessels] + [dispatch, vesselGroupVessels] ) - if (!vesselGroupSearchVessels?.length && !newVesselGroupSearchVessels?.length) { + if (!vesselGroupVessels?.length) { return null } @@ -120,20 +113,7 @@ function VesselGroupVessels() { - {newVesselGroupSearchVessels?.map((vessel) => { - if (!vessel.identity) { - return null - } - return ( - onVesselRemoveClick(vessel, 'new')} - /> - ) - })} - {vesselGroupSearchVessels?.map((vessel) => { + {vesselGroupVessels?.map((vessel) => { if (!vessel.identity) { return null } @@ -142,7 +122,7 @@ function VesselGroupVessels() { key={`${vessel?.vesselId}-${vessel.dataset}`} className={styles.new} vessel={vessel} - onRemoveClick={(vessel) => onVesselRemoveClick(vessel, 'search')} + onRemoveClick={(vessel) => onVesselRemoveClick(vessel)} /> ) })} diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index 0cb5b17679..ebecfc4d54 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -1,5 +1,5 @@ import { createAsyncThunk, PayloadAction, createSlice } from '@reduxjs/toolkit' -import { uniq, uniqBy } from 'es-toolkit' +import { uniq } from 'es-toolkit' import { RootState } from 'reducers' import { APIPagination, @@ -15,10 +15,12 @@ import { GFWAPI, parseAPIError, ParsedAPIError } from '@globalfishingwatch/api-c import { resolveEndpoint } from '@globalfishingwatch/datasets-client' import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' import { AsyncReducerStatus } from 'utils/async-slice' -import { getVesselId } from 'features/vessel/vessel.utils' import { INCLUDES_RELATED_SELF_REPORTED_INFO_ID } from 'features/vessel/vessel.config' import { fetchDatasetByIdThunk, selectDatasetById } from '../datasets/datasets.slice' -import { mergeVesselGroupVesselIdentities } from './vessel-groups.utils' +import { + flatVesselGroupSearchVessels, + mergeVesselGroupVesselIdentities, +} from './vessel-groups.utils' export const MAX_VESSEL_GROUP_VESSELS = 1000 @@ -35,14 +37,13 @@ interface VesselGroupModalState { isModalOpen: boolean vesselGroupEditId: string | null confirmationMode: VesselGroupConfirmationMode - groupVessels: VesselGroupVessel[] | null + vessels: VesselGroupVesselIdentity[] | null search: { - id: IdField + idField: IdField + ids: string[] | null status: AsyncReducerStatus error: ParsedAPIError | null - vessels: VesselGroupVesselIdentity[] | null } - newSearchVessels: VesselGroupVesselIdentity[] | null } type SearchVesselsBody = { datasets: string[]; where?: string; ids?: string[] } @@ -88,38 +89,32 @@ const initialState: VesselGroupModalState = { isModalOpen: false, vesselGroupEditId: null, confirmationMode: 'save', - groupVessels: null, + vessels: null, search: { - id: 'mmsi', + idField: 'mmsi', + ids: null, status: AsyncReducerStatus.Idle, - vessels: null, error: null, }, - newSearchVessels: null, } export const searchVesselGroupsVesselsThunk = createAsyncThunk( 'vessel-groups/searchVessels', async ( - { vessels, idField }: { vessels: VesselGroupVessel[]; idField: IdField }, + { ids, idField }: { ids: string[]; idField: IdField }, { signal, rejectWithValue, getState } ) => { const state = getState() as any - const vesselGroupDatasets = uniq(vessels?.flatMap((v) => v.dataset || [])) - const allVesselDatasets = (selectVesselsDatasets(state) || []).filter( + const searchDatasets = (selectVesselsDatasets(state) || []).filter( (d) => d.status !== DatasetStatus.Deleted && d.configuration?.apiSupportedVersions?.includes('v3') /*&& d.alias?.some((alias) => alias.includes(':latest'))*/ ) - const searchDatasets = vesselGroupDatasets?.length - ? allVesselDatasets.filter((dataset) => vesselGroupDatasets.includes(dataset.id)) - : allVesselDatasets - if (searchDatasets?.length) { const dataset = searchDatasets[0] const datasets = searchDatasets.map((d) => d.id) - const uniqVesselIds = uniq(vessels.map(({ vesselId }) => vesselId)) + const uniqVesselIds = uniq(ids) const isVesselByIdSearch = idField === 'vesselId' const datasetConfig: DataviewDatasetConfig = { endpoint: isVesselByIdSearch ? EndpointId.VesselList : EndpointId.VesselSearch, @@ -138,6 +133,10 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( id: 'limit', value: SEARCH_PAGINATION, }) + datasetConfig.query?.push({ + id: 'includes', + value: [INCLUDES_RELATED_SELF_REPORTED_INFO_ID], + }) } try { const url = resolveEndpoint(dataset, datasetConfig) @@ -159,24 +158,9 @@ export const searchVesselGroupsVesselsThunk = createAsyncThunk( body: fetchBody, signal, }) - // API returns multiple instances of the same vessel with the same id and dataset - const uniqSearchResults = uniqBy(searchResults, (vessel) => - [getVesselId(vessel), vessel.dataset].join(',') - ) - // Searching could return same vessel id from different datasets so we need to choose the original one - const searchResultsFiltered = isVesselByIdSearch - ? uniqSearchResults.filter((vessel) => { - const vesselId = getVesselId(vessel) - return ( - vessels.find((v) => { - const isSameVesselid = v.vesselId === vesselId - const isSameDataset = v.dataset ? v.dataset === vessel.dataset : true - return isSameVesselid && isSameDataset - }) !== undefined - ) - }) - : uniqSearchResults - return mergeVesselGroupVesselIdentities(vessels, searchResultsFiltered) + + const vesselGroupVessels = flatVesselGroupSearchVessels(searchResults) + return vesselGroupVessels } catch (e: any) { console.warn(e) return rejectWithValue(parseAPIError(e)) @@ -281,23 +265,20 @@ export const vesselGroupModalSlice = createSlice({ setVesselGroupsModalOpen: (state, action: PayloadAction) => { state.isModalOpen = action.payload }, - setVesselGroupSearchId: (state, action: PayloadAction) => { - state.search.id = action.payload + setVesselGroupSearchIdField: (state, action: PayloadAction) => { + state.search.idField = action.payload }, - resetVesselGroupModalStatus: (state) => { + resetVesselGroupModalSearchStatus: (state) => { state.search.status = AsyncReducerStatus.Idle }, - setVesselGroupSearchVessels: ( + setVesselGroupModalVessels: ( state, action: PayloadAction ) => { - state.search.vessels = action.payload - }, - setNewVesselGroupSearchVessels: (state, action: PayloadAction) => { - state.newSearchVessels = action.payload + state.vessels = action.payload }, - setVesselGroupVessels: (state, action: PayloadAction) => { - state.groupVessels = action.payload + setVesselGroupModalSearchIds: (state, action: PayloadAction) => { + state.search.ids = action.payload }, setVesselGroupEditId: (state, action: PayloadAction) => { state.vesselGroupEditId = action.payload @@ -312,11 +293,11 @@ export const vesselGroupModalSlice = createSlice({ extraReducers(builder) { builder.addCase(searchVesselGroupsVesselsThunk.pending, (state) => { state.search.status = AsyncReducerStatus.Loading - state.search.vessels = null + state.vessels = null }) builder.addCase(searchVesselGroupsVesselsThunk.fulfilled, (state, action) => { state.search.status = AsyncReducerStatus.Finished - state.search.vessels = action.payload + state.vessels = action.payload }) builder.addCase(searchVesselGroupsVesselsThunk.rejected, (state, action) => { if (action.error.message === 'Aborted') { @@ -328,11 +309,11 @@ export const vesselGroupModalSlice = createSlice({ }) builder.addCase(getVesselInVesselGroupThunk.pending, (state) => { state.search.status = AsyncReducerStatus.Loading - state.search.vessels = null + state.vessels = null }) builder.addCase(getVesselInVesselGroupThunk.fulfilled, (state, action) => { state.search.status = AsyncReducerStatus.Finished - state.search.vessels = action.payload + state.vessels = action.payload }) builder.addCase(getVesselInVesselGroupThunk.rejected, (state, action) => { if (action.error.message === 'Aborted') { @@ -346,26 +327,24 @@ export const vesselGroupModalSlice = createSlice({ }) export const { - resetVesselGroupModal, - resetVesselGroupModalStatus, - setVesselGroupEditId, - setVesselGroupVessels, - setVesselGroupSearchId, setVesselGroupsModalOpen, - setVesselGroupSearchVessels, - setNewVesselGroupSearchVessels, + setVesselGroupSearchIdField, + resetVesselGroupModalSearchStatus, + setVesselGroupModalVessels, + setVesselGroupModalSearchIds, + setVesselGroupEditId, setVesselGroupConfirmationMode, + resetVesselGroupModal, } = vesselGroupModalSlice.actions export const selectVesselGroupModalOpen = (state: RootState) => state.vesselGroupModal.isModalOpen -export const selectVesselGroupSearchId = (state: RootState) => state.vesselGroupModal.search.id +export const selectVesselGroupModalSearchIdField = (state: RootState) => + state.vesselGroupModal.search.idField export const selectVesselGroupSearchStatus = (state: RootState) => state.vesselGroupModal.search.status -export const selectVesselGroupSearchVessels = (state: RootState) => - state.vesselGroupModal.search.vessels -export const selectVesselGroupsVessels = (state: RootState) => state.vesselGroupModal.groupVessels -export const selectNewVesselGroupSearchVessels = (state: RootState) => - state.vesselGroupModal.newSearchVessels +export const selectVesselGroupModalVessels = (state: RootState) => state.vesselGroupModal.vessels +export const selectVesselGroupsModalSearchIds = (state: RootState) => + state.vesselGroupModal.search.ids export const selectVesselGroupEditId = (state: RootState) => state.vesselGroupModal.vesselGroupEditId export const selectVesselGroupConfirmationMode = (state: RootState) => diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts index 7363bd3ea4..a42f043a13 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts @@ -17,7 +17,7 @@ import { updateVesselGroupVesselsThunk, } from './vessel-groups.slice' import { - setNewVesselGroupSearchVessels, + setVesselGroupModalVessels, setVesselGroupEditId, setVesselGroupsModalOpen, } from './vessel-groups-modal.slice' @@ -84,6 +84,7 @@ export const useVesselGroupsModal = () => { const dispatch = useAppDispatch() const createVesselGroupWithVessels = useCallback( async (vesselGroupId: string, vessels: AddVesselGroupVessel[]) => { + // TODO:VV3 user vessels const vesselsWithDataset = vessels.map((vessel) => ({ ...vessel, vesselId: @@ -96,8 +97,15 @@ export const useVesselGroupsModal = () => { if (vesselGroupId && vesselGroupId !== NEW_VESSEL_GROUP_ID) { dispatch(setVesselGroupEditId(vesselGroupId)) } - // TODO:VV3 remove this any - dispatch(setNewVesselGroupSearchVessels(vesselsWithDataset as any)) + const vesselGroupDataset = vesselsWithDataset.map((vessel) => { + return { + vesselId: vessel.vesselId, + dataset: vessel.dataset, + relationId: vessel.vesselId, + identity: vessel, + } + }) + // dispatch(setVesselGroupModalVessels(vesselGroupDataset)) dispatch(setVesselGroupsModalOpen(true)) } else { console.warn('No related activity datasets founds for', vesselsWithDataset) diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts index c961d00d83..9e8787ad3a 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.selectors.ts @@ -12,29 +12,19 @@ import { LastWorkspaceVisited } from 'features/workspace/workspace.slice' import { WORKSPACE } from 'routes/routes' import { DEFAULT_WORKSPACE_CATEGORY, DEFAULT_WORKSPACE_ID } from 'data/workspaces' import { getVesselGroupsInDataviews } from 'features/datasets/datasets.utils' -import { - selectNewVesselGroupSearchVessels, - selectVesselGroupSearchVessels, -} from './vessel-groups-modal.slice' - -export const selectAllVesselGroupSearchVessels = createSelector( - [selectVesselGroupSearchVessels, selectNewVesselGroupSearchVessels], - (vessels, newVessels) => { - return [...(newVessels || []), ...(vessels || [])] - } -) +import { selectVesselGroupModalVessels } from './vessel-groups-modal.slice' export const selectHasVesselGroupVesselsOverflow = createSelector( - [selectAllVesselGroupSearchVessels], + [selectVesselGroupModalVessels], (vessels = []) => { - return vessels.length > MAX_VESSEL_GROUP_VESSELS + return vessels !== null && vessels.length > MAX_VESSEL_GROUP_VESSELS } ) export const selectHasVesselGroupSearchVessels = createSelector( - [selectAllVesselGroupSearchVessels], + [selectVesselGroupModalVessels], (vessels = []) => { - return vessels.length > 0 + return vessels !== null && vessels.length > 0 } ) diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts index 984716027f..c1fe336586 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts @@ -34,8 +34,11 @@ export const prepareVesselGroupVesselsUpdate = (vessels: VesselGroupVesselIdenti } export const getVesselGroupUniqVessels = ( - vessels: VesselGroupVesselIdentity[] + vessels: VesselGroupVesselIdentity[] | null ): VesselGroupVesselIdentity[] => { + if (!vessels) { + return [] + } return uniqBy( vessels.flatMap((vessel) => { const identities = getVesselIdentities(vessel.identity!, { @@ -88,3 +91,32 @@ export const mergeVesselGroupVesselIdentities = ( return aValue > bValue ? 1 : -1 }) } + +export const flatVesselGroupSearchVessels = ( + vesselIdentities: IdentityVessel[] +): VesselGroupVesselIdentity[] => { + return vesselIdentities + .flatMap((vessel) => { + const identities = getVesselIdentities(vessel, { + identitySource: VesselIdentitySourceEnum.SelfReported, + }) + if (!identities?.length) { + return [] + } + const relationId = getVesselId(vessel) + return identities.map((i) => ({ + vesselId: i.id, + dataset: i.dataset as string, + relationId, + identity: (relationId === i.id ? vessel : undefined) as IdentityVessel, + })) + }) + .toSorted((a, b) => { + const aValue = getVesselProperty(a.identity!, 'shipname') + const bValue = getVesselProperty(b.identity!, 'shipname') + if (aValue === bValue) { + return 0 + } + return aValue > bValue ? 1 : -1 + }) +} diff --git a/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx b/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx index ecc43c3501..87671948e1 100644 --- a/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx +++ b/apps/fishing-map/features/workspace/vessel-groups/VesselGroupsLayerPanel.tsx @@ -13,7 +13,7 @@ import { formatInfoField } from 'utils/info' import VesselGroupReportLink from 'features/reports/vessel-groups/VesselGroupReportLink' import { useAppDispatch } from 'features/app/app.hooks' import { - setNewVesselGroupSearchVessels, + setVesselGroupModalVessels, setVesselGroupEditId, setVesselGroupsModalOpen, } from 'features/vessel-groups/vessel-groups-modal.slice' @@ -66,8 +66,7 @@ function VesselGroupLayerPanel({ const onEditClick = () => { if (vesselGroup && (vesselGroup?.id || !vesselGroup?.vessels?.length)) { dispatch(setVesselGroupEditId(vesselGroup.id)) - // TODO:VV3 remove this any - dispatch(setNewVesselGroupSearchVessels(vesselGroup.vessels as any)) + dispatch(setVesselGroupModalVessels(vesselGroup.vessels)) dispatch(setVesselGroupsModalOpen(true)) } } From f71c7e62ea77edc536ec94988a88882783c03349 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 27 Sep 2024 13:27:10 +0200 Subject: [PATCH 11/14] get uniq vessel group report vessels --- .../vessels/VesselGroupReportVessels.tsx | 4 ++-- .../vessels/vessel-group-report-vessels.selectors.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx index 71c38b18d3..fd907bd24c 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx @@ -3,8 +3,8 @@ import { DateTime } from 'luxon' import { useTranslation } from 'react-i18next' import parse from 'html-react-parser' import ReportVesselsFilter from 'features/reports/activity/vessels/ReportVesselsFilter' -import { selectVGRVessels } from 'features/reports/vessel-groups/vessel-group-report.slice' import { + selectVGRUniqVessels, selectVGRVesselsFlags, selectVGRVesselsGraphDataGrouped, selectVGRVesselsTimeRange, @@ -25,7 +25,7 @@ import styles from './VesselGroupReportVessels.module.css' function VesselGroupReportVessels() { const { t } = useTranslation() - const vessels = useSelector(selectVGRVessels) + const vessels = useSelector(selectVGRUniqVessels) const subsection = useSelector(selectVGRVesselsSubsection) const reportDataview = useSelector(selectVGRDataview) const timeRange = useSelector(selectVGRVesselsTimeRange) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index 6285da10eb..9018cfb6da 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -40,7 +40,14 @@ const getVesselSource = (vessel: IdentityVessel) => { export type VesselGroupVesselTableParsed = VesselGroupVesselIdentity & VesselGroupReportVesselParsed -export const selectVGRVesselsParsed = createSelector([selectVGRVessels], (vessels) => { +export const selectVGRUniqVessels = createSelector([selectVGRVessels], (vessels) => { + if (!vessels?.length) { + return + } + return getVesselGroupUniqVessels(vessels).filter((v) => v.identity) +}) + +export const selectVGRVesselsParsed = createSelector([selectVGRUniqVessels], (vessels) => { if (!vessels?.length) { return } @@ -146,7 +153,7 @@ export const selectVGRVesselsPaginated = createSelector( export const selectVGRVesselsPagination = createSelector( [ selectVGRVesselsPaginated, - selectVGRVessels, + selectVGRUniqVessels, selectVGRVesselsFiltered, selectVGRVesselPage, selectVGRVesselsResultsPerPage, From 05b932da01a0d6580b272ed87e1ca8a6ce27eea2 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 27 Sep 2024 14:20:46 +0200 Subject: [PATCH 12/14] fix add vesselGroup vessels from sidebar --- .../vessel-groups/VesselGroupAddButton.tsx | 12 ++-- .../vessel-groups/VesselGroupListTooltip.tsx | 4 +- .../vessel-groups-modal.slice.ts | 1 + .../vessel-groups/vessel-groups.hooks.ts | 53 +++----------- .../vessel-groups/vessel-groups.utils.ts | 70 +++++++++++++------ .../features/vessel/vessel.slice.ts | 2 + .../workspace/vessels/VesselsSection.tsx | 10 ++- 7 files changed, 79 insertions(+), 73 deletions(-) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx index ca92ff8196..c8f049e71e 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupAddButton.tsx @@ -14,6 +14,7 @@ import { useVesselGroupsUpdate, NEW_VESSEL_GROUP_ID, } from './vessel-groups.hooks' +import { parseVesselGroupVessels } from './vessel-groups.utils' type VesselGroupAddButtonProps = { children?: React.ReactNode @@ -72,29 +73,30 @@ function VesselGroupAddButton(props: VesselGroupAddButtonProps) { const { vessels, onAddToVesselGroup, children = } = props const addVesselsToVesselGroup = useVesselGroupsUpdate() const createVesselGroupWithVessels = useVesselGroupsModal() + const vesselGroupVessels = parseVesselGroupVessels(vessels) const handleAddToVesselGroupClick = useCallback( async (vesselGroupId: string) => { if (vesselGroupId !== NEW_VESSEL_GROUP_ID) { - if (vessels.length) { - const vesselGroup = await addVesselsToVesselGroup(vesselGroupId, vessels) + if (vesselGroupVessels.length) { + const vesselGroup = await addVesselsToVesselGroup(vesselGroupId, vesselGroupVessels) if (onAddToVesselGroup && vesselGroup) { onAddToVesselGroup(vesselGroup?.id) } } } else { - createVesselGroupWithVessels(vesselGroupId, vessels) + createVesselGroupWithVessels(vesselGroupId, vesselGroupVessels) if (onAddToVesselGroup) { onAddToVesselGroup(vesselGroupId) } } }, - [addVesselsToVesselGroup, createVesselGroupWithVessels, onAddToVesselGroup, vessels] + [addVesselsToVesselGroup, createVesselGroupWithVessels, onAddToVesselGroup, vesselGroupVessels] ) return ( ) diff --git a/apps/fishing-map/features/vessel-groups/VesselGroupListTooltip.tsx b/apps/fishing-map/features/vessel-groups/VesselGroupListTooltip.tsx index ad8b1169d5..ebbb03d80c 100644 --- a/apps/fishing-map/features/vessel-groups/VesselGroupListTooltip.tsx +++ b/apps/fishing-map/features/vessel-groups/VesselGroupListTooltip.tsx @@ -5,16 +5,16 @@ import { useSelector } from 'react-redux' import React from 'react' import { Popover, Spinner } from '@globalfishingwatch/ui-components' import { - AddVesselGroupVessel, NEW_VESSEL_GROUP_ID, useVesselGroupsOptions, } from 'features/vessel-groups/vessel-groups.hooks' import { selectHasUserGroupsPermissions } from 'features/user/selectors/user.permissions.selectors' import styles from './VesselGroupListTooltip.module.css' +import { VesselGroupVesselIdentity } from './vessel-groups-modal.slice' type VesselGroupListTooltipProps = { children?: React.ReactNode - vessels?: AddVesselGroupVessel[] + vessels?: VesselGroupVesselIdentity[] onAddToVesselGroup?: (vesselGroupId: string) => void } diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index ebecfc4d54..35ee36b08c 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -235,6 +235,7 @@ export const getVesselInVesselGroupThunk = createAsyncThunk( vesselGroup.vessels, vesselsIdentities.entries ) + console.log('🚀 ~ vesselGroupVessels:', vesselGroup.vessels) console.log('🚀 ~ vesselGroupVessels:', vesselGroupVessels) return vesselGroupVessels } catch (e: any) { diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts index a42f043a13..db39fe62cf 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.hooks.ts @@ -6,9 +6,8 @@ import { VesselGroup } from '@globalfishingwatch/api-types' import { selectAllVisibleVesselGroups } from 'features/user/selectors/user.permissions.selectors' import { getVesselGroupLabel } from 'features/vessel-groups/vessel-groups.utils' import { IdentityVesselData } from 'features/vessel/vessel.slice' -import { getCurrentIdentityVessel } from 'features/vessel/vessel.utils' -import { VesselLastIdentity } from 'features/search/search.slice' -import { ReportVesselWithDatasets } from 'features/reports/areas/area-reports.selectors' +// import { VesselLastIdentity } from 'features/search/search.slice' +// import { ReportVesselWithDatasets } from 'features/reports/areas/area-reports.selectors' import { useAppDispatch } from 'features/app/app.hooks' import { sortByCreationDate } from 'utils/dates' import { @@ -17,17 +16,15 @@ import { updateVesselGroupVesselsThunk, } from './vessel-groups.slice' import { - setVesselGroupModalVessels, setVesselGroupEditId, + setVesselGroupModalVessels, setVesselGroupsModalOpen, + VesselGroupVesselIdentity, } from './vessel-groups-modal.slice' export const NEW_VESSEL_GROUP_ID = 'new-vessel-group' -export type AddVesselGroupVessel = - | VesselLastIdentity - | ReportVesselWithDatasets - | IdentityVesselData +export type AddVesselGroupVessel = IdentityVesselData | VesselGroupVesselIdentity export const useVesselGroupsOptions = () => { const { t } = useTranslation() @@ -51,21 +48,10 @@ export const useVesselGroupsOptions = () => { export const useVesselGroupsUpdate = () => { const dispatch = useAppDispatch() const addVesselsToVesselGroup = useCallback( - async (vesselGroupId: string, vessels: AddVesselGroupVessel[]) => { + async (vesselGroupId: string, vessels: VesselGroupVesselIdentity[]) => { const vesselGroup: UpdateVesselGroupThunkParams = { id: vesselGroupId, - vessels: vessels.flatMap((vessel) => { - const { id, dataset } = getCurrentIdentityVessel(vessel as IdentityVesselData) - if (!id || !dataset) { - return [] - } - return { - vesselId: id, - dataset: dataset, - // TODO:VV3 insert vessels with its relationships - relationId: '', - } - }), + vessels: vessels, } const dispatchedAction = await dispatch(updateVesselGroupVesselsThunk(vesselGroup)) if (updateVesselGroupVesselsThunk.fulfilled.match(dispatchedAction)) { @@ -83,32 +69,15 @@ export const useVesselGroupsUpdate = () => { export const useVesselGroupsModal = () => { const dispatch = useAppDispatch() const createVesselGroupWithVessels = useCallback( - async (vesselGroupId: string, vessels: AddVesselGroupVessel[]) => { - // TODO:VV3 user vessels - const vesselsWithDataset = vessels.map((vessel) => ({ - ...vessel, - vesselId: - (vessel as VesselLastIdentity)?.id || (vessel as ReportVesselWithDatasets)?.vesselId, - dataset: (typeof vessel?.dataset === 'string' - ? vessel.dataset - : vessel.dataset?.id || (vessel as ReportVesselWithDatasets)?.infoDataset?.id) as string, - })) - if (vesselsWithDataset?.length) { + async (vesselGroupId: string, vessels: VesselGroupVesselIdentity[]) => { + if (vessels?.length) { if (vesselGroupId && vesselGroupId !== NEW_VESSEL_GROUP_ID) { dispatch(setVesselGroupEditId(vesselGroupId)) } - const vesselGroupDataset = vesselsWithDataset.map((vessel) => { - return { - vesselId: vessel.vesselId, - dataset: vessel.dataset, - relationId: vessel.vesselId, - identity: vessel, - } - }) - // dispatch(setVesselGroupModalVessels(vesselGroupDataset)) + dispatch(setVesselGroupModalVessels(vessels)) dispatch(setVesselGroupsModalOpen(true)) } else { - console.warn('No related activity datasets founds for', vesselsWithDataset) + console.warn('No related activity datasets founds for', vessels) } }, [dispatch] diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts index c1fe336586..863129dce9 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts @@ -7,8 +7,10 @@ import { } from '@globalfishingwatch/api-types' import { PUBLIC_SUFIX } from 'data/config' import { getVesselId, getVesselIdentities, getVesselProperty } from 'features/vessel/vessel.utils' +import { IdentityVesselData } from 'features/vessel/vessel.slice' import { VESSEL_GROUPS_REPORT_RELEASE_DATE } from './vessel-groups.config' import { VesselGroupVesselIdentity } from './vessel-groups-modal.slice' +import { AddVesselGroupVessel } from './vessel-groups.hooks' export const getVesselGroupLabel = (vesselGroup: VesselGroup) => { const isPrivate = !vesselGroup.id.endsWith(`-${PUBLIC_SUFIX}`) @@ -95,28 +97,50 @@ export const mergeVesselGroupVesselIdentities = ( export const flatVesselGroupSearchVessels = ( vesselIdentities: IdentityVessel[] ): VesselGroupVesselIdentity[] => { - return vesselIdentities - .flatMap((vessel) => { - const identities = getVesselIdentities(vessel, { - identitySource: VesselIdentitySourceEnum.SelfReported, - }) - if (!identities?.length) { - return [] - } - const relationId = getVesselId(vessel) - return identities.map((i) => ({ - vesselId: i.id, - dataset: i.dataset as string, - relationId, - identity: (relationId === i.id ? vessel : undefined) as IdentityVessel, - })) - }) - .toSorted((a, b) => { - const aValue = getVesselProperty(a.identity!, 'shipname') - const bValue = getVesselProperty(b.identity!, 'shipname') - if (aValue === bValue) { - return 0 - } - return aValue > bValue ? 1 : -1 + return vesselIdentities.flatMap((vessel) => { + const identities = getVesselIdentities(vessel, { + identitySource: VesselIdentitySourceEnum.SelfReported, }) + if (!identities?.length) { + return [] + } + const relationId = getVesselId(vessel) + return identities.map((i) => ({ + vesselId: i.id, + dataset: i.dataset as string, + relationId, + identity: (relationId === i.id ? vessel : undefined) as IdentityVessel, + })) + }) +} + +export function parseVesselGroupVessels( + vessels: AddVesselGroupVessel[] +): VesselGroupVesselIdentity[] { + return vessels.map((vessel) => { + // TODO:VV3 add support on include area report vessels into a group + // if ((vessel as ReportVesselWithDatasets).vesselId) { + // return { + // vesselId: (vessel as ReportVesselWithDatasets).vesselId, + // dataset: (vessel as ReportVesselWithDatasets).infoDataset?.id as string, + // relationId: (vessel as ReportVesselWithDatasets).vesselId, + // } as VesselGroupVesselIdentity + // } + if ((vessel as IdentityVesselData).identities?.length) { + const identityVessel = vessel as IdentityVesselData + const relationId = identityVessel.id + return { + vesselId: identityVessel.id, + dataset: identityVessel.datasetId, + relationId: relationId, + identity: + relationId === identityVessel.id + ? { + selfReportedInfo: identityVessel.identities, + } + : undefined, + } as VesselGroupVesselIdentity + } + return vessel as VesselGroupVesselIdentity + }) } diff --git a/apps/fishing-map/features/vessel/vessel.slice.ts b/apps/fishing-map/features/vessel/vessel.slice.ts index ca6dab7b95..657c104d17 100644 --- a/apps/fishing-map/features/vessel/vessel.slice.ts +++ b/apps/fishing-map/features/vessel/vessel.slice.ts @@ -53,6 +53,7 @@ export type IdentityVesselData = { id: string identities: VesselDataIdentity[] dataset: Dataset + datasetId: string } & VesselInstanceDatasets & Pick< IdentityVessel, @@ -148,6 +149,7 @@ export const fetchVesselInfoThunk = createAsyncThunk( return { id: getVesselProperty(vessel, 'id'), dataset: dataset, + datasetId: dataset?.id, combinedSourcesInfo: vessel?.combinedSourcesInfo, registryOwners: vessel?.registryOwners, registryPublicAuthorizations: vessel?.registryPublicAuthorizations, diff --git a/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx b/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx index fd0d98cad3..72c44de3a8 100644 --- a/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx +++ b/apps/fishing-map/features/workspace/vessels/VesselsSection.tsx @@ -35,6 +35,8 @@ import { useAppDispatch } from 'features/app/app.hooks' import { getVesselGroupDataviewInstance } from 'features/reports/vessel-groups/vessel-group-report.dataviews' import { selectActiveVesselsDataviews } from 'features/dataviews/selectors/dataviews.categories.selectors' import { setVesselGroupConfirmationMode } from 'features/vessel-groups/vessel-groups-modal.slice' +import { IdentityVesselData } from 'features/vessel/vessel.slice' +import { getVesselId, getVesselIdentities } from 'features/vessel/vessel.utils' import VesselEventsLegend from './VesselEventsLegend' import VesselLayerPanel from './VesselLayerPanel' import VesselsFromPositions from './VesselsFromPositions' @@ -145,7 +147,13 @@ function VesselsSection(): React.ReactElement { ) const vesselsToVesselGroup = areVesselsLoading ? [] - : vesselResources.map((resource) => resource.data) + : vesselResources.map(({ data }) => { + return { + id: getVesselId(data), + identities: getVesselIdentities(data), + datasetId: data.dataset, + } as IdentityVesselData + }) return (
    From a986daa4d1ba9b7d217f352ee9064bd2b790a362 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 27 Sep 2024 14:36:47 +0200 Subject: [PATCH 13/14] fix create vesselgroup from search --- apps/fishing-map/features/reports/areas/area-reports.utils.ts | 1 + apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/fishing-map/features/reports/areas/area-reports.utils.ts b/apps/fishing-map/features/reports/areas/area-reports.utils.ts index 185a8b25c7..e5e0601076 100644 --- a/apps/fishing-map/features/reports/areas/area-reports.utils.ts +++ b/apps/fishing-map/features/reports/areas/area-reports.utils.ts @@ -300,6 +300,7 @@ export function parseReportVesselsToIdentity( return { id: vessel.id || vessel.vesselId, dataset: vessel.infoDataset, + datasetId: vessel.infoDataset?.id, info: vessel.infoDataset?.id!, track: vessel.trackDataset?.id!, registryOwners: [], diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts index 863129dce9..8d5c135547 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups.utils.ts @@ -131,11 +131,12 @@ export function parseVesselGroupVessels( const relationId = identityVessel.id return { vesselId: identityVessel.id, - dataset: identityVessel.datasetId, + dataset: identityVessel.datasetId || (identityVessel.dataset?.id as string), relationId: relationId, identity: relationId === identityVessel.id ? { + dataset: identityVessel.datasetId || identityVessel.dataset?.id, selfReportedInfo: identityVessel.identities, } : undefined, From 0d8b56b6d23872385382cdbdd5a09dfae725e1bd Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 27 Sep 2024 16:03:13 +0200 Subject: [PATCH 14/14] fetch identities in area report in add vessels to group --- .../vessels/ReportVesselsTableFooter.tsx | 21 +++++--- .../reports/areas/area-reports.utils.ts | 45 ---------------- .../vessel-groups/VesselGroupAddButton.tsx | 54 ++++++++++++++++--- .../vessel-groups-modal.slice.ts | 14 +++-- .../vessel-groups/vessel-groups.utils.ts | 8 --- 5 files changed, 68 insertions(+), 74 deletions(-) diff --git a/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx b/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx index 85fc3b4db3..52da554920 100644 --- a/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx +++ b/apps/fishing-map/features/reports/activity/vessels/ReportVesselsTableFooter.tsx @@ -4,6 +4,7 @@ import cx from 'classnames' import { Fragment, useMemo } from 'react' import { unparse as unparseCSV } from 'papaparse' import { saveAs } from 'file-saver' +import { uniq } from 'es-toolkit' import { Button, IconButton } from '@globalfishingwatch/ui-components' import I18nNumber from 'features/i18n/i18nNumber' import { useLocationConnect } from 'routes/routes.hook' @@ -19,10 +20,7 @@ import { selectReportAreaName, ReportVesselWithDatasets, } from 'features/reports/areas/area-reports.selectors' -import { - parseReportVesselsToIdentity, - getVesselsFiltered, -} from 'features/reports/areas/area-reports.utils' +import { getVesselsFiltered } from 'features/reports/areas/area-reports.utils' import styles from './ReportVesselsTableFooter.module.css' import { selectReportVesselsListWithAllInfo, @@ -48,8 +46,15 @@ export default function ReportVesselsTableFooter({ reportName }: ReportVesselsTa const heatmapDataviews = useSelector(selectActiveActivityAndDetectionsDataviews) const { start, end } = useSelector(selectTimeRange) - const vesselGroupIdentityVessels = useMemo(() => { - return parseReportVesselsToIdentity(reportVesselFilter ? allFilteredVessels : allVessels) + const vesselGroupVessels = useMemo(() => { + const vessels = reportVesselFilter ? allFilteredVessels : allVessels + if (!vessels?.length) { + return null + } + return { + ids: vessels?.flatMap((v) => v.id || v.vesselId || []), + datasets: uniq(vessels.flatMap((v) => v.dataset || [])), + } }, [allFilteredVessels, allVessels, reportVesselFilter]) const onDownloadVesselsClick = () => { @@ -96,7 +101,6 @@ export default function ReportVesselsTableFooter({ reportName }: ReportVesselsTa }) } const onAddToVesselGroup = () => { - const dataviewIds = heatmapDataviews.map(({ id }) => id) dispatch(setVesselGroupConfirmationMode('saveAndSeeInWorkspace')) trackEvent({ category: TrackCategory.VesselGroups, @@ -165,7 +169,8 @@ export default function ReportVesselsTableFooter({ reportName }: ReportVesselsTa