mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-23 06:59:10 -04:00
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:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user