mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
* Adds a component for adding identifications, both from Obs Detail and Obs Edit * Minor style and eslint change to disallow all-caps string literals Closes #103 Co-authored-by: Ken-ichi Ueda <kenichi.ueda@gmail.com>
This commit is contained in:
@@ -6,6 +6,12 @@ module.exports = {
|
||||
"comma-dangle": [2, "never"],
|
||||
"space-in-parens": [2, "always"],
|
||||
"prettier/prettier": 0,
|
||||
"i18next/no-literal-string": [2, {
|
||||
words: {
|
||||
// Minor change to the default to disallow all-caps string literals as well
|
||||
exclude: ["[0-9!-/:-@[-`{-~]+"]
|
||||
}
|
||||
}],
|
||||
"no-var": 1
|
||||
},
|
||||
// need this so jest doesn't show as undefined in jest.setup.js
|
||||
|
||||
2566
package-lock.json
generated
2566
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"translate": "node src/i18n/i18ncli.js build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gorhom/bottom-sheet": "^4.3.1",
|
||||
"@react-native-community/cameraroll": "^4.1.2",
|
||||
"@react-native-community/checkbox": "^0.5.12",
|
||||
"@react-native-community/datetimepicker": "^6.1.0",
|
||||
|
||||
@@ -6,22 +6,18 @@ import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
|
||||
|
||||
const createIdentification = async ( params: Object ): Promise<?number> => {
|
||||
const apiToken = await getJWTToken( false );
|
||||
try {
|
||||
const apiParams = {
|
||||
identification: params
|
||||
};
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
// additional keys for obs detail id creation:
|
||||
// body
|
||||
// vision
|
||||
// disagreement
|
||||
const response = await inatjs.identifications.create( apiParams, options );
|
||||
return response.total_results;
|
||||
} catch ( e ) {
|
||||
console.log( "Couldn't create identification:", JSON.stringify( e.response ), );
|
||||
}
|
||||
const apiParams = {
|
||||
identification: params
|
||||
};
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
// additional keys for obs detail id creation:
|
||||
// body
|
||||
// vision
|
||||
// disagreement
|
||||
const response = await inatjs.identifications.create( apiParams, options );
|
||||
return response.total_results;
|
||||
};
|
||||
|
||||
export default createIdentification;
|
||||
|
||||
@@ -8,7 +8,6 @@ import jwt from "react-native-jwt-io";
|
||||
import {Platform} from "react-native";
|
||||
import {getBuildNumber, getDeviceType, getSystemName, getSystemVersion, getVersion} from "react-native-device-info";
|
||||
import Realm from "realm";
|
||||
|
||||
import realmConfig from "../../models/index";
|
||||
|
||||
// Base API domain can be overridden (in case we want to use staging URL) - either by placing it in .env file, or
|
||||
@@ -302,6 +301,18 @@ const getUsername = async (): Promise<string> => {
|
||||
return await RNSInfo.getItem( "username", {} );
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the logged-in user
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const getUser = async (): Promise<Object | null> => {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
return realm.objects( "User" ).filtered( "signedIn == true" )[0];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the logged-in userId
|
||||
*
|
||||
@@ -310,9 +321,9 @@ const getUsername = async (): Promise<string> => {
|
||||
const getUserId = async (): Promise<string | null> => {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
const currentUser = realm.objects( "User" ).filtered( "signedIn == true" )[0];
|
||||
const currentUserId = currentUser?.id?.toString( );
|
||||
const userId = currentUser?.id?.toString( );
|
||||
realm.close( );
|
||||
return currentUserId;
|
||||
return userId;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -337,5 +348,6 @@ export {
|
||||
getUsername,
|
||||
signOut,
|
||||
getJWTToken,
|
||||
getUser,
|
||||
getUserId
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ const ActivityItem = ( { item, navToTaxonDetails, handlePress, toggleRefetch }:
|
||||
}, [user] );
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[item.temporary ? viewStyles.temporaryRow : null]}>
|
||||
<View style={[viewStyles.userProfileRow, viewStyles.rowBorder]}>
|
||||
{user && (
|
||||
<Pressable
|
||||
@@ -73,7 +73,7 @@ const ActivityItem = ( { item, navToTaxonDetails, handlePress, toggleRefetch }:
|
||||
<View style={viewStyles.speciesDetailRow}>
|
||||
<Text>{item.body}</Text>
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useContext } from "react";
|
||||
import _ from "lodash";
|
||||
import { Text, View, Image, Pressable, ScrollView, LogBox } from "react-native";
|
||||
import React, {useState, useContext, useEffect} from "react";
|
||||
import type { Node } from "react";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { t } from "i18next";
|
||||
import { formatISO } from "date-fns";
|
||||
import { Text, View, Image, Pressable, ScrollView, LogBox, Alert } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import ActivityTab from "./ActivityTab";
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import PhotoScroll from "../SharedComponents/PhotoScroll";
|
||||
import DataTab from "./DataTab";
|
||||
import { useRemoteObservation } from "./hooks/useRemoteObservation";
|
||||
import Taxon from "../../models/Taxon";
|
||||
import User from "../../models/User";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import createComment from "./helpers/createComment";
|
||||
import faveObservation from "./helpers/faveObservation";
|
||||
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
|
||||
import { formatObsListTime } from "../../sharedHelpers/dateAndTime";
|
||||
import createComment from "./helpers/createComment";
|
||||
import createIdentification from "../Identify/helpers/createIdentification";
|
||||
import DataTab from "./DataTab";
|
||||
import faveObservation from "./helpers/faveObservation";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ObsDetailsHeader from "./ObsDetailsHeader";
|
||||
import PhotoScroll from "../SharedComponents/PhotoScroll";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import Taxon from "../../models/Taxon";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import User from "../../models/User";
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useRemoteObservation } from "./hooks/useRemoteObservation";
|
||||
import { viewStyles, textStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import { formatObsListTime } from "../../sharedHelpers/dateAndTime";
|
||||
import { getUser } from "../LoginSignUp/AuthenticationService";
|
||||
|
||||
// this is getting triggered by passing dates, like _created_at, through
|
||||
// react navigation via the observation object. it doesn't seem to
|
||||
@@ -33,6 +37,7 @@ LogBox.ignoreLogs( [
|
||||
] );
|
||||
|
||||
const ObsDetails = ( ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const [refetch, setRefetch] = useState( false );
|
||||
const [showCommentBox, setShowCommentBox] = useState( false );
|
||||
const [comment, setComment] = useState( "" );
|
||||
@@ -41,6 +46,7 @@ const ObsDetails = ( ): Node => {
|
||||
let observation = params.observation;
|
||||
const [tab, setTab] = useState( 0 );
|
||||
const navigation = useNavigation( );
|
||||
const [ids, setIds] = useState( [] );
|
||||
|
||||
// TODO: we'll probably need to redo this logic a bit now that we're
|
||||
// passing an observation via navigation instead of reopening realm
|
||||
@@ -54,20 +60,77 @@ const ObsDetails = ( ): Node => {
|
||||
|
||||
const toggleRefetch = ( ) => setRefetch( !refetch );
|
||||
|
||||
useEffect( () => {
|
||||
if ( observation ) {setIds( observation.identifications.map( i => i ) );}
|
||||
}, [observation] );
|
||||
|
||||
if ( !observation ) { return null; }
|
||||
|
||||
const ids = observation.identifications.map( i => i );
|
||||
|
||||
const comments = observation.comments.map( c => c );
|
||||
const photos = _.compact( observation.observationPhotos.map( op => op.photo ) );
|
||||
const user = observation.user;
|
||||
const taxon = observation.taxon;
|
||||
const uuid = observation.uuid;
|
||||
|
||||
const onIDAdded = async ( identification ) => {
|
||||
console.log( "onIDAdded", identification );
|
||||
|
||||
// Add temporary ID to observation.identifications ("ghosted" ID, while we're trying to add it)
|
||||
const currentUser = await getUser();
|
||||
const newId = {
|
||||
body: identification.body,
|
||||
taxon: identification.taxon,
|
||||
user: {
|
||||
id: currentUser?.id,
|
||||
login: currentUser?.login,
|
||||
signedIn: true
|
||||
},
|
||||
created_at: formatISO( Date.now() ),
|
||||
uuid: identification.uuid,
|
||||
vision: false,
|
||||
// This tells us to render is ghosted (since it's temporarily visible until getting a response from the server)
|
||||
temporary: true
|
||||
};
|
||||
setIds( [ ...ids, newId ] );
|
||||
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const results = await createIdentification( { observation_id: observation.uuid, taxon_id: newId.taxon.id, body: newId.body } );
|
||||
|
||||
if ( results === 1 ) {
|
||||
// Remove ghosted highlighting
|
||||
newId.temporary = false;
|
||||
setIds( [ ...ids, newId ] );
|
||||
} else {
|
||||
// Couldn't create ID
|
||||
error = t( "Couldnt-create-identification", { error: t( "Unknown-error" ) } );
|
||||
}
|
||||
} catch ( e ) {
|
||||
error = t( "Couldnt-create-identification", { error: e.message } );
|
||||
}
|
||||
|
||||
if ( error ) {
|
||||
// Remove temporary ID and show error
|
||||
setIds( [...ids] );
|
||||
|
||||
Alert.alert(
|
||||
"Error",
|
||||
error,
|
||||
[{ text: t( "OK" ) }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const navToUserProfile = userId => navigation.navigate( "UserProfile", { userId } );
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id: taxon.id } );
|
||||
const navToCVSuggestions = ( ) => {
|
||||
const navToAddID = ( ) => {
|
||||
addObservations( [observation] );
|
||||
navigation.navigate( "camera", { screen: "Suggestions" } );
|
||||
navigation.push( "AddID", { onIDAdded: onIDAdded, goBackOnSave: true } );
|
||||
};
|
||||
const openCommentBox = ( ) => setShowCommentBox( true );
|
||||
const submitComment = async ( ) => {
|
||||
@@ -155,14 +218,14 @@ const ObsDetails = ( ): Node => {
|
||||
onPress={showActivityTab}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={textStyles.greenButtonText}>ACTIVITY</Text>
|
||||
<TranslatedText style={textStyles.greenButtonText} text="ACTIVITY" />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={showDataTab}
|
||||
testID="ObsDetails.DataTab"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={textStyles.greenButtonText}>DATA</Text>
|
||||
<TranslatedText style={textStyles.greenButtonText} text="DATA" />
|
||||
</Pressable>
|
||||
</View>
|
||||
{tab === 0
|
||||
@@ -182,7 +245,7 @@ const ObsDetails = ( ): Node => {
|
||||
but it doesn't appear to be working on staging either (Mar 11, 2022) */}
|
||||
<RoundGreenButton
|
||||
buttonText="Suggest an ID"
|
||||
handlePress={navToCVSuggestions}
|
||||
handlePress={navToAddID}
|
||||
testID="ObsDetail.cvSuggestionsButton"
|
||||
/>
|
||||
</View>
|
||||
|
||||
196
src/components/ObsEdit/AddID.js
Normal file
196
src/components/ObsEdit/AddID.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
View,
|
||||
Pressable,
|
||||
TouchableOpacity, FlatList, Image
|
||||
} from "react-native";
|
||||
import {useNavigation} from "@react-navigation/native";
|
||||
import { viewStyles, textStyles } from "../../styles/obsDetails/addID";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TextInput as NativeTextInput } from "react-native";
|
||||
import AddIDHeader from "./AddIDHeader";
|
||||
import {useRef, useState} from "react";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetModal,
|
||||
BottomSheetModalProvider
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import {Button, Headline, Text, TextInput} from "react-native-paper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import {colors} from "../../styles/global";
|
||||
import uuid from "react-native-uuid";
|
||||
|
||||
type Props = {
|
||||
route: {
|
||||
params: {
|
||||
onIDAdded: ( identification: {[string]: any} ) => void,
|
||||
goBackOnSave: boolean,
|
||||
hideComment: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const AddID = ( { route }: Props ): React.Node => {
|
||||
const { t } = useTranslation( );
|
||||
const [comment, setComment] = useState( "" );
|
||||
const [commentDraft, setCommentDraft] = useState( "" );
|
||||
const { onIDAdded, goBackOnSave, hideComment } = route.params;
|
||||
const bottomSheetModalRef = useRef( null );
|
||||
const [taxonSearch, setTaxonSearch] = useState( "" );
|
||||
const taxonList = useRemoteSearchResults( taxonSearch, "taxa", "taxon.name,taxon.preferred_common_name,taxon.default_photo.square_url,taxon.rank" );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const renderBackdrop = ( props ) => (
|
||||
<BottomSheetBackdrop {...props} pressBehavior={"close"}
|
||||
appearsOnIndex={0}
|
||||
disappearsOnIndex={-1}
|
||||
/>
|
||||
);
|
||||
|
||||
const editComment = ( event ) => {
|
||||
setCommentDraft( comment );
|
||||
bottomSheetModalRef.current?.present();
|
||||
};
|
||||
|
||||
const createPhoto = ( photo ) => {
|
||||
return {
|
||||
id: photo.id,
|
||||
url: photo.square_url
|
||||
};
|
||||
};
|
||||
|
||||
const createID = ( taxon ) => {
|
||||
const newTaxon = {
|
||||
id: taxon.id,
|
||||
default_photo: taxon.default_photo ? createPhoto( taxon.default_photo ) : null,
|
||||
name: taxon.name,
|
||||
preferred_common_name: taxon.preferred_common_name,
|
||||
rank: taxon.rank
|
||||
};
|
||||
const newID = {
|
||||
uuid: uuid.v4( ),
|
||||
body: comment,
|
||||
taxon: newTaxon
|
||||
};
|
||||
|
||||
return newID;
|
||||
};
|
||||
|
||||
const renderTaxonResult = ( {item} ) => {
|
||||
const taxonImage = item.default_photo ? { uri: item.default_photo.square_url } : Icon.getImageSourceSync( "leaf", 50, colors.inatGreen );
|
||||
|
||||
return <View style={viewStyles.taxonResult} testID={`Search.taxa.${item.id}`}>
|
||||
<Image style={viewStyles.taxonResultIcon} source={taxonImage} testID={`Search.taxa.${item.id}.photo`}
|
||||
/>
|
||||
<View style={viewStyles.taxonResultNameContainer}>
|
||||
<Text style={textStyles.taxonResultName}>{item.name}</Text>
|
||||
<Text style={textStyles.taxonResultScientificName}>{item.preferred_common_name}</Text>
|
||||
</View>
|
||||
<Pressable style={viewStyles.taxonResultInfo} onPress={() => navigation.navigate( "TaxonDetails", { id: item.id } )} accessibilityRole="link"><Icon style={textStyles.taxonResultInfoIcon} name="information-outline" size={25} /></Pressable>
|
||||
<Pressable style={viewStyles.taxonResultSelect} onPress={() => { onIDAdded( createID( item ) ); if ( goBackOnSave ) {navigation.goBack();} }} accessibilityRole="link"><Icon style={textStyles.taxonResultSelectIcon} name="check-bold" size={25} /></Pressable>
|
||||
</View>;
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheetModalProvider>
|
||||
<ViewNoFooter>
|
||||
<AddIDHeader showEditComment={!hideComment && comment.length === 0} onEditCommentPressed={editComment} />
|
||||
<View>
|
||||
<View style={viewStyles.scrollView}>
|
||||
{comment.length > 0 && <View>
|
||||
<Text>{t( "ID-Comment" )}</Text>
|
||||
<View style={viewStyles.commentContainer}>
|
||||
<Icon style={textStyles.commentLeftIcon} name="chat-processing-outline" size={25} />
|
||||
<Text style={textStyles.comment}>{comment}</Text>
|
||||
<Pressable style={viewStyles.commentRightIconContainer} onPress={editComment} accessibilityRole="link"><Icon style={textStyles.commentRightIcon} name="pencil" size={25} /></Pressable>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
<Text>{t( "Search-Taxon-ID" )}</Text>
|
||||
<TextInput
|
||||
testID={"SearchTaxon"}
|
||||
left={<TextInput.Icon name={() => <Icon style={textStyles.taxonSearchIcon} name={"magnify"} size={25} />} />}
|
||||
style={viewStyles.taxonSearch}
|
||||
value={taxonSearch}
|
||||
onChangeText={setTaxonSearch}
|
||||
selectionColor={colors.black}
|
||||
/>
|
||||
<FlatList
|
||||
data={taxonList}
|
||||
renderItem={renderTaxonResult}
|
||||
keyExtractor={item => item.id}
|
||||
style={viewStyles.taxonList}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
index={0}
|
||||
enableOverDrag={false}
|
||||
enablePanDownToClose={false}
|
||||
snapPoints={["50%"]}
|
||||
backdropComponent={renderBackdrop}
|
||||
style={viewStyles.bottomModal}
|
||||
>
|
||||
<Headline style={textStyles.commentHeader}>{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-optional-comment" )}</Headline>
|
||||
<View style={viewStyles.commentInputContainer}>
|
||||
<TextInput
|
||||
keyboardType="default"
|
||||
style={viewStyles.commentInput}
|
||||
value={commentDraft}
|
||||
selectionColor={colors.black}
|
||||
activeUnderlineColor={colors.transparent}
|
||||
autoFocus
|
||||
multiline
|
||||
onChangeText={setCommentDraft}
|
||||
render={( innerProps ) => (
|
||||
<NativeTextInput
|
||||
{...innerProps}
|
||||
style={[
|
||||
innerProps.style,
|
||||
viewStyles.commentInputText
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={viewStyles.commentClear}
|
||||
onPress={() => setCommentDraft( "" )}>
|
||||
<Text
|
||||
style={[viewStyles.commentClearText, commentDraft.length === 0 ? textStyles.disabled : null]}>{t( "Clear" )}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={viewStyles.commentButtonContainer}>
|
||||
<Button
|
||||
style={viewStyles.commentButton}
|
||||
uppercase={false}
|
||||
color={colors.midGray}
|
||||
onPress={() => {
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
}}>{t( "Cancel" )}</Button>
|
||||
<Button
|
||||
style={viewStyles.commentButton}
|
||||
uppercase={false}
|
||||
disabled={commentDraft.length === 0}
|
||||
color={colors.midGray}
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
setComment( commentDraft );
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
}}>{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-comment" )}</Button>
|
||||
</View>
|
||||
</BottomSheetModal>
|
||||
</ViewNoFooter>
|
||||
</BottomSheetModalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddID;
|
||||
|
||||
32
src/components/ObsEdit/AddIDHeader.js
Normal file
32
src/components/ObsEdit/AddIDHeader.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Headline } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {Pressable, View} from "react-native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { viewStyles } from "../../styles/obsDetails/addID";
|
||||
|
||||
type Props = {
|
||||
showEditComment: boolean,
|
||||
onEditCommentPressed: ( event: any ) => void
|
||||
}
|
||||
|
||||
const AddIDHeader = ( { showEditComment, onEditCommentPressed }: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
return (
|
||||
<View style={viewStyles.headerRow}>
|
||||
<HeaderBackButton onPress={( ) => navigation.goBack( )} />
|
||||
<Headline>{t( "Add-ID-Header" )}</Headline>
|
||||
{showEditComment ?
|
||||
<Pressable onPress={onEditCommentPressed} accessibilityRole="link"><Icon name="chat-processing-outline" size={25} /></Pressable> : <View />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddIDHeader;
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { Text, Pressable, FlatList, View } from "react-native";
|
||||
import {Text, Pressable, FlatList, View} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,9 +25,14 @@ const IdentificationSection = ( ): Node => {
|
||||
const currentObs = observations[currentObsIndex];
|
||||
const identification = currentObs.taxon;
|
||||
|
||||
const onIDAdded = async ( id ) => {
|
||||
console.log( "onIDAdded", id );
|
||||
updateIdentification( id.taxon );
|
||||
};
|
||||
|
||||
const updateIdentification = ( taxon ) => updateTaxon( taxon );
|
||||
|
||||
const navToSuggestionsPage = ( ) => navigation.navigate( "Suggestions" );
|
||||
const navToAddID = ( ) => navigation.push( "AddID", { onIDAdded: onIDAdded, hideComment: true } );
|
||||
|
||||
const renderIconicTaxaButton = ( { item } ) => {
|
||||
const id = iconicTaxaIds[item];
|
||||
@@ -60,7 +65,7 @@ const IdentificationSection = ( ): Node => {
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={navToSuggestionsPage}
|
||||
onPress={navToAddID}
|
||||
>
|
||||
<PlaceholderText text="edit" style={[textStyles.text]} />
|
||||
</Pressable>
|
||||
@@ -70,7 +75,7 @@ const IdentificationSection = ( ): Node => {
|
||||
return (
|
||||
<>
|
||||
<RoundGreenButton
|
||||
handlePress={navToSuggestionsPage}
|
||||
handlePress={navToAddID}
|
||||
buttonText="View Identification Suggestions"
|
||||
testID="ObsEdit.Suggestions"
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,6 @@ type Props = {
|
||||
}
|
||||
|
||||
const PlaceholderText = ( { text, style }: Props ): Node => (
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
<Text style={[{ color: "red", textTransform: "uppercase", fontSize: 30 }].concat( style )}>
|
||||
{ text }
|
||||
</Text>
|
||||
|
||||
@@ -14,6 +14,7 @@ import PhotoGalleryProvider from "../providers/PhotoGalleryProvider";
|
||||
import PhotoGallery from "../components/PhotoLibrary/PhotoGallery";
|
||||
import PermissionGate from "../components/SharedComponents/PermissionGate";
|
||||
import Mortal from "../components/SharedComponents/Mortal";
|
||||
import AddID from "../components/ObsEdit/AddID";
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
@@ -79,6 +80,11 @@ const CameraStackNavigation = ( ): React.Node => (
|
||||
headerShown: true
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AddID"
|
||||
component={AddID}
|
||||
options={hideHeader}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</PhotoGalleryProvider>
|
||||
</Mortal>
|
||||
|
||||
@@ -9,6 +9,7 @@ import ObsDetails from "../components/ObsDetails/ObsDetails";
|
||||
import UserProfile from "../components/UserProfile/UserProfile";
|
||||
import TaxonDetails from "../components/TaxonDetails/TaxonDetails";
|
||||
import Mortal from "../components/SharedComponents/Mortal";
|
||||
import AddID from "../components/ObsEdit/AddID";
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
@@ -47,6 +48,11 @@ const MyObservationsStackNavigation = ( ): React.Node => (
|
||||
component={TaxonDetails}
|
||||
options={showBackButton}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AddID"
|
||||
component={AddID}
|
||||
options={hideHeader}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</Mortal>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ const formatObsListTime = ( date ) => {
|
||||
return format( date, dateTime );
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
formatDateAndTime,
|
||||
timeAgo,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export const colors = {
|
||||
white: "#ffffff",
|
||||
black: "#000000",
|
||||
transparent: "#ff000000",
|
||||
inatGreen: "#77b300",
|
||||
gray: "#393939",
|
||||
lightGray: "#f5f5f5",
|
||||
|
||||
145
src/styles/obsDetails/addID.js
Normal file
145
src/styles/obsDetails/addID.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// @flow strict-local
|
||||
|
||||
import { StyleSheet, Dimensions } from "react-native";
|
||||
|
||||
import type { ViewStyleProp, TextStyleProp, ImageStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import { colors } from "../global";
|
||||
|
||||
const { width } = Dimensions.get( "screen" );
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
padding: 10
|
||||
},
|
||||
scrollView: {
|
||||
paddingTop: 10,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingBottom: 60
|
||||
},
|
||||
commentInputContainer: {
|
||||
position: "relative"
|
||||
},
|
||||
commentInput: {
|
||||
backgroundColor: colors.white,
|
||||
height: 150,
|
||||
padding: 0,
|
||||
marginBottom: 20
|
||||
},
|
||||
commentInputText: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.gray,
|
||||
borderRadius: 10,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
height: 150
|
||||
},
|
||||
bottomModal: {
|
||||
padding: 20
|
||||
},
|
||||
commentButtonContainer: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%",
|
||||
justifyContent: "space-around"
|
||||
},
|
||||
commentButton: {
|
||||
width: 150
|
||||
},
|
||||
commentContainer: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: width - 40,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "#D9E5B8",
|
||||
alignItems: "center"
|
||||
},
|
||||
commentRightIconContainer: {
|
||||
marginLeft: "auto"
|
||||
},
|
||||
taxonSearch: {
|
||||
marginTop: 10
|
||||
},
|
||||
taxonList: {
|
||||
marginBottom: "auto",
|
||||
marginTop: 20
|
||||
},
|
||||
taxonResult: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 10
|
||||
},
|
||||
taxonResultIcon: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 10,
|
||||
backgroundColor: colors.lightGray
|
||||
},
|
||||
taxonResultNameContainer: {
|
||||
flexDirection: "column"
|
||||
},
|
||||
taxonResultInfo: {
|
||||
marginLeft: "auto",
|
||||
marginRight: 10
|
||||
},
|
||||
commentClear: {
|
||||
position: "absolute",
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
zIndex: 9999,
|
||||
width: 40,
|
||||
height: 20
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
disabled: {
|
||||
color: colors.lightGray
|
||||
},
|
||||
taxonResultName: {
|
||||
fontWeight: "bold"
|
||||
},
|
||||
taxonResultScientificName: {
|
||||
color: colors.gray,
|
||||
fontStyle: "italic"
|
||||
},
|
||||
commentHeader: {
|
||||
fontSize: 22,
|
||||
marginBottom: 10
|
||||
},
|
||||
taxonSearchIcon: {
|
||||
color: colors.gray
|
||||
},
|
||||
taxonResultInfoIcon: {
|
||||
color: colors.gray
|
||||
},
|
||||
taxonResultSelectIcon: {
|
||||
color: colors.inatGreen
|
||||
},
|
||||
commentClearText: {
|
||||
color: colors.black
|
||||
},
|
||||
commentLeftIcon: {
|
||||
marginRight: 10,
|
||||
color: colors.inatGreen
|
||||
},
|
||||
commentRightIcon: {
|
||||
color: colors.inatGreen
|
||||
}
|
||||
} );
|
||||
|
||||
const imageStyles: { [string]: ImageStyleProp } = StyleSheet.create( {
|
||||
|
||||
} );
|
||||
|
||||
export {
|
||||
viewStyles,
|
||||
textStyles,
|
||||
imageStyles
|
||||
};
|
||||
@@ -8,6 +8,9 @@ import { colors } from "../global";
|
||||
const { width } = Dimensions.get( "screen" );
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
temporaryRow: {
|
||||
opacity: 0.5
|
||||
},
|
||||
hoverCommentBox: {
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
52
tests/unit/components/AddID/AddID.test.js
Normal file
52
tests/unit/components/AddID/AddID.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import {fireEvent, render, waitFor} from "@testing-library/react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import AddID from "../../../../src/components/ObsEdit/AddID";
|
||||
import factory, {makeResponse} from "../../../factory";
|
||||
// Mock inaturalistjs so we can make some fake responses
|
||||
jest.mock( "inaturalistjs" );
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
// this resolves a test failure with the Animated library:
|
||||
// Animated: `useNativeDriver` is not supported because the native animated module is missing.
|
||||
jest.useFakeTimers( );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useRoute: ( ) => ( {
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
const testTaxaList = [
|
||||
{ taxon: factory( "RemoteTaxon" ) },
|
||||
{ taxon: factory( "RemoteTaxon" ) },
|
||||
{ taxon: factory( "RemoteTaxon" ) }
|
||||
];
|
||||
|
||||
const mockExpected = testTaxaList;
|
||||
|
||||
const renderAddID = ( route ) => render(
|
||||
<NavigationContainer>
|
||||
<AddID route={route} />
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
test( "renders taxon search results", async ( ) => {
|
||||
inatjs.search.mockResolvedValue( makeResponse( mockExpected ) );
|
||||
const route = { params: { } };
|
||||
const { getByTestId } = renderAddID( route );
|
||||
|
||||
|
||||
const input = getByTestId( "SearchTaxon" );
|
||||
await waitFor( () => {
|
||||
fireEvent.changeText( input, "Some taxon" );
|
||||
} );
|
||||
|
||||
const taxon = testTaxaList[0].taxon;
|
||||
|
||||
expect( getByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy( );
|
||||
expect( getByTestId( `Search.taxa.${taxon.id}.photo` ).props.source ).toStrictEqual( { "uri": taxon.default_photo.square_url } );
|
||||
} );
|
||||
Reference in New Issue
Block a user