mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
301 flag id functionality obsdetail (#333)
* Flag Id Modal created. * Flag item modal radio buttons, styling, cancel button * Flag Item Modal, Api Flags file created * Added id field in Identifications realm schema, Flag Item Modal API call * Clear form, toggle function, click title not just checkbox to toggle * Fixed android checkbox toggle. * Clean up. Flag ID ObsDetail. Closes #301. * Flag realm model, flagged status shown on initial load * Refresh observation after item flagged. In Progress. * Remove console logs, add onError * save button loading spinner * Realm 31 migration for obsservation updated_at * Flags test file * FlagItemModal tests * FlagItemModal tests --------- Co-authored-by: Amanda Bullington <albullington@gmail.com>
This commit is contained in:
23
src/api/flags.js
Normal file
23
src/api/flags.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "all"
|
||||
};
|
||||
|
||||
const createFlag = async (
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.flags.create( { ...PARAMS, ...params }, opts );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default createFlag;
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { deleteComments } from "api/comments";
|
||||
import { isCurrentUser } from "components/LoginSignUp/AuthenticationService";
|
||||
import FlagItemModal from "components/ObsDetails/FlagItemModal";
|
||||
import InlineUser from "components/SharedComponents/InlineUser";
|
||||
import KebabMenu from "components/SharedComponents/KebabMenu";
|
||||
import UserText from "components/SharedComponents/UserText";
|
||||
import {
|
||||
Image, Pressable, Text, View
|
||||
Image,
|
||||
Pressable, Text, View
|
||||
} from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import _ from "lodash";
|
||||
@@ -39,11 +41,17 @@ const ActivityItem = ( {
|
||||
}: Props ): Node => {
|
||||
const [currentUser, setCurrentUser] = useState( null );
|
||||
const [kebabMenuVisible, setKebabMenuVisible] = useState( false );
|
||||
const [flagModalVisible, setFlagModalVisible] = useState( false );
|
||||
const [flaggedStatus, setFlaggedStatus] = useState( false );
|
||||
const { taxon } = item;
|
||||
const { user } = item;
|
||||
|
||||
const realm = useRealm( );
|
||||
const queryClient = useQueryClient( );
|
||||
const itemType = item.category ? "Identification" : "Comment";
|
||||
const activityItemClassName = flaggedStatus
|
||||
? "flex-row border border-borderGray py-1 justify-between bg-flaggedBackground"
|
||||
: "flex-row border border-borderGray py-1 justify-between";
|
||||
const isOnline = useIsConnected( );
|
||||
|
||||
useEffect( ( ) => {
|
||||
@@ -52,7 +60,12 @@ const ActivityItem = ( {
|
||||
setCurrentUser( current );
|
||||
};
|
||||
isActiveUserTheCurrentUser( );
|
||||
}, [user] );
|
||||
|
||||
// show flagged activity item right after flag item modal closes
|
||||
if ( item.flags?.length > 0 ) {
|
||||
setFlaggedStatus( true );
|
||||
}
|
||||
}, [user, item] );
|
||||
|
||||
const deleteCommentMutation = useAuthenticatedMutation(
|
||||
( uuid, optsWithAuth ) => deleteComments( uuid, optsWithAuth ),
|
||||
@@ -64,6 +77,15 @@ const ActivityItem = ( {
|
||||
}
|
||||
);
|
||||
|
||||
const closeFlagItemModal = () => {
|
||||
setFlagModalVisible( false );
|
||||
};
|
||||
|
||||
const onItemFlagged = () => {
|
||||
setFlaggedStatus( true );
|
||||
refetchRemoteObservation();
|
||||
};
|
||||
|
||||
const showNoInternetIcon = accessibilityLabel => (
|
||||
<View className="mr-3">
|
||||
<IconMaterial
|
||||
@@ -77,7 +99,7 @@ const ActivityItem = ( {
|
||||
|
||||
return (
|
||||
<View className={item.temporary && "opacity-50"}>
|
||||
<View className="flex-row border border-borderGray py-1 justify-between">
|
||||
<View className={activityItemClassName}>
|
||||
{user && <InlineUser user={user} />}
|
||||
<View className="flex-row items-center">
|
||||
{item.vision
|
||||
@@ -87,9 +109,19 @@ const ActivityItem = ( {
|
||||
source={require( "images/id_rg.png" )}
|
||||
/>
|
||||
)}
|
||||
<Text className="color-inatGreen mr-2">
|
||||
{item.category ? t( `Category-${item.category}` ) : ""}
|
||||
</Text>
|
||||
{
|
||||
flaggedStatus
|
||||
? (
|
||||
<Text className="color-flaggedText mr-2">
|
||||
{t( "Flagged" )}
|
||||
</Text>
|
||||
)
|
||||
: (
|
||||
<Text className="color-inatGreen mr-2">
|
||||
{item.category ? t( `Category-${item.category}` ) : ""}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
{item.created_at
|
||||
&& (
|
||||
<Text>
|
||||
@@ -119,7 +151,13 @@ const ActivityItem = ( {
|
||||
visible={kebabMenuVisible}
|
||||
setVisible={setKebabMenuVisible}
|
||||
>
|
||||
{/* TODO: build out this menu */}
|
||||
{!currentUser ? (
|
||||
<Menu.Item
|
||||
onPress={() => setFlagModalVisible( true )}
|
||||
title={t( "Flag" )}
|
||||
testID="MenuItem.Flag"
|
||||
/>
|
||||
) : undefined}
|
||||
<View />
|
||||
</KebabMenu>
|
||||
)}
|
||||
@@ -150,6 +188,16 @@ const ActivityItem = ( {
|
||||
<UserText baseStyle={textStyles.activityItemBody} text={item.body} />
|
||||
</View>
|
||||
)}
|
||||
{!currentUser
|
||||
&& (
|
||||
<FlagItemModal
|
||||
id={item.id}
|
||||
showFlagItemModal={flagModalVisible}
|
||||
closeFlagItemModal={closeFlagItemModal}
|
||||
itemType={itemType}
|
||||
onItemFlagged={onItemFlagged}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
201
src/components/ObsDetails/FlagItemModal.js
Normal file
201
src/components/ObsDetails/FlagItemModal.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// @flow
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import createFlag from "api/flags";
|
||||
import Button from "components/SharedComponents/Buttons/Button";
|
||||
import {
|
||||
Modal,
|
||||
SafeAreaView,
|
||||
Text, View
|
||||
} from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
findNodeHandle
|
||||
} from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { TextInput } from "react-native-paper";
|
||||
import IconMaterial from "react-native-vector-icons/MaterialIcons";
|
||||
import useAuthenticatedMutation from "sharedHooks/useAuthenticatedMutation";
|
||||
|
||||
type Props = {
|
||||
id:number,
|
||||
itemType:string,
|
||||
showFlagItemModal: boolean,
|
||||
closeFlagItemModal: Function,
|
||||
onItemFlagged: Function
|
||||
}
|
||||
|
||||
const FlagItemModal = ( {
|
||||
id, itemType, showFlagItemModal, closeFlagItemModal, onItemFlagged
|
||||
}: Props ): Node => {
|
||||
const keyboardScrollRef = useRef( null );
|
||||
const [checkBoxValue, setCheckBoxValue] = useState( "none" );
|
||||
const [explanation, setExplanation] = useState( "" );
|
||||
const [loading, setLoading] = useState( false );
|
||||
|
||||
const showErrorAlert = error => Alert.alert(
|
||||
"Error",
|
||||
error,
|
||||
[{ text: t( "OK" ) }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
|
||||
const scrollToInput = node => {
|
||||
keyboardScrollRef?.current?.scrollToFocusedInput( node );
|
||||
};
|
||||
|
||||
const toggleCheckBoxValue = checkbox => {
|
||||
if ( checkBoxValue === checkbox ) {
|
||||
setCheckBoxValue( "none" );
|
||||
} else { setCheckBoxValue( checkbox ); }
|
||||
};
|
||||
|
||||
const resetFlagModal = () => {
|
||||
setCheckBoxValue( "none" );
|
||||
setExplanation( "" );
|
||||
closeFlagItemModal();
|
||||
setLoading( false );
|
||||
};
|
||||
|
||||
const createFlagMutation = useAuthenticatedMutation(
|
||||
( params, optsWithAuth ) => createFlag( params, optsWithAuth ),
|
||||
{
|
||||
onSuccess: data => {
|
||||
resetFlagModal();
|
||||
onItemFlagged( data );
|
||||
},
|
||||
onError: error => {
|
||||
setLoading( false );
|
||||
showErrorAlert( error );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const submitFlag = () => {
|
||||
if ( checkBoxValue !== "none" ) {
|
||||
let params = {
|
||||
flag: {
|
||||
flaggable_type: itemType,
|
||||
flaggable_id: id,
|
||||
flag: checkBoxValue
|
||||
|
||||
}
|
||||
};
|
||||
if ( checkBoxValue === "other" ) {
|
||||
params = { ...params, flag_explanation: explanation };
|
||||
}
|
||||
setLoading( true );
|
||||
createFlagMutation.mutate( params );
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={showFlagItemModal}
|
||||
animationType="slide"
|
||||
className="flex-1"
|
||||
testID="FlagItemModal"
|
||||
>
|
||||
<SafeAreaView className="flex-1">
|
||||
<View className="flex-row-reverse justify-between p-6 border-b">
|
||||
<IconMaterial name="close" onPress={closeFlagItemModal} size={30} />
|
||||
<Text className="text-xl">
|
||||
{t( "Flag-An-Item" )}
|
||||
</Text>
|
||||
</View>
|
||||
<KeyboardAwareScrollView
|
||||
ref={keyboardScrollRef}
|
||||
enableOnAndroid
|
||||
enableAutomaticScroll
|
||||
extraHeight={200}
|
||||
className="p-6"
|
||||
>
|
||||
<Text className="text-base">
|
||||
{t( "Flag-Item-Description" )}
|
||||
</Text>
|
||||
<View className="flex-row my-2">
|
||||
<CheckBox
|
||||
disabled={false}
|
||||
value={checkBoxValue === "spam"}
|
||||
onValueChange={() => toggleCheckBoxValue( "spam" )}
|
||||
/>
|
||||
<Text
|
||||
className="font-bold text-lg ml-5"
|
||||
onPress={() => toggleCheckBoxValue( "spam" )}
|
||||
>
|
||||
{t( "Spam" )}
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="mb-2 text-base" style>{t( "Spam-Examples" )}</Text>
|
||||
|
||||
<View className="flex-row my-2">
|
||||
<CheckBox
|
||||
disabled={false}
|
||||
value={checkBoxValue === "inappropriate"}
|
||||
onValueChange={() => toggleCheckBoxValue( "inappropriate" )}
|
||||
/>
|
||||
<Text
|
||||
className="font-bold text-lg ml-5"
|
||||
onPress={() => toggleCheckBoxValue( "inappropriate" )}
|
||||
>
|
||||
{t( "Offensive-Inappropriate" )}
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="mb-2 text-base">{t( "Offensive-Inappropriate-Examples" )}</Text>
|
||||
|
||||
<View className="flex-row my-2">
|
||||
<CheckBox
|
||||
disabled={false}
|
||||
value={checkBoxValue === "other"}
|
||||
onValueChange={() => toggleCheckBoxValue( "other" )}
|
||||
/>
|
||||
<Text
|
||||
className="font-bold text-lg ml-5"
|
||||
onPress={() => toggleCheckBoxValue( "other" )}
|
||||
>
|
||||
{t( "Other" )}
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="mb-2 text-base">{t( "Flag-Item-Other-Description" )}</Text>
|
||||
{( checkBoxValue === "other" )
|
||||
&& (
|
||||
<>
|
||||
<TextInput
|
||||
className="text-sm"
|
||||
placeholder={t( "Flag-Item-Other-Input-Hint" )}
|
||||
value={explanation}
|
||||
onChangeText={text => setExplanation( text )}
|
||||
onFocus={e => scrollToInput( findNodeHandle( e.target ) )}
|
||||
/>
|
||||
<Text>{`${explanation.length}/255`}</Text>
|
||||
</>
|
||||
)}
|
||||
<View className="flex-row justify-center m-4">
|
||||
<Button
|
||||
className="rounded m-2"
|
||||
text={t( "Cancel" )}
|
||||
onPress={() => resetFlagModal()}
|
||||
/>
|
||||
<Button
|
||||
className="rounded m-2"
|
||||
text={t( "Save" )}
|
||||
onPress={submitFlag}
|
||||
level="primary"
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
|
||||
);
|
||||
};
|
||||
export default FlagItemModal;
|
||||
@@ -169,6 +169,16 @@ const ObsDetails = ( ): Node => {
|
||||
} );
|
||||
};
|
||||
|
||||
// reload if change to observation
|
||||
useEffect( () => {
|
||||
if ( localObservation && remoteObservation ) {
|
||||
const remoteUpdatedAt = new Date( remoteObservation?.updated_at );
|
||||
if ( remoteUpdatedAt > localObservation?.updated_at ) {
|
||||
Observation.upsertRemoteObservations( [remoteObservation], realm );
|
||||
}
|
||||
}
|
||||
}, [localObservation, remoteObservation, realm] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( localObservation && !localObservation.viewed && !markViewedMutation.isLoading ) {
|
||||
markViewedMutation.mutate( { id: uuid } );
|
||||
|
||||
@@ -158,6 +158,14 @@ Finish = Finish
|
||||
|
||||
Fish = Fish
|
||||
|
||||
Flag-An-Item = Flag An Item
|
||||
|
||||
Flag-Item-Description = Flagging brings something to the attention of volunteer site curators. Please don't flag problems you can address with identifications, the Data Quality Assessment, or by talking to the person who made the content.
|
||||
|
||||
Flag-Item-Other-Description = Some other reason you can explain below.
|
||||
|
||||
Flag-Item-Other-Input-Hint = Specify the reason you're flagging this item
|
||||
|
||||
Following = Following
|
||||
|
||||
# Forgot password link
|
||||
@@ -329,6 +337,10 @@ Observation-Attribution = Observation © {$attribution} · {$licenseCode}
|
||||
|
||||
Observations = Observations
|
||||
|
||||
Offensive-Inappropriate = Offensive/Inappropriate
|
||||
|
||||
Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our defintion of "appropriate," see the FAQ.
|
||||
|
||||
Open = Open
|
||||
|
||||
# Picker prompt on observation edit
|
||||
@@ -521,6 +533,10 @@ Sort-By = Sort By
|
||||
|
||||
Sort-by = Sort by
|
||||
|
||||
Spam = Spam
|
||||
|
||||
Spam-Examples = Commercial solicitation, links to nowhere, etc.
|
||||
|
||||
Species = Species
|
||||
|
||||
Status = Status
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
"Filters": "Filters",
|
||||
"Finish": "Finish",
|
||||
"Fish": "Fish",
|
||||
"Flag-An-Item": "Flag An Item",
|
||||
"Flag-Item-Description": "Flagging brings something to the attention of volunteer site curators. Please don't flag problems you can address with identifications, the Data Quality Assessment, or by talking to the person who made the content.",
|
||||
"Flag-Item-Other-Description": "Some other reason you can explain below.",
|
||||
"Flag-Item-Other-Input-Hint": "Specify the reason you're flagging this item",
|
||||
"Following": "Following",
|
||||
"Forgot-Password": {
|
||||
"comment": "Forgot password link",
|
||||
@@ -232,6 +236,8 @@
|
||||
"Observation": "Observation",
|
||||
"Observation-Attribution": "Observation © { $attribution } · { $licenseCode }",
|
||||
"Observations": "Observations",
|
||||
"Offensive-Inappropriate": "Offensive/Inappropriate",
|
||||
"Offensive-Inappropriate-Examples": "Misleading or illegal content, racial or ethnic slurs, etc. For more on our defintion of \"appropriate,\" see the FAQ.",
|
||||
"Open": "Open",
|
||||
"Organism-is-wild": {
|
||||
"comment": "Picker prompt on observation edit",
|
||||
@@ -353,6 +359,8 @@
|
||||
},
|
||||
"Sort-By": "Sort By",
|
||||
"Sort-by": "Sort by",
|
||||
"Spam": "Spam",
|
||||
"Spam-Examples": "Commercial solicitation, links to nowhere, etc.",
|
||||
"Species": "Species",
|
||||
"Status": "Status",
|
||||
"STATUS-header": {
|
||||
|
||||
@@ -158,6 +158,14 @@ Finish = Finish
|
||||
|
||||
Fish = Fish
|
||||
|
||||
Flag-An-Item = Flag An Item
|
||||
|
||||
Flag-Item-Description = Flagging brings something to the attention of volunteer site curators. Please don't flag problems you can address with identifications, the Data Quality Assessment, or by talking to the person who made the content.
|
||||
|
||||
Flag-Item-Other-Description = Some other reason you can explain below.
|
||||
|
||||
Flag-Item-Other-Input-Hint = Specify the reason you're flagging this item
|
||||
|
||||
Following = Following
|
||||
|
||||
# Forgot password link
|
||||
@@ -329,6 +337,10 @@ Observation-Attribution = Observation © {$attribution} · {$licenseCode}
|
||||
|
||||
Observations = Observations
|
||||
|
||||
Offensive-Inappropriate = Offensive/Inappropriate
|
||||
|
||||
Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our defintion of "appropriate," see the FAQ.
|
||||
|
||||
Open = Open
|
||||
|
||||
# Picker prompt on observation edit
|
||||
@@ -521,6 +533,10 @@ Sort-By = Sort By
|
||||
|
||||
Sort-by = Sort by
|
||||
|
||||
Spam = Spam
|
||||
|
||||
Spam-Examples = Commercial solicitation, links to nowhere, etc.
|
||||
|
||||
Species = Species
|
||||
|
||||
Status = Status
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Realm } from "@realm/react";
|
||||
|
||||
import Flag from "./Flag";
|
||||
import User from "./User";
|
||||
|
||||
class Comment extends Realm.Object {
|
||||
@@ -7,6 +8,7 @@ class Comment extends Realm.Object {
|
||||
uuid: true,
|
||||
body: true,
|
||||
created_at: true,
|
||||
flags: Flag.FLAG_FIELDS,
|
||||
id: true,
|
||||
user: User && User.USER_FIELDS
|
||||
};
|
||||
@@ -31,6 +33,7 @@ class Comment extends Realm.Object {
|
||||
uuid: "string",
|
||||
body: "string?",
|
||||
created_at: { type: "string?", mapTo: "createdAt" },
|
||||
flags: "Flag[]",
|
||||
id: "int?",
|
||||
user: "User?",
|
||||
// this creates an inverse relationship so comments
|
||||
|
||||
39
src/realmModels/Flag.js
Normal file
39
src/realmModels/Flag.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Realm } from "@realm/react";
|
||||
|
||||
// import User from "./User";
|
||||
|
||||
class Flag extends Realm.Object {
|
||||
static FLAG_FIELDS = {
|
||||
id: true,
|
||||
comment: true,
|
||||
created_at: true,
|
||||
flag: true,
|
||||
flaggable_content: true,
|
||||
flaggable_id: true,
|
||||
flaggable_type: true,
|
||||
resolved: true,
|
||||
uuid: true
|
||||
};
|
||||
|
||||
static mapApiToRealm( flag ) {
|
||||
return flag;
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Flag",
|
||||
primaryKey: "uuid",
|
||||
properties: {
|
||||
created_at: { type: "string?", mapTo: "createdAt" },
|
||||
id: "int",
|
||||
comment: "string?",
|
||||
flag: "string",
|
||||
flaggable_content: "string?",
|
||||
flaggable_id: "int?",
|
||||
flaggable_type: "string?",
|
||||
resolved: "bool",
|
||||
uuid: "string?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Flag;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Realm } from "@realm/react";
|
||||
|
||||
import Flag from "./Flag";
|
||||
import Taxon from "./Taxon";
|
||||
import User from "./User";
|
||||
|
||||
@@ -10,6 +11,8 @@ class Identification extends Realm.Object {
|
||||
created_at: true,
|
||||
current: true,
|
||||
disagreement: true,
|
||||
id: true,
|
||||
flags: Flag.FLAG_FIELDS,
|
||||
taxon: Taxon.TAXON_FIELDS,
|
||||
updated_at: true,
|
||||
// $FlowFixMe
|
||||
@@ -22,6 +25,7 @@ class Identification extends Realm.Object {
|
||||
return {
|
||||
...id,
|
||||
createdAt: id.created_at,
|
||||
flags: id.flags.length > 0 ? Flag.mapApiToRealm( id.flags ) : [],
|
||||
taxon: Taxon.mapApiToRealm( id.taxon ),
|
||||
user: User.mapApiToRealm( id.user )
|
||||
};
|
||||
@@ -30,6 +34,7 @@ class Identification extends Realm.Object {
|
||||
static mapApiToRealm( id, realm ) {
|
||||
const newId = {
|
||||
...id,
|
||||
flags: Flag.mapApiToRealm( id.flags ),
|
||||
taxon: Taxon.mapApiToRealm( id.taxon ),
|
||||
user: User.mapApiToRealm( id.user, realm )
|
||||
};
|
||||
@@ -44,6 +49,8 @@ class Identification extends Realm.Object {
|
||||
body: "string?",
|
||||
category: "string?",
|
||||
created_at: { type: "string?", mapTo: "createdAt" },
|
||||
flags: "Flag[]",
|
||||
id: "int?",
|
||||
taxon: "Taxon?",
|
||||
user: "User?",
|
||||
vision: "bool?",
|
||||
|
||||
@@ -36,7 +36,8 @@ class Observation extends Realm.Object {
|
||||
quality_grade: true,
|
||||
taxon: Taxon.TAXON_FIELDS,
|
||||
time_observed_at: true,
|
||||
user: User && User.USER_FIELDS
|
||||
user: User && User.USER_FIELDS,
|
||||
updated_at: true
|
||||
}
|
||||
|
||||
static async new( obs ) {
|
||||
@@ -422,6 +423,7 @@ class Observation extends Realm.Object {
|
||||
// only by changing observed_on_string
|
||||
time_observed_at: { type: "string?", mapTo: "timeObservedAt" },
|
||||
user: "User?",
|
||||
updated_at: "date?",
|
||||
viewed: "bool?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import RNFS from "react-native-fs";
|
||||
|
||||
import Application from "./Application";
|
||||
import Comment from "./Comment";
|
||||
import Flag from "./Flag";
|
||||
import Identification from "./Identification";
|
||||
import Observation from "./Observation";
|
||||
import ObservationPhoto from "./ObservationPhoto";
|
||||
@@ -14,6 +15,7 @@ export default {
|
||||
schema: [
|
||||
Application,
|
||||
Comment,
|
||||
Flag,
|
||||
Identification,
|
||||
Observation,
|
||||
ObservationPhoto,
|
||||
@@ -22,9 +24,19 @@ export default {
|
||||
Taxon,
|
||||
User
|
||||
],
|
||||
schemaVersion: 31,
|
||||
schemaVersion: 32,
|
||||
path: `${RNFS.DocumentDirectoryPath}/db.realm`,
|
||||
migration: ( oldRealm, newRealm ) => {
|
||||
if ( oldRealm.schemaVersion < 32 ) {
|
||||
const oldObservations = oldRealm.objects( "Observation" );
|
||||
const newObservations = newRealm.objects( "Observation" );
|
||||
oldObservations.keys( ).forEach( objectIndex => {
|
||||
const oldObservation = oldObservations[objectIndex];
|
||||
const newObservation = newObservations[objectIndex];
|
||||
newObservation.updated_at = oldObservation.created_at;
|
||||
} );
|
||||
}
|
||||
|
||||
// Apparently you need to migrate when making a property optional
|
||||
if ( oldRealm.schemaVersion < 31 ) {
|
||||
const oldTaxa = oldRealm.objects( "Taxon" );
|
||||
@@ -55,6 +67,7 @@ export default {
|
||||
if ( oldRealm.schemaVersion < 29 ) {
|
||||
const oldTaxa = oldRealm.objects( "Taxon" );
|
||||
const newTaxa = newRealm.objects( "Taxon" );
|
||||
|
||||
// loop through all objects and set the new property in the new schema
|
||||
oldTaxa.keys( ).forEach( objectIndex => {
|
||||
const oldTaxon = oldTaxa[objectIndex];
|
||||
|
||||
@@ -85,7 +85,9 @@ module.exports = {
|
||||
buttonPrimaryDisabled: PRIMARY_DISABLED,
|
||||
buttonWarningDisabled: WARNING_DISABLED,
|
||||
buttonNeutralDisabled: NEUTRAL_DISABLED,
|
||||
selectionGreen: "#C1FF00"
|
||||
selectionGreen: "#C1FF00",
|
||||
flaggedBackground: "#fcf8e3",
|
||||
flaggedText: "#8a6d3a"
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
|
||||
121
tests/unit/components/ObsDetails/Flags.test.js
Normal file
121
tests/unit/components/ObsDetails/Flags.test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import ActivityItem from "components/ObsDetails/ActivityItem";
|
||||
import FlagItemModal from "components/ObsDetails/FlagItemModal";
|
||||
import React from "react";
|
||||
import { Provider as PaperProvider } from "react-native-paper";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
jest.useFakeTimers( );
|
||||
const mockCallback = jest.fn();
|
||||
const mockObservation = factory( "LocalObservation", {
|
||||
created_at: "2022-11-27T19:07:41-08:00",
|
||||
time_observed_at: "2023-12-14T21:07:41-09:30"
|
||||
} );
|
||||
|
||||
const mockIdentification = factory( "RemoteIdentification" );
|
||||
|
||||
jest.mock( "sharedHooks/useIsConnected" );
|
||||
|
||||
jest.mock( "sharedHelpers/dateAndTime", ( ) => ( {
|
||||
__esModule: true,
|
||||
formatIdDate: jest.fn()
|
||||
} ) );
|
||||
|
||||
jest.mock( "providers/contexts", ( ) => ( {
|
||||
__esModule: true,
|
||||
RealmContext: {
|
||||
useRealm: jest.fn()
|
||||
}
|
||||
} ) );
|
||||
|
||||
// TODO if/when we test mutation behavior, the mutation will need to be mocked
|
||||
// so it actually does something, or we need to take a different approach
|
||||
const mockMutate = jest.fn();
|
||||
jest.mock( "sharedHooks/useAuthenticatedMutation", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
mutate: mockMutate
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/components/LoginSignUp/AuthenticationService", ( ) => ( {
|
||||
getUserId: ( ) => mockObservation.user.id,
|
||||
isCurrentUser: ( ) => false
|
||||
} ) );
|
||||
|
||||
jest.mock( "react-native-keyboard-aware-scroll-view", () => {
|
||||
const KeyboardAwareScrollView = require( "react-native" ).ScrollView;
|
||||
return { KeyboardAwareScrollView };
|
||||
} );
|
||||
|
||||
test( "renders activity item with Flag Button", async ( ) => {
|
||||
renderComponent(
|
||||
<PaperProvider>
|
||||
<ActivityItem item={mockIdentification} />
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", false );
|
||||
|
||||
fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
|
||||
expect( screen.getByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Flag" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders Flag Modal when Flag button pressed", async ( ) => {
|
||||
renderComponent(
|
||||
<PaperProvider>
|
||||
<ActivityItem item={mockIdentification} />
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", false );
|
||||
|
||||
fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
|
||||
expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
fireEvent.press( await screen.findByTestId( "MenuItem.Flag" ) );
|
||||
expect( screen.queryByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", true );
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders Flag Modal content", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Other" ) ).toBeTruthy( );
|
||||
expect( screen.getAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
} );
|
||||
|
||||
test( "calls flag api when save button pressed", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
fireEvent.press( screen.queryByText( "Spam" ) );
|
||||
expect( screen.getByText( "Save" ) ).toBeTruthy( );
|
||||
fireEvent.press( screen.queryByText( "Save" ) );
|
||||
expect( await mockMutate ).toHaveBeenCalled();
|
||||
} );
|
||||
Reference in New Issue
Block a user