diff --git a/src/components/Match/Match.tsx b/src/components/Match/Match.tsx index 3091390ad..96b446872 100644 --- a/src/components/Match/Match.tsx +++ b/src/components/Match/Match.tsx @@ -1,5 +1,5 @@ import { useNetInfo } from "@react-native-community/netinfo"; -import type { ApiPhoto, ApiSuggestion } from "api/types"; +import type { ApiPhoto, ApiSuggestion, ApiTaxon } from "api/types"; import LocationSection from "components/ObsDetailsDefaultMode/LocationSection/LocationSection"; import MapSection @@ -7,6 +7,7 @@ import MapSection import { ActivityIndicator, Body2, Button, Heading3, ScrollViewWrapper, } from "components/SharedComponents"; +import HeaderEditIcon from "components/SharedComponents/ObsDetails/HeaderEditIcon"; import { View } from "components/styledComponents"; import React from "react"; import type { ScrollView } from "react-native"; @@ -43,6 +44,7 @@ interface Props { scrollRef: React.RefObject; iconicTaxon?: RealmTaxon; setIconicTaxon: ( taxon: RealmTaxon ) => void; + taxonToSave?: ApiTaxon; } const Match = ( { @@ -59,6 +61,7 @@ const Match = ( { scrollRef, iconicTaxon, setIconicTaxon, + taxonToSave, }: Props ) => { const { t } = useTranslation( ); const { isConnected } = useNetInfo( ); @@ -83,6 +86,7 @@ const Match = ( { ) } + ) } + } + { scrollRef={scrollRef} iconicTaxon={iconicTaxon} setIconicTaxon={setIconicTaxon} + taxonToSave={taxon} /> {renderPermissionsGate( { // If the user grants location permission while on this screen, diff --git a/src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatch.tsx b/src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatch.tsx index 182d8e28a..4141b30c6 100644 --- a/src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatch.tsx +++ b/src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatch.tsx @@ -5,13 +5,12 @@ import PhotosSection from "components/Match/PhotosSection"; import LocationSection from "components/ObsDetailsDefaultMode/LocationSection/LocationSection"; import MapSection from "components/ObsDetailsDefaultMode/MapSection/MapSection"; import { Button, ScrollViewWrapper } from "components/SharedComponents"; +import HeaderEditIcon from "components/SharedComponents/ObsDetails/HeaderEditIcon"; import { View } from "components/styledComponents"; import React from "react"; import type { RealmObservation } from "realmModels/types"; import { useTranslation } from "sharedHooks"; -import SavedMatchHeaderRight from "./SavedMatchHeaderRight"; - interface Props { observation: RealmObservation; navToTaxonDetails: ( ) => void; @@ -29,8 +28,8 @@ const SavedMatch = ( { return ( - - + + { if ( params?.lastScreen === "Suggestions" ) { navigation.navigate( "Suggestions", { lastScreen: "ObsEdit" } ); + } else if ( params?.lastScreen === "Match" && unsavedChanges ) { + // When coming from the match screen, we don't have a version of the match to roll back to + // so if there are changes, they need to restart + // In the future, we'll support a rollback https://linear.app/inaturalist/issue/MOB-1091/match-screen-edit-flow-should-roll-back-changes-on-back-navigation + if ( unsavedChanges ) { + setDiscardObservationSheetVisible( true ); + } else { + navigation.goBack( ); + } } else if ( shouldNavigateBack ) { navigation.goBack( ); } else if ( !savedLocally || savedOrUploadedMultiObsFlow === true ) { diff --git a/src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatchHeaderRight.tsx b/src/components/SharedComponents/ObsDetails/HeaderEditIcon.tsx similarity index 63% rename from src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatchHeaderRight.tsx rename to src/components/SharedComponents/ObsDetails/HeaderEditIcon.tsx index 33614b07d..a56bb0dc9 100644 --- a/src/components/ObsDetailsDefaultMode/SavedMatch/SavedMatchHeaderRight.tsx +++ b/src/components/SharedComponents/ObsDetails/HeaderEditIcon.tsx @@ -1,9 +1,10 @@ import { useNavigation } from "@react-navigation/native"; +import type { ApiTaxon } from "api/types"; import { INatIconButton, } from "components/SharedComponents"; import React, { useCallback, useEffect } from "react"; -import type { RealmObservation } from "realmModels/types"; +import type { RealmObservation, RealmTaxon } from "realmModels/types"; import { useNavigateToObsEdit, useTranslation, @@ -12,20 +13,28 @@ import colors from "styles/tailwindColors"; interface Props { observation: RealmObservation; + lastScreen?: string; + taxon?: ApiTaxon | RealmTaxon; } -const SavedMatchHeaderRight = ( { +const HeaderEditIcon = ( { observation, + lastScreen, + taxon, }: Props ) => { const navigation = useNavigation( ); const { t } = useTranslation( ); const navigateToObsEdit = useNavigateToObsEdit( ); + const handleEditPress = useCallback( ( ) => { + navigateToObsEdit( observation, lastScreen, taxon ); + }, [taxon, navigateToObsEdit, observation, lastScreen] ); + const headerRight = useCallback( ( ) => ( navigateToObsEdit( observation )} + testID="ObsEditIcon" + onPress={handleEditPress} icon="pencil" color={String( colors?.darkGray )} accessibilityLabel={t( "Edit" )} @@ -33,8 +42,7 @@ const SavedMatchHeaderRight = ( { /> ), [ - observation, - navigateToObsEdit, + handleEditPress, t, ], ); @@ -47,4 +55,4 @@ const SavedMatchHeaderRight = ( { return null; }; -export default SavedMatchHeaderRight; +export default HeaderEditIcon; diff --git a/src/sharedHooks/useNavigateToObsEdit.ts b/src/sharedHooks/useNavigateToObsEdit.ts index 98fcfe4d5..e833c0b53 100644 --- a/src/sharedHooks/useNavigateToObsEdit.ts +++ b/src/sharedHooks/useNavigateToObsEdit.ts @@ -1,17 +1,34 @@ import type { ParamListBase } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; -import type { RealmObservation } from "realmModels/types"; +import type { ApiTaxon } from "api/types"; +import type { RealmObservation, RealmTaxon } from "realmModels/types"; import useStore from "stores/useStore"; function useNavigateToObsEdit() { const navigation = useNavigation>(); const prepareObsEdit = useStore( state => state.prepareObsEdit ); const setMyObsOffsetToRestore = useStore( state => state.setMyObsOffsetToRestore ); + const updateObservationKeys = useStore( state => state.updateObservationKeys ); - function navigateToObsEdit( localObservation: RealmObservation ) { + function navigateToObsEdit( + localObservation: RealmObservation, + lastScreen?: string, + taxon?: ApiTaxon | RealmTaxon, + ) { prepareObsEdit( localObservation ); - navigation.navigate( "ObsEdit" ); + if ( taxon ) { + updateObservationKeys( { + owners_identification_from_vision: true, + taxon, + } ); + } + navigation.navigate( + "ObsEdit", + lastScreen + ? { lastScreen } + : undefined, + ); setMyObsOffsetToRestore(); } diff --git a/tests/unit/components/SharedComponents/ObsDetails/HeaderEditIcon.test.js b/tests/unit/components/SharedComponents/ObsDetails/HeaderEditIcon.test.js new file mode 100644 index 000000000..9d35c9a98 --- /dev/null +++ b/tests/unit/components/SharedComponents/ObsDetails/HeaderEditIcon.test.js @@ -0,0 +1,60 @@ +import { fireEvent, screen } from "@testing-library/react-native"; +import HeaderEditIcon from "components/SharedComponents/ObsDetails/HeaderEditIcon"; +import React from "react"; +import useStore from "stores/useStore"; +import factory from "tests/factory"; +import { renderComponent } from "tests/helpers/render"; + +const mockNavigate = jest.fn(); +const mockSetOptions = jest.fn(); +jest.mock( "@react-navigation/native", () => ( { + ...jest.requireActual( "@react-navigation/native" ), + useNavigation: () => ( { + navigate: mockNavigate, + setOptions: mockSetOptions, + } ), +} ) ); + +describe( "HeaderEditIcon", () => { + const mockObservation = factory( "LocalObservation" ); + + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( "sets navigation header options", () => { + renderComponent( ); + + expect( mockSetOptions ).toHaveBeenCalledWith( + expect.objectContaining( { + headerRight: expect.any( Function ), + } ), + ); + } ); + + it( "renders edit button and calls relevant functions on press", () => { + const prepareObsEdit = jest.fn(); + const setMyObsOffsetToRestore = jest.fn(); + + useStore.setState( { + prepareObsEdit, + setMyObsOffsetToRestore, + } ); + + renderComponent( ); + + const headerRightCall = mockSetOptions.mock.calls[0][0]; + const HeaderRightComponent = headerRightCall.headerRight; + + renderComponent( ); + + const editButton = screen.getByTestId( "ObsEditIcon" ); + expect( editButton ).toBeVisible(); + + fireEvent.press( editButton ); + + expect( prepareObsEdit ).toHaveBeenCalledWith( mockObservation ); + expect( mockNavigate ).toHaveBeenCalledWith( "ObsEdit", undefined ); + expect( setMyObsOffsetToRestore ).toHaveBeenCalled(); + } ); +} );