Merge pull request #3334 from inaturalist/mob-968-add-edit-icon-to-top-right-of-match-screen-2

MOB-968 Match screen edit icon
This commit is contained in:
Seth Peterson
2026-02-05 11:57:07 -06:00
committed by GitHub
7 changed files with 116 additions and 16 deletions

View File

@@ -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<ScrollView | null>;
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 = ( {
</Body2>
)
}
<HeaderEditIcon observation={observation} lastScreen="Match" taxon={iconicTaxon} />
</View>
<PhotosSection
taxon={taxon}
@@ -143,6 +147,7 @@ const Match = ( {
</Body2>
)
}
<HeaderEditIcon observation={observation} lastScreen="Match" />
</View>
<PhotosSection
taxon={taxon}
@@ -188,6 +193,7 @@ const Match = ( {
)
: <MatchHeader topSuggestion={topSuggestion} />
}
<HeaderEditIcon observation={observation} lastScreen="Match" taxon={taxonToSave} />
</View>
<PhotosSection
representativePhoto={topSuggestion?.taxon?.representative_photo}

View File

@@ -546,6 +546,7 @@ const MatchContainer = ( ) => {
scrollRef={scrollRef}
iconicTaxon={iconicTaxon}
setIconicTaxon={setIconicTaxon}
taxonToSave={taxon}
/>
{renderPermissionsGate( {
// If the user grants location permission while on this screen,

View File

@@ -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 (
<ScrollViewWrapper testID="SavedMatch.container">
<SavedMatchHeaderRight observation={observation} />
<View className={`${matchCardClassTop} mt-[10px]`}>
<HeaderEditIcon observation={observation} />
<View className={matchCardClassTop}>
<MatchHeader hideObservationStatus topSuggestion={observation} />
</View>
<PhotosSection

View File

@@ -22,7 +22,7 @@ const { useRealm } = RealmContext;
type Props = {
observations: Object[],
currentObservation: Object
currentObservation: Object,
}
const ObsEditHeader = ( {
@@ -86,6 +86,15 @@ const ObsEditHeader = ( {
const handleBackButtonPress = useCallback( ( ) => {
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 ) {

View File

@@ -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(
( ) => (
<INatIconButton
testID="SavedMatch.editButton"
onPress={() => 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;

View File

@@ -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<NativeStackNavigationProp<ParamListBase>>();
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();
}

View File

@@ -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( <HeaderEditIcon observation={mockObservation} /> );
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( <HeaderEditIcon observation={mockObservation} /> );
const headerRightCall = mockSetOptions.mock.calls[0][0];
const HeaderRightComponent = headerRightCall.headerRight;
renderComponent( <HeaderRightComponent /> );
const editButton = screen.getByTestId( "ObsEditIcon" );
expect( editButton ).toBeVisible();
fireEvent.press( editButton );
expect( prepareObsEdit ).toHaveBeenCalledWith( mockObservation );
expect( mockNavigate ).toHaveBeenCalledWith( "ObsEdit", undefined );
expect( setMyObsOffsetToRestore ).toHaveBeenCalled();
} );
} );