mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Remove unused code and packages, add eslint package for TS, suppress TS errors (#2924)
This commit is contained in:
committed by
GitHub
parent
29c0696a88
commit
95151e065b
36
.eslintrc.js
36
.eslintrc.js
@@ -9,8 +9,6 @@ module.exports = {
|
||||
},
|
||||
extends: [
|
||||
"airbnb",
|
||||
// This was added to the RN0.72 template, but it does not work with our current setup
|
||||
// "@react-native",
|
||||
"plugin:i18next/recommended",
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended",
|
||||
"plugin:react-native-a11y/ios",
|
||||
@@ -74,17 +72,6 @@ module.exports = {
|
||||
"no-underscore-dangle": 0,
|
||||
// This gets around eslint problems when typing functions in TS
|
||||
"no-unused-vars": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
vars: "all",
|
||||
args: "after-used",
|
||||
// Overriding airbnb to allow leading underscore to indicate unused var
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
"no-void": 0,
|
||||
"prefer-destructuring": [2, { object: true, array: false }],
|
||||
quotes: [2, "double"],
|
||||
@@ -133,15 +120,34 @@ module.exports = {
|
||||
"react-native-a11y/has-valid-accessibility-live-region": 1,
|
||||
"react-native-a11y/has-valid-important-for-accessibility": 1,
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": "error",
|
||||
|
||||
// it's supposedly safe to remove no-undef because TS's compiler handles
|
||||
// this, but I'm bumping into this error a lot in VSCode - 20240624 amanda
|
||||
// https://eslint.org/docs/latest/rules/no-undef#handled_by_typescript
|
||||
"no-undef": "error",
|
||||
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
args: "after-used",
|
||||
// Overriding airbnb to allow leading underscore to indicate unused var
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
// TODO: we should actually type these at some point ~amanda 041824
|
||||
"@typescript-eslint/no-shadow": "error",
|
||||
"@typescript-eslint/ban-types": 0,
|
||||
"@typescript-eslint/no-var-requires": 0
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/no-wrapper-object-types": 0,
|
||||
"@typescript-eslint/no-require-imports": 0,
|
||||
"@typescript-eslint/no-unsafe-function-type": 0,
|
||||
"@typescript-eslint/no-empty-object-types": 0,
|
||||
"@typescript-eslint/no-empty-object-type": 0,
|
||||
"@typescript-eslint/no-explicit-any": 1,
|
||||
"@typescript-eslint/no-unused-expressions": 1
|
||||
},
|
||||
// need this so jest doesn't show as undefined in jest.setup.js
|
||||
env: {
|
||||
|
||||
9
index.js
9
index.js
@@ -29,7 +29,6 @@ import { Alert, AppRegistry } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import { setJSExceptionHandler, setNativeExceptionHandler } from "react-native-exception-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { startNetworkLogging } from "react-native-network-logger";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { getInstallID } from "sharedHelpers/installData.ts";
|
||||
import { reactQueryRetry } from "sharedHelpers/logging";
|
||||
@@ -113,19 +112,13 @@ setNativeExceptionHandler(
|
||||
logger.error( `Native Error: ${exceptionString}`, crashData );
|
||||
} catch ( e ) {
|
||||
// Last-ditch attempt to log something
|
||||
logger.error( `Native Error: ${exceptionString} (failed to save context)` );
|
||||
logger.error( `Native Error: ${exceptionString} (failed to save context)`, e );
|
||||
}
|
||||
},
|
||||
true, // Force quit the app to prevent zombie states
|
||||
true // Enable on iOS
|
||||
);
|
||||
|
||||
// Only in debug builds
|
||||
// eslint-disable-next-line no-undef
|
||||
if ( __DEV__ ) {
|
||||
startNetworkLogging();
|
||||
}
|
||||
|
||||
initI18next();
|
||||
|
||||
// Configure inatjs to use the chosen URLs
|
||||
|
||||
1237
package-lock.json
generated
1237
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -111,7 +111,6 @@
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-modal": "^13.0.1",
|
||||
"react-native-modal-datetime-picker": "^17.1.0",
|
||||
"react-native-network-logger": "^1.15.0",
|
||||
"react-native-open-maps": "^0.4.3",
|
||||
"react-native-orientation-locker": "github:wonday/react-native-orientation-locker",
|
||||
"react-native-paper": "^5.12.3",
|
||||
@@ -135,19 +134,16 @@
|
||||
"react-native-worklets-core": "^1.3.3",
|
||||
"realm": "^20.1.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
"uuid": "^11.0.5",
|
||||
"vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#924a801b224be6243679c8237fbe0ec58ece0ac9",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@babel/preset-typescript": "^7.24.1",
|
||||
"@babel/runtime": "^7.24.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@fluent/syntax": "^0.19.0",
|
||||
"@react-native/babel-preset": "0.73.21",
|
||||
"@react-native/eslint-config": "0.73.2",
|
||||
"@react-native/metro-config": "0.73.5",
|
||||
"@react-native/typescript-config": "0.73.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.28.11",
|
||||
@@ -161,6 +157,7 @@
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "^18.0.7",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"babel-jest": "^29.6.3",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||
@@ -176,6 +173,7 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-module-resolver": "^1.5.0",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"eslint-plugin-react-native-a11y": "^3.5.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"eslint-plugin-testing-library": "^6.2.0",
|
||||
@@ -212,4 +210,4 @@
|
||||
"bundle exec rubocop --force-exclusion --parallel"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "all"
|
||||
};
|
||||
|
||||
const createFlag = async (
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<?Object> => {
|
||||
try {
|
||||
const { results } = await inatjs.flags.create( { ...PARAMS, ...params }, opts );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e, { context: { functionName: "createFlag", opts } } );
|
||||
}
|
||||
};
|
||||
|
||||
export default createFlag;
|
||||
@@ -1,16 +0,0 @@
|
||||
// Wrapper around things that should only be visible in debug mode
|
||||
import { View } from "components/styledComponents";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { useDebugMode } from "sharedHooks";
|
||||
|
||||
const Debug = ( { children }: PropsWithChildren ) => {
|
||||
const { isDebug } = useDebugMode( );
|
||||
if ( !isDebug ) return null;
|
||||
return (
|
||||
<View className="bg-deeppink">
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Debug;
|
||||
@@ -115,11 +115,6 @@ const Developer = (): Node => {
|
||||
text="UI LIBRARY"
|
||||
className="mb-5"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate( "network" )}
|
||||
text="NETWORK"
|
||||
className="mb-5"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => { throw new Error( "Test error" ); }}
|
||||
text="TEST ERROR"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import ViewWrapper from "components/SharedComponents/ViewWrapper";
|
||||
import React from "react";
|
||||
import NetworkLogger from "react-native-network-logger";
|
||||
|
||||
const NetworkLogging = () => {
|
||||
// eslint-disable-next-line no-undef
|
||||
if ( !__DEV__ ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NetworkLogger />
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkLogging;
|
||||
@@ -1,40 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { ActivityIndicator } from "components/SharedComponents";
|
||||
import * as React from "react";
|
||||
import { FlatList, Text } from "react-native";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
messageList: Array<Object>,
|
||||
testID: string
|
||||
}
|
||||
|
||||
const MessageList = ( {
|
||||
loading,
|
||||
messageList,
|
||||
testID
|
||||
}: Props ): React.Node => {
|
||||
if ( loading ) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
testID="Messages.activityIndicator"
|
||||
size={25}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderMessages = ( { item } ) => (
|
||||
<Text>{item.subject}</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={messageList}
|
||||
renderItem={renderMessages}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageList;
|
||||
@@ -1,66 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import searchMessages from "api/messages.ts";
|
||||
import { Body3, Tabs, ViewWrapper } from "components/SharedComponents";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import useCurrentUser from "sharedHooks/useCurrentUser.ts";
|
||||
|
||||
import MessageList from "./MessageList";
|
||||
|
||||
const NOTIFICATIONS_ID = "NOTIFICATIONS";
|
||||
const MESSAGES_ID = "MESSAGES";
|
||||
|
||||
const Messages = (): Node => {
|
||||
const currentUser = useCurrentUser();
|
||||
const [activeTab, setActiveTab] = useState( NOTIFICATIONS_ID );
|
||||
const { data, isLoading } = useAuthenticatedQuery(
|
||||
["searchMessages"],
|
||||
optsWithAuth => searchMessages( { page: 1 }, optsWithAuth ),
|
||||
{
|
||||
enabled: !!currentUser
|
||||
}
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: NOTIFICATIONS_ID,
|
||||
text: "Notifications",
|
||||
onPress: () => {
|
||||
setActiveTab( NOTIFICATIONS_ID );
|
||||
}
|
||||
},
|
||||
{
|
||||
id: MESSAGES_ID,
|
||||
text: "Messages",
|
||||
onPress: () => {
|
||||
setActiveTab( MESSAGES_ID );
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{currentUser
|
||||
? (
|
||||
<>
|
||||
<Tabs tabs={tabs} activeId={activeTab} />
|
||||
<MessageList
|
||||
loading={isLoading}
|
||||
messageList={data}
|
||||
testID="Messages.messages"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<Body3 className="self-center">
|
||||
{t( "You-must-be-logged-in-to-view-messages" )}
|
||||
</Body3>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Messages;
|
||||
@@ -1,182 +0,0 @@
|
||||
// @flow
|
||||
import createFlag from "api/flags";
|
||||
import {
|
||||
BackButton,
|
||||
Body1, Body3, Button, Checkbox, Subheading1
|
||||
} from "components/SharedComponents";
|
||||
import {
|
||||
Modal,
|
||||
SafeAreaView,
|
||||
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 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">
|
||||
<BackButton onPress={closeFlagItemModal} />
|
||||
<Subheading1 className="text-xl">
|
||||
{t( "Flag-An-Item" )}
|
||||
</Subheading1>
|
||||
</View>
|
||||
<KeyboardAwareScrollView
|
||||
ref={keyboardScrollRef}
|
||||
enableOnAndroid
|
||||
enableAutomaticScroll
|
||||
extraHeight={200}
|
||||
className="p-6"
|
||||
>
|
||||
<Body1 className="text-base">
|
||||
{t( "Flag-Item-Description" )}
|
||||
</Body1>
|
||||
<View className="flex-row my-2">
|
||||
<Checkbox
|
||||
isChecked={checkBoxValue === "spam"}
|
||||
onValueChange={() => toggleCheckBoxValue( "spam" )}
|
||||
text={t( "Spam" )}
|
||||
/>
|
||||
</View>
|
||||
<Body1 className="mb-2 text-base" style>{t( "Spam-Examples" )}</Body1>
|
||||
|
||||
<View className="flex-row my-2">
|
||||
<Checkbox
|
||||
isChecked={checkBoxValue === "inappropriate"}
|
||||
onValueChange={() => toggleCheckBoxValue( "inappropriate" )}
|
||||
text={t( "Offensive-Inappropriate" )}
|
||||
/>
|
||||
</View>
|
||||
<Body1 className="mb-2 text-base">{t( "Offensive-Inappropriate-Examples" )}</Body1>
|
||||
|
||||
<View className="flex-row my-2">
|
||||
<Checkbox
|
||||
isChecked={checkBoxValue === "other"}
|
||||
onValueChange={() => toggleCheckBoxValue( "other" )}
|
||||
text={t( "Other" )}
|
||||
/>
|
||||
</View>
|
||||
<Body1 className="mb-2 text-base">{t( "Flag-Item-Other-Description" )}</Body1>
|
||||
{( 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 ) )}
|
||||
accessibilityLabel={t( "Reason--flag" )}
|
||||
/>
|
||||
<Body3>{`${explanation.length}/255`}</Body3>
|
||||
</>
|
||||
)}
|
||||
<View className="flex-row justify-center m-4">
|
||||
<Button
|
||||
className="m-2"
|
||||
text={t( "Cancel" )}
|
||||
onPress={() => resetFlagModal()}
|
||||
/>
|
||||
<Button
|
||||
className="m-2"
|
||||
text={t( "Save" )}
|
||||
onPress={submitFlag}
|
||||
level="primary"
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
|
||||
);
|
||||
};
|
||||
export default FlagItemModal;
|
||||
@@ -1,115 +0,0 @@
|
||||
// @flow
|
||||
import { createSubscription } from "api/observations";
|
||||
import KebabMenu from "components/SharedComponents/KebabMenu.tsx";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Alert, Platform, Share } from "react-native";
|
||||
import {
|
||||
useAuthenticatedMutation,
|
||||
useCurrentUser
|
||||
} from "sharedHooks";
|
||||
|
||||
const observationsUrl = "https://www.inaturalist.org/observations";
|
||||
|
||||
type Props = {
|
||||
observationId: number,
|
||||
white?: boolean,
|
||||
subscriptions: Object,
|
||||
uuid: string,
|
||||
refetchSubscriptions: Function
|
||||
}
|
||||
|
||||
const HeaderKebabMenu = ( {
|
||||
observationId,
|
||||
white = true,
|
||||
subscriptions,
|
||||
uuid,
|
||||
refetchSubscriptions
|
||||
}: Props ): Node => {
|
||||
const currentUser = useCurrentUser( );
|
||||
const [kebabMenuVisible, setKebabMenuVisible] = useState( false );
|
||||
|
||||
const url = `${observationsUrl}/${observationId?.toString( )}`;
|
||||
const sharingOptions = {
|
||||
url: "",
|
||||
message: ""
|
||||
};
|
||||
const isSubscribed = subscriptions?.length > 0;
|
||||
|
||||
if ( Platform.OS === "ios" ) {
|
||||
sharingOptions.url = url;
|
||||
} else {
|
||||
sharingOptions.message = url;
|
||||
}
|
||||
|
||||
const handleShare = async ( ) => {
|
||||
setKebabMenuVisible( false );
|
||||
try {
|
||||
return await Share.share( sharingOptions );
|
||||
} catch ( error ) {
|
||||
Alert.alert( error.message );
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSubscription = useAuthenticatedMutation(
|
||||
( params, optsWithAuth ) => createSubscription( params, optsWithAuth ),
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchSubscriptions();
|
||||
},
|
||||
onError: error => {
|
||||
Alert.alert( error.message );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const toggleSubscriptionOnPress = async ( ) => {
|
||||
setKebabMenuVisible( false );
|
||||
try {
|
||||
return await toggleSubscription.mutate( { uuid } );
|
||||
} catch ( error ) {
|
||||
Alert.alert( error.message );
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<KebabMenu
|
||||
visible={kebabMenuVisible}
|
||||
setVisible={setKebabMenuVisible}
|
||||
white={white}
|
||||
accessibilityLabel={t( "Observation-options" )}
|
||||
accessibilityHint={t( "Show-observation-options" )}
|
||||
large
|
||||
>
|
||||
<KebabMenu.Item
|
||||
isFirst
|
||||
onPress={handleShare}
|
||||
title={t( "Share" )}
|
||||
testID="MenuItem.Share"
|
||||
/>
|
||||
{!!currentUser && ( isSubscribed
|
||||
? (
|
||||
<KebabMenu.Item
|
||||
isFirst
|
||||
onPress={toggleSubscriptionOnPress}
|
||||
title={t( "Ignore-notifications" )}
|
||||
testID="MenuItem.IgnoreNotifications"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<KebabMenu.Item
|
||||
isFirst
|
||||
onPress={toggleSubscriptionOnPress}
|
||||
title={t( "Enable-notifications" )}
|
||||
testID="MenuItem.EnableNotifications"
|
||||
/>
|
||||
) )}
|
||||
|
||||
</KebabMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderKebabMenu;
|
||||
@@ -1,96 +0,0 @@
|
||||
// @flow
|
||||
import KebabMenu from "components/SharedComponents/KebabMenu.tsx";
|
||||
import {
|
||||
View
|
||||
} from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
current: boolean,
|
||||
currentUser: boolean,
|
||||
itemType: "Identification" | "Comment",
|
||||
setShowDeleteCommentSheet: Function,
|
||||
setShowEditCommentSheet: Function,
|
||||
setShowWithdrawIDSheet: Function,
|
||||
updateIdentification: Function,
|
||||
}
|
||||
|
||||
const ActivityItemKebabMenu = ( {
|
||||
current,
|
||||
currentUser,
|
||||
itemType,
|
||||
setShowDeleteCommentSheet,
|
||||
setShowEditCommentSheet,
|
||||
setShowWithdrawIDSheet,
|
||||
updateIdentification
|
||||
}:Props ): Node => {
|
||||
const [kebabMenuVisible, setKebabMenuVisible] = useState( false );
|
||||
|
||||
if ( !currentUser ) {
|
||||
// flags removed from mvp
|
||||
// placeholder for kebabmenu
|
||||
return <View className="h-[44px] mr-[15px]" />;
|
||||
}
|
||||
|
||||
if ( itemType === "Identification" ) {
|
||||
return (
|
||||
<KebabMenu
|
||||
visible={kebabMenuVisible}
|
||||
setVisible={setKebabMenuVisible}
|
||||
accessibilityLabel={t( "Identification-options" )}
|
||||
>
|
||||
{current === true
|
||||
? (
|
||||
<KebabMenu.Item
|
||||
isFirst
|
||||
onPress={async ( ) => {
|
||||
setShowWithdrawIDSheet( true );
|
||||
setKebabMenuVisible( false );
|
||||
}}
|
||||
title={t( "Withdraw" )}
|
||||
testID="MenuItem.Withdraw"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<KebabMenu.Item
|
||||
isFirst
|
||||
onPress={async ( ) => {
|
||||
updateIdentification( { current: true } );
|
||||
setKebabMenuVisible( false );
|
||||
}}
|
||||
title={t( "Restore" )}
|
||||
/>
|
||||
)}
|
||||
</KebabMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KebabMenu
|
||||
visible={kebabMenuVisible}
|
||||
setVisible={setKebabMenuVisible}
|
||||
accessibilityLabel={t( "Comment-options" )}
|
||||
>
|
||||
<KebabMenu.Item
|
||||
onPress={async ( ) => {
|
||||
setShowEditCommentSheet( true );
|
||||
setKebabMenuVisible( false );
|
||||
}}
|
||||
title={t( "Edit-comment" )}
|
||||
testID="MenuItem.EditComment"
|
||||
/>
|
||||
<KebabMenu.Item
|
||||
onPress={async ( ) => {
|
||||
setShowDeleteCommentSheet( true );
|
||||
setKebabMenuVisible( false );
|
||||
}}
|
||||
title={t( "Delete-comment" )}
|
||||
testID="MenuItem.DeleteComment"
|
||||
/>
|
||||
</KebabMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityItemKebabMenu;
|
||||
@@ -1,315 +0,0 @@
|
||||
// @flow
|
||||
import {
|
||||
refresh as refreshNetInfo,
|
||||
useNetInfo
|
||||
} from "@react-native-community/netinfo";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import { faveObservation, unfaveObservation } from "api/observations";
|
||||
import { deleteQualityMetric, fetchQualityMetrics, setQualityMetric } from "api/qualityMetrics";
|
||||
import DataQualityAssessment from "components/ObsDetails/DataQualityAssessment";
|
||||
import {
|
||||
BottomSheet,
|
||||
Button,
|
||||
List2
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import { compact, groupBy } from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import * as React from "react";
|
||||
import Observation from "realmModels/Observation";
|
||||
import { log } from "sharedHelpers/logger";
|
||||
import {
|
||||
useAuthenticatedMutation,
|
||||
useAuthenticatedQuery,
|
||||
useLocalObservation
|
||||
} from "sharedHooks";
|
||||
import useRemoteObservation from "sharedHooks/useRemoteObservation";
|
||||
|
||||
const logger = log.extend( "DQAContainer" );
|
||||
|
||||
const DQAContainer = ( ): React.Node => {
|
||||
const { isConnected } = useNetInfo( );
|
||||
const { params } = useRoute( );
|
||||
const { observationUUID } = params;
|
||||
const [loadingAgree, setLoadingAgree] = useState( false );
|
||||
const [loadingDisagree, setLoadingDisagree] = useState( false );
|
||||
const [loadingMetric, setLoadingMetric] = useState( "none" );
|
||||
const [hideErrorSheet, setHideErrorSheet] = useState( true );
|
||||
const [hideOfflineSheet, setHideOfflineSheet] = useState( true );
|
||||
|
||||
const localObservation = useLocalObservation( observationUUID );
|
||||
const fetchRemoteObservationEnabled = !localObservation || localObservation?.wasSynced();
|
||||
const {
|
||||
remoteObservation,
|
||||
refetchRemoteObservation,
|
||||
isRefetching
|
||||
} = useRemoteObservation( observationUUID, fetchRemoteObservationEnabled );
|
||||
const observation = remoteObservation
|
||||
? Observation.mapApiToRealm( remoteObservation )
|
||||
: localObservation;
|
||||
|
||||
const fetchMetricsParams = {
|
||||
id: observationUUID,
|
||||
fields: "metric,agree,user_id",
|
||||
ttl: -1
|
||||
};
|
||||
|
||||
const setNotLoading = useCallback( () => {
|
||||
setLoadingMetric( "none" );
|
||||
if ( loadingAgree ) {
|
||||
setLoadingAgree( false );
|
||||
}
|
||||
if ( loadingDisagree ) {
|
||||
setLoadingDisagree( false );
|
||||
}
|
||||
}, [loadingAgree, loadingDisagree] );
|
||||
|
||||
const setLoading = ( metric, vote ) => {
|
||||
setLoadingMetric( metric );
|
||||
if ( vote ) {
|
||||
setLoadingAgree( true );
|
||||
} else {
|
||||
setLoadingDisagree( true );
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: qualityMetrics,
|
||||
refetch: refetchQualityMetrics
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchQualityMetrics", observationUUID],
|
||||
optsWithAuth => fetchQualityMetrics( fetchMetricsParams, optsWithAuth )
|
||||
);
|
||||
|
||||
const combinedQualityMetrics = {
|
||||
...groupBy( qualityMetrics, "metric" ),
|
||||
...groupBy( observation?.votes, "vote_scope" )
|
||||
};
|
||||
|
||||
/**
|
||||
* After a success mutation of the needs_id vote we start the refetching of the remote
|
||||
* observation to update the metric status of the observation. So we need to wait until
|
||||
* the refetching is done to set the loading state to false.
|
||||
*/
|
||||
useEffect( () => {
|
||||
if ( !isRefetching ) {
|
||||
setNotLoading();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isRefetching] );
|
||||
|
||||
const faveMutation = useAuthenticatedMutation(
|
||||
( faveParams, optsWithAuth ) => faveObservation( faveParams, optsWithAuth ),
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchRemoteObservation();
|
||||
},
|
||||
onError: () => {
|
||||
setHideErrorSheet( false );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const unfaveMutation = useAuthenticatedMutation(
|
||||
( faveParams, optsWithAuth ) => unfaveObservation( faveParams, optsWithAuth ),
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchRemoteObservation();
|
||||
},
|
||||
onError: () => {
|
||||
setHideErrorSheet( false );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const createQualityMetricMutation = useAuthenticatedMutation(
|
||||
( qualityMetricParams, optsWithAuth ) => setQualityMetric( qualityMetricParams, optsWithAuth ),
|
||||
{
|
||||
onSuccess: async ( ) => {
|
||||
await refetchQualityMetrics( );
|
||||
await refetchRemoteObservation( );
|
||||
setNotLoading( );
|
||||
},
|
||||
onError: error => {
|
||||
logger.error( "createQualityMetricMutation failure", error );
|
||||
setHideErrorSheet( false );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const setMetricVote = ( { metric, vote } ) => {
|
||||
setLoading( metric, vote );
|
||||
|
||||
const qualityMetricParams = {
|
||||
id: observationUUID,
|
||||
metric,
|
||||
agree: vote,
|
||||
ttyl: -1
|
||||
};
|
||||
createQualityMetricMutation.mutate( qualityMetricParams );
|
||||
};
|
||||
|
||||
// The quality metric "needs_id" uses a fave/unfave vote with vote_scope: "needs_id"
|
||||
// as it's interaction with the API
|
||||
const setNeedsIDVote = ( { vote } ) => {
|
||||
setLoading( "needs_id", vote );
|
||||
|
||||
const faveParams = {
|
||||
id: observationUUID,
|
||||
scope: "needs_id",
|
||||
vote: vote === false
|
||||
? "no"
|
||||
: "yes"
|
||||
};
|
||||
faveMutation.mutate( faveParams );
|
||||
};
|
||||
|
||||
const removeQualityMetricMutation = useAuthenticatedMutation(
|
||||
( deleteParams, options ) => deleteQualityMetric( deleteParams, options ),
|
||||
{
|
||||
onSuccess: async ( ) => {
|
||||
await refetchQualityMetrics( );
|
||||
await refetchRemoteObservation( );
|
||||
setNotLoading( );
|
||||
},
|
||||
onError: error => {
|
||||
logger.error( "removeQualityMetricMutation failed", error );
|
||||
setHideErrorSheet( false );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const removeMetricVote = ( { metric, vote } ) => {
|
||||
setLoading( metric, vote );
|
||||
|
||||
const qualityMetricParams = {
|
||||
id: observationUUID,
|
||||
metric,
|
||||
ttyl: -1
|
||||
};
|
||||
removeQualityMetricMutation.mutate( qualityMetricParams );
|
||||
};
|
||||
|
||||
// The quality metric "needs_id" uses a fave/unfave vote with vote_scope: "needs_id"
|
||||
// as it's interaction with the API
|
||||
const removeNeedsIDVote = ( { vote } ) => {
|
||||
setLoading( "needs_id", vote );
|
||||
|
||||
const unfaveParams = {
|
||||
id: observationUUID,
|
||||
scope: "needs_id"
|
||||
};
|
||||
unfaveMutation.mutate( unfaveParams );
|
||||
};
|
||||
|
||||
const ifMajorityAgree = metric => {
|
||||
const votesOfMetric = combinedQualityMetrics[metric];
|
||||
if ( votesOfMetric && votesOfMetric.length > 0 ) {
|
||||
const agreeCount = votesOfMetric.filter(
|
||||
element => element.agree
|
||||
).length;
|
||||
const disagreeCount = votesOfMetric.filter(
|
||||
element => !element.agree
|
||||
).length;
|
||||
|
||||
return agreeCount >= disagreeCount;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkTest = metric => {
|
||||
if ( observation ) {
|
||||
const obsDataToCheck = {
|
||||
date: observation.observed_on,
|
||||
location: [observation.latitude, observation.longitude],
|
||||
evidence: compact( [
|
||||
observation.observationPhotos || observation.observation_photos,
|
||||
observation.observationSounds || observation.sounds
|
||||
] ),
|
||||
taxonId: observation.taxon?.id,
|
||||
rankLevel: observation.taxon?.rank_level,
|
||||
identifications: observation.identifications
|
||||
};
|
||||
if ( metric === "date" ) {
|
||||
return obsDataToCheck[metric] !== null;
|
||||
}
|
||||
if ( metric === "location" ) {
|
||||
const removedNull = obsDataToCheck[metric]
|
||||
.filter( value => value !== null );
|
||||
return removedNull.length !== 0;
|
||||
}
|
||||
if ( metric === "evidence" ) {
|
||||
const removedEmpty = obsDataToCheck[metric]
|
||||
.filter( value => Object.keys( value ).length !== 0 );
|
||||
return removedEmpty.length !== 0;
|
||||
}
|
||||
if ( metric === "id_supported" ) {
|
||||
const { taxonId } = obsDataToCheck;
|
||||
const supportedIDs = obsDataToCheck.identifications.filter(
|
||||
identification => identification.taxon.id === taxonId
|
||||
).length;
|
||||
return supportedIDs >= 2;
|
||||
}
|
||||
if ( metric === "rank" && obsDataToCheck.rankLevel <= 10 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const resetButtonsOnError = () => {
|
||||
setNotLoading();
|
||||
setHideErrorSheet( true );
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataQualityAssessment
|
||||
qualityMetrics={combinedQualityMetrics}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
qualityGrade={observation?.quality_grade}
|
||||
setMetricVote={setMetricVote}
|
||||
removeMetricVote={removeMetricVote}
|
||||
setNeedsIDVote={setNeedsIDVote}
|
||||
removeNeedsIDVote={removeNeedsIDVote}
|
||||
ifMajorityAgree={ifMajorityAgree}
|
||||
checkTest={checkTest}
|
||||
isConnected={isConnected}
|
||||
recheckisConnected={refreshNetInfo}
|
||||
/>
|
||||
<BottomSheet
|
||||
headerText={t( "ERROR-VOTING-IN-DQA" )}
|
||||
hidden={hideErrorSheet}
|
||||
hideCloseButton
|
||||
onPressClose={( ) => resetButtonsOnError( )}
|
||||
>
|
||||
<View className="px-[26px] py-[20px] flex-col space-y-[20px]">
|
||||
<List2 className="text-darkGray">{t( "Error-voting-in-DQA-description" )}</List2>
|
||||
<Button
|
||||
text={t( "OK" )}
|
||||
onPress={() => resetButtonsOnError( )}
|
||||
/>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
<BottomSheet
|
||||
headerText={t( "ERROR-LOADING-DQA" )}
|
||||
hidden={hideOfflineSheet}
|
||||
hideCloseButton
|
||||
onPressClose={( ) => setHideOfflineSheet( true )}
|
||||
>
|
||||
<View className="px-[26px] py-[20px] flex-col space-y-[20px]">
|
||||
<List2 className="text-darkGray">{t( "Offline-DQA-description" )}</List2>
|
||||
<Button
|
||||
text={t( "OK" )}
|
||||
onPress={() => setHideOfflineSheet( true )}
|
||||
/>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DQAContainer;
|
||||
@@ -1,317 +0,0 @@
|
||||
// @flow
|
||||
import DQAVoteButtons from "components/ObsDetails/DetailsTab/DQAVoteButtons";
|
||||
import {
|
||||
Body1,
|
||||
Body3,
|
||||
Divider,
|
||||
Heading4,
|
||||
INatIcon,
|
||||
List2,
|
||||
OfflineNotice,
|
||||
ScrollViewWrapper,
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
// eslint-disable-next-line max-len
|
||||
import QualityGradeStatus from "components/SharedComponents/QualityGradeStatus/QualityGradeStatus.tsx";
|
||||
import { View } from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
const titleOption = option => {
|
||||
switch ( option ) {
|
||||
case "research":
|
||||
return t( "Data-quality-assessment-title-research" );
|
||||
case "needs_id":
|
||||
return t( "Data-quality-assessment-title-needs-id" );
|
||||
default:
|
||||
return t( "Data-quality-assessment-title-casual" );
|
||||
}
|
||||
};
|
||||
|
||||
const titleDescription = option => {
|
||||
switch ( option ) {
|
||||
case "research":
|
||||
return t( "Data-quality-assessment-description-research" );
|
||||
case "needs_id":
|
||||
return t( "Data-quality-assessment-description-needs-id" );
|
||||
default:
|
||||
return t( "Data-quality-assessment-description-casual" );
|
||||
}
|
||||
};
|
||||
type Props = {
|
||||
checkTest: Function,
|
||||
ifMajorityAgree: Function,
|
||||
isConnected?: boolean,
|
||||
loadingAgree: boolean,
|
||||
loadingDisagree: boolean,
|
||||
loadingMetric: string,
|
||||
qualityGrade: string,
|
||||
qualityMetrics: Object,
|
||||
recheckisConnected: Function,
|
||||
removeMetricVote: Function,
|
||||
removeNeedsIDVote: Function,
|
||||
setMetricVote: Function,
|
||||
setNeedsIDVote: Function,
|
||||
}
|
||||
|
||||
const DataQualityAssessment = ( {
|
||||
checkTest,
|
||||
ifMajorityAgree,
|
||||
isConnected,
|
||||
loadingAgree,
|
||||
loadingDisagree,
|
||||
loadingMetric,
|
||||
qualityGrade,
|
||||
qualityMetrics,
|
||||
recheckisConnected,
|
||||
removeMetricVote,
|
||||
removeNeedsIDVote,
|
||||
setMetricVote,
|
||||
setNeedsIDVote
|
||||
}: Props ): Node => {
|
||||
const isResearchGrade = qualityGrade === "research";
|
||||
const sectionClass = "flex-row my-[14px] space-x-[11px]";
|
||||
const voteClass = "flex-row mr-[15px] my-[7px] justify-between items-center";
|
||||
const listTextClass = "flex-row shrink space-x-[11px]";
|
||||
|
||||
const renderMetricIndicator = metric => {
|
||||
const ifAgree = ifMajorityAgree( metric );
|
||||
if ( ifAgree || ifAgree === null ) {
|
||||
return (
|
||||
<INatIcon
|
||||
testID="DQA.pass"
|
||||
name="checkmark-circle"
|
||||
size={19}
|
||||
color={colors.inatGreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<INatIcon
|
||||
name="triangle-exclamation"
|
||||
size={19}
|
||||
color={colors.warningRed}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderIndicator = metric => {
|
||||
const ifAgree = checkTest( metric );
|
||||
if ( ifAgree || ifAgree === null ) {
|
||||
return (
|
||||
<INatIcon name="checkmark-circle" size={19} color={colors.inatGreen} /> );
|
||||
}
|
||||
return (
|
||||
<INatIcon
|
||||
name="triangle-exclamation"
|
||||
size={19}
|
||||
color={colors.warningRed}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if ( isConnected === false ) {
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<OfflineNotice onPress={( ) => recheckisConnected( )} />
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// console.log( "[DEBUG DataQualityAssessment.js] qualityMetrics?.date: ", qualityMetrics?.date );
|
||||
|
||||
return (
|
||||
<ScrollViewWrapper testID="DataQualityAssessment">
|
||||
<View className="mx-[26px] my-[19px] space-y-[9px]">
|
||||
<QualityGradeStatus
|
||||
qualityGrade={qualityGrade}
|
||||
color={
|
||||
qualityGrade === "research"
|
||||
? colors.inatGreen
|
||||
: colors.darkGray
|
||||
}
|
||||
/>
|
||||
<View className="flex-row space-x-[7px]">
|
||||
{isResearchGrade && (
|
||||
<INatIcon
|
||||
name="checkmark-circle"
|
||||
size={19}
|
||||
color={colors.inatGreen}
|
||||
/>
|
||||
)}
|
||||
<Body1 className="text-darkGray">{titleOption( qualityGrade )}</Body1>
|
||||
</View>
|
||||
<List2 className="text-darkGray">{titleDescription( qualityGrade )}</List2>
|
||||
</View>
|
||||
<Divider />
|
||||
<View className="mx-[15px]">
|
||||
<View className={sectionClass}>
|
||||
{renderIndicator( "date" )}
|
||||
<Body3>{t( "Data-quality-assessment-date-specified" )}</Body3>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={sectionClass}>
|
||||
{renderIndicator( "location" )}
|
||||
<Body3>{t( "Data-quality-assessment-location-specified" )}</Body3>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={sectionClass}>
|
||||
{renderIndicator( "evidence" )}
|
||||
<Body3>{t( "Data-quality-assessment-has-photos-or-sounds" )}</Body3>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={sectionClass}>
|
||||
{renderIndicator( "id_supported" )}
|
||||
<Body3>
|
||||
{t( "Data-quality-assessment-id-supported-by-two-or-more" )}
|
||||
</Body3>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={sectionClass}>
|
||||
{renderIndicator( "rank" )}
|
||||
<Body3>
|
||||
{t(
|
||||
"Data-quality-assessment-community-taxon-species-level-or-lower"
|
||||
)}
|
||||
</Body3>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={voteClass}>
|
||||
<View className={listTextClass}>
|
||||
{renderMetricIndicator( "date" )}
|
||||
<Body3>{t( "Data-quality-assessment-date-is-accurate" )}</Body3>
|
||||
</View>
|
||||
<DQAVoteButtons
|
||||
metric="date"
|
||||
votes={qualityMetrics?.date}
|
||||
setVote={setMetricVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeMetricVote}
|
||||
/>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={voteClass}>
|
||||
<View className={listTextClass}>
|
||||
{renderMetricIndicator( "location" )}
|
||||
<Body3>{t( "Data-quality-assessment-location-is-accurate" )}</Body3>
|
||||
</View>
|
||||
<DQAVoteButtons
|
||||
metric="location"
|
||||
votes={qualityMetrics?.location}
|
||||
setVote={setMetricVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeMetricVote}
|
||||
/>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={voteClass}>
|
||||
<View className={listTextClass}>
|
||||
{renderMetricIndicator( "wild" )}
|
||||
<Body3>{t( "Data-quality-assessment-organism-is-wild" )}</Body3>
|
||||
</View>
|
||||
<DQAVoteButtons
|
||||
metric="wild"
|
||||
votes={qualityMetrics?.wild}
|
||||
setVote={setMetricVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeMetricVote}
|
||||
/>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={voteClass}>
|
||||
<View className={listTextClass}>
|
||||
{renderMetricIndicator( "evidence" )}
|
||||
<Body3>{t( "Data-quality-assessment-evidence-of-organism" )}</Body3>
|
||||
</View>
|
||||
<DQAVoteButtons
|
||||
metric="evidence"
|
||||
votes={qualityMetrics?.evidence}
|
||||
setVote={setMetricVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeMetricVote}
|
||||
/>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={voteClass}>
|
||||
<View className={listTextClass}>
|
||||
{renderMetricIndicator( "recent" )}
|
||||
<Body3>
|
||||
{t( "Data-quality-assessment-recent-evidence-of-organism" )}
|
||||
</Body3>
|
||||
</View>
|
||||
<DQAVoteButtons
|
||||
metric="recent"
|
||||
votes={qualityMetrics?.recent}
|
||||
setVote={setMetricVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeMetricVote}
|
||||
/>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
<View className={voteClass}>
|
||||
<View className={listTextClass}>
|
||||
{renderMetricIndicator( "subject" )}
|
||||
<Body3>{t( "Data-quality-assessment-single-subject" )}</Body3>
|
||||
</View>
|
||||
<DQAVoteButtons
|
||||
metric="subject"
|
||||
votes={qualityMetrics?.subject}
|
||||
setVote={setMetricVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeMetricVote}
|
||||
/>
|
||||
</View>
|
||||
<Divider />
|
||||
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center mt-5 py-2 pl-4 pr-[30px] bg-lightGray">
|
||||
<Body3 className="flex-1 mr-1">
|
||||
{t(
|
||||
"Data-quality-assessment-can-taxon-still-be-confirmed-improved-based-on-the-evidence"
|
||||
)}
|
||||
</Body3>
|
||||
<DQAVoteButtons
|
||||
metric="needs_id"
|
||||
votes={qualityMetrics?.needs_id}
|
||||
setVote={setNeedsIDVote}
|
||||
loadingAgree={loadingAgree}
|
||||
loadingDisagree={loadingDisagree}
|
||||
loadingMetric={loadingMetric}
|
||||
removeVote={removeNeedsIDVote}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="my-[30px] mx-[15px] space-y-[11px]">
|
||||
<Heading4>{t( "ABOUT-THE-DQA" )}</Heading4>
|
||||
<List2>{t( "About-the-DQA-description" )}</List2>
|
||||
</View>
|
||||
</ScrollViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataQualityAssessment;
|
||||
@@ -1,39 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
Body2,
|
||||
INatIcon
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import * as React from "react";
|
||||
import { getShadow } from "styles/global";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
const CoordinatesCopiedNotification = ( ): React.Node => (
|
||||
<View
|
||||
className={classnames(
|
||||
"flex",
|
||||
"flex-row",
|
||||
"justify-center",
|
||||
"items-center",
|
||||
"bottom-1/2",
|
||||
"bg-white",
|
||||
"p-3",
|
||||
"rounded-xl"
|
||||
)}
|
||||
style={getShadow( )}
|
||||
>
|
||||
<Body2 className="mr-3">
|
||||
{t( "Coordinates-copied-to-clipboard" )}
|
||||
</Body2>
|
||||
<INatIcon
|
||||
name="checkmark-circle"
|
||||
size={19}
|
||||
color={colors.inatGreen}
|
||||
/>
|
||||
</View>
|
||||
|
||||
);
|
||||
export default CoordinatesCopiedNotification;
|
||||
@@ -1,182 +0,0 @@
|
||||
// @flow
|
||||
import createFlag from "api/flags";
|
||||
import {
|
||||
BackButton,
|
||||
Body1, Body3, Button, Checkbox, Subheading1
|
||||
} from "components/SharedComponents";
|
||||
import {
|
||||
Modal,
|
||||
SafeAreaView,
|
||||
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 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">
|
||||
<BackButton onPress={closeFlagItemModal} />
|
||||
<Subheading1 className="text-xl">
|
||||
{t( "Flag-An-Item" )}
|
||||
</Subheading1>
|
||||
</View>
|
||||
<KeyboardAwareScrollView
|
||||
ref={keyboardScrollRef}
|
||||
enableOnAndroid
|
||||
enableAutomaticScroll
|
||||
extraHeight={200}
|
||||
className="p-6"
|
||||
>
|
||||
<Body1 className="text-base">
|
||||
{t( "Flag-Item-Description" )}
|
||||
</Body1>
|
||||
<View className="flex-row my-2">
|
||||
<Checkbox
|
||||
isChecked={checkBoxValue === "spam"}
|
||||
onValueChange={() => toggleCheckBoxValue( "spam" )}
|
||||
text={t( "Spam" )}
|
||||
/>
|
||||
</View>
|
||||
<Body1 className="mb-2 text-base" style>{t( "Spam-Examples" )}</Body1>
|
||||
|
||||
<View className="flex-row my-2">
|
||||
<Checkbox
|
||||
isChecked={checkBoxValue === "inappropriate"}
|
||||
onValueChange={() => toggleCheckBoxValue( "inappropriate" )}
|
||||
text={t( "Offensive-Inappropriate" )}
|
||||
/>
|
||||
</View>
|
||||
<Body1 className="mb-2 text-base">{t( "Offensive-Inappropriate-Examples" )}</Body1>
|
||||
|
||||
<View className="flex-row my-2">
|
||||
<Checkbox
|
||||
isChecked={checkBoxValue === "other"}
|
||||
onValueChange={() => toggleCheckBoxValue( "other" )}
|
||||
text={t( "Other" )}
|
||||
/>
|
||||
</View>
|
||||
<Body1 className="mb-2 text-base">{t( "Flag-Item-Other-Description" )}</Body1>
|
||||
{( 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 ) )}
|
||||
accessibilityLabel={t( "Reason--flag" )}
|
||||
/>
|
||||
<Body3>{`${explanation.length}/255`}</Body3>
|
||||
</>
|
||||
)}
|
||||
<View className="flex-row justify-center m-4">
|
||||
<Button
|
||||
className="m-2"
|
||||
text={t( "Cancel" )}
|
||||
onPress={() => resetFlagModal()}
|
||||
/>
|
||||
<Button
|
||||
className="m-2"
|
||||
text={t( "Save" )}
|
||||
onPress={submitFlag}
|
||||
level="primary"
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
|
||||
);
|
||||
};
|
||||
export default FlagItemModal;
|
||||
@@ -1,139 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body3,
|
||||
INatIconButton
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import * as React from "react";
|
||||
import { useCurrentUser, useTranslation } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
type Props = {
|
||||
metric: string,
|
||||
votes: [Object],
|
||||
loadingAgree: boolean,
|
||||
loadingDisagree: boolean,
|
||||
loadingMetric: ?string,
|
||||
setVote: Function,
|
||||
removeVote: Function
|
||||
}
|
||||
|
||||
const getUserVote = ( currentUser, metric, votes ) => {
|
||||
if ( votes && votes.length > 0 ) {
|
||||
const match = votes.find( element => ( element.user_id === currentUser?.id ) );
|
||||
if ( match ) {
|
||||
if ( metric === "needs_id" ) {
|
||||
return match.vote_flag === true;
|
||||
}
|
||||
return match.agree === true;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderVoteCount = ( status, metric, votes ) => {
|
||||
if ( !votes ) return null;
|
||||
|
||||
const count = votes
|
||||
?.filter( qualityMetric => {
|
||||
if ( metric === "needs_id" ) {
|
||||
return qualityMetric.vote_flag === status;
|
||||
}
|
||||
return qualityMetric.agree === status;
|
||||
} )
|
||||
?.length;
|
||||
if ( !count || count === 0 ) return null;
|
||||
|
||||
return <Body3 classname="ml-[5px]">{count}</Body3>;
|
||||
};
|
||||
|
||||
const DQAVoteButtons = ( {
|
||||
metric,
|
||||
votes,
|
||||
loadingAgree,
|
||||
loadingDisagree,
|
||||
loadingMetric,
|
||||
setVote,
|
||||
removeVote
|
||||
}: Props ): React.Node => {
|
||||
const { t } = useTranslation( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const userAgrees = getUserVote( currentUser, metric, votes );
|
||||
const activityIndicatorOffset = "mx-[7px]";
|
||||
|
||||
const renderAgree = () => {
|
||||
if ( loadingAgree && loadingMetric === metric ) {
|
||||
return ( <ActivityIndicator size={33} className={activityIndicatorOffset} /> );
|
||||
}
|
||||
if ( userAgrees ) {
|
||||
return (
|
||||
<INatIconButton
|
||||
testID="DQAVoteButton.UserAgree"
|
||||
icon="arrow-up-bold-circle"
|
||||
size={33}
|
||||
color={colors.inatGreen}
|
||||
onPress={() => removeVote( { metric, vote: true } )}
|
||||
accessibilityLabel={t( "Add-agreement" )}
|
||||
accessibilityHint={t( "Adds-your-vote-of-agreement" )}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<INatIconButton
|
||||
testID="DQAVoteButton.EmptyAgree"
|
||||
icon="arrow-up-bold-circle-outline"
|
||||
size={33}
|
||||
onPress={() => setVote( { metric, vote: true } )}
|
||||
accessibilityLabel={t( "Remove-agreement" )}
|
||||
accessibilityHint={t( "Removes-your-vote-of-agreement" )}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDisagree = () => {
|
||||
if ( loadingDisagree && loadingMetric === metric ) {
|
||||
return ( <ActivityIndicator size={30} className={activityIndicatorOffset} /> );
|
||||
}
|
||||
|
||||
if ( userAgrees === false ) {
|
||||
return (
|
||||
<INatIconButton
|
||||
testID="DQAVoteButton.UserDisagree"
|
||||
icon="arrow-down-bold-circle"
|
||||
size={33}
|
||||
color={colors.warningRed}
|
||||
onPress={() => removeVote( { metric, vote: false } )}
|
||||
accessibilityLabel={t( "Remove-disagreement" )}
|
||||
accessibilityHint={t( "Removes-your-vote-of-disagreement" )}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<INatIconButton
|
||||
testID="DQAVoteButton.EmptyDisagree"
|
||||
icon="arrow-down-bold-circle-outline"
|
||||
size={33}
|
||||
onPress={() => setVote( { metric, vote: false } )}
|
||||
accessibilityLabel={t( "Add-disagreement" )}
|
||||
accessibilityHint={t( "Adds-your-vote-of-disagreement" )}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center justify-between w-[97px] space-x-[11px]">
|
||||
<View className="flex-row items-center w-1/2">
|
||||
{renderAgree()}
|
||||
{renderVoteCount( true, metric, votes )}
|
||||
</View>
|
||||
<View className="flex-row items-center w-1/2">
|
||||
{renderDisagree()}
|
||||
{renderVoteCount( false, metric, votes )}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DQAVoteButtons;
|
||||
@@ -1,73 +0,0 @@
|
||||
// @flow
|
||||
import {
|
||||
BottomSheet,
|
||||
Button,
|
||||
DisplayTaxon
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
|
||||
type Props = {
|
||||
onPressClose: Function,
|
||||
updateIdentification: Function,
|
||||
taxon: Object
|
||||
}
|
||||
|
||||
const showTaxon = taxon => {
|
||||
if ( !taxon ) {
|
||||
return <Text>{t( "Unknown-organism" )}</Text>;
|
||||
}
|
||||
return (
|
||||
<View className="flex-row mx-[15px]">
|
||||
<DisplayTaxon taxon={taxon} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const WithdrawIDSheet = ( {
|
||||
onPressClose,
|
||||
updateIdentification,
|
||||
taxon
|
||||
}: Props ): Node => (
|
||||
<BottomSheet
|
||||
onPressClose={onPressClose}
|
||||
headerText={t( "WITHDRAW-ID-QUESTION" )}
|
||||
>
|
||||
<View
|
||||
className="mx-[26px] space-y-[11px] my-[15px]"
|
||||
>
|
||||
{showTaxon( taxon )}
|
||||
</View>
|
||||
<View className="flex-row justify-evenly mx-3 mb-3">
|
||||
<Button
|
||||
text={t( "CANCEL" )}
|
||||
onPress={( ) => {
|
||||
onPressClose();
|
||||
}}
|
||||
className="mx-2"
|
||||
testID="ObsDetail.WithdrawId.cancel"
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={t( "Closes-withdraw-id-sheet" )}
|
||||
level="secondary"
|
||||
/>
|
||||
<Button
|
||||
text={t( "WITHDRAW-ID" )}
|
||||
onPress={( ) => {
|
||||
updateIdentification( { current: false } );
|
||||
onPressClose();
|
||||
}}
|
||||
className="mx-2 grow"
|
||||
testID="ObsDetail.WithdrawId.withdraw"
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={t( "Withdraws-identification" )}
|
||||
level="primary"
|
||||
/>
|
||||
|
||||
</View>
|
||||
</BottomSheet>
|
||||
);
|
||||
|
||||
export default WithdrawIDSheet;
|
||||
@@ -1,10 +0,0 @@
|
||||
// @flow
|
||||
|
||||
const checkCamelAndSnakeCase = ( object: Object, camelCaseKey: string ): ?string => {
|
||||
if ( !object ) { return ""; }
|
||||
const snakeCaseKey = camelCaseKey.replace( /[A-Z]/g, letter => `_${letter.toLowerCase()}` );
|
||||
|
||||
return object[camelCaseKey] || object[snakeCaseKey];
|
||||
};
|
||||
|
||||
export default checkCamelAndSnakeCase;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { CommonActions } from "@react-navigation/native";
|
||||
|
||||
// Creates a navigation tree that navigates to the ObsDetails screen with a specific obs UUID,
|
||||
// and when navigating back from the ObsDetails screen, it'll go back to ObsList screen
|
||||
export default function navigateToObsDetails( navigation, uuid ) {
|
||||
navigation.dispatch(
|
||||
CommonActions.reset( {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: "TabNavigator",
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: "TabStackNavigator",
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ name: "ObsList" },
|
||||
{
|
||||
name: "ObsDetails",
|
||||
params: { uuid }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
} )
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
import checkCamelAndSnakeCase from "components/ObsDetails/helpers/checkCamelAndSnakeCase";
|
||||
import { Body4, INatIcon } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import * as React from "react";
|
||||
import useTranslation from "sharedHooks/useTranslation.ts";
|
||||
|
||||
type Props = {
|
||||
observation: Object,
|
||||
};
|
||||
|
||||
const Geoprivacy = ( { observation }: Props ): React.Node => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
let geoprivacy = checkCamelAndSnakeCase( observation, "geoprivacy" );
|
||||
|
||||
if ( !geoprivacy ) {
|
||||
geoprivacy = t( "No-Location" );
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-row mt-[11px]">
|
||||
<INatIcon name="globe-outline" size={14} />
|
||||
<Body4
|
||||
className="text-darkGray ml-[5px]"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{geoprivacy}
|
||||
</Body4>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Geoprivacy;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { INatIconButton } from "components/SharedComponents";
|
||||
import React from "react";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
color: string;
|
||||
uniqueKey: string;
|
||||
uploadSingleObservation: ( ) => void;
|
||||
}
|
||||
|
||||
const UploadStartIcon = ( {
|
||||
color,
|
||||
uniqueKey,
|
||||
uploadSingleObservation
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
return (
|
||||
<INatIconButton
|
||||
icon="upload-saved"
|
||||
color={color}
|
||||
size={33}
|
||||
onPress={uploadSingleObservation}
|
||||
disabled={false}
|
||||
accessibilityLabel={t( "Start-upload" )}
|
||||
testID={`UploadIcon.start.${uniqueKey}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default UploadStartIcon;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import {
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import UserList from "./UserList";
|
||||
|
||||
const UserListContainer = ( ) => {
|
||||
const navigation = useNavigation( );
|
||||
const { params } = useRoute( );
|
||||
const { users, headerOptions } = params;
|
||||
|
||||
useEffect( ( ) => {
|
||||
navigation.setOptions( headerOptions );
|
||||
}, [headerOptions, navigation] );
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<View className="border-b border-lightGray mt-5" />
|
||||
<UserList users={users} />
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserListContainer;
|
||||
@@ -520,10 +520,6 @@ Filter-by-observed-on-date = Filter by observations observed on a specific date
|
||||
Filter-by-uploaded-between-dates = Filter by observations uploaded between two specific dates
|
||||
Filter-by-uploaded-on-date = Filter by observations uploaded on a specific date
|
||||
Filters = Filters
|
||||
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
|
||||
# Status when an item has been flagged
|
||||
Flagged = Flagged
|
||||
Flash = Flash
|
||||
@@ -876,8 +872,6 @@ Observers = Observers
|
||||
Observers-View = Observers View
|
||||
# Month of October
|
||||
October = October
|
||||
Offensive-Inappropriate = Offensive/Inappropriate
|
||||
Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of "appropriate," see the FAQ.
|
||||
Offline-DQA-description = The DQA may not be accurate. Check your internet connection and try again.
|
||||
Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load.
|
||||
# Generic confirmation, e.g. button on a warning alert
|
||||
@@ -904,10 +898,6 @@ Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer = Or, you can
|
||||
# Picker prompt on observation edit
|
||||
Organism-is-captive = Organism is captive
|
||||
Organisms-that-are-identified-to-species = Organisms that are identified to species rank or below
|
||||
# Generic option in a list for unanticipated cases, e.g. a choice to manually
|
||||
# enter an explanation for why you are flagging something instead of choosing
|
||||
# one of the existing options
|
||||
Other = Other
|
||||
OTHER-DATA = OTHER DATA
|
||||
OTHER-SUGGESTIONS = OTHER SUGGESTIONS
|
||||
# Tab on notifications showing notifications about content created by others.
|
||||
@@ -1039,9 +1029,6 @@ Ranks-Zoosection = Zoosection
|
||||
Ranks-ZOOSUBSECTION = ZOOSUBSECTION
|
||||
Ranks-Zoosubsection = Zoosubsection
|
||||
Read-more-on-Wikipedia = Read more on Wikipedia
|
||||
# Label for the input in the form for creating a flag that allows the user to
|
||||
# describe a reason they are creating the flag
|
||||
Reason--flag = Reason
|
||||
# Help text for the button that opens the sound recorder
|
||||
Record-a-sound = Record a sound
|
||||
# Title of screen asking for permission to access the microphone
|
||||
@@ -1102,8 +1089,6 @@ Reviewed-observations-only = Reviewed observations only
|
||||
Satellite--map-type = Satellite
|
||||
# Label for a button that persists something
|
||||
SAVE = SAVE
|
||||
# Label for a button that persists something
|
||||
Save = Save
|
||||
SAVE-ALL = SAVE ALL
|
||||
# Button that saves all observations in a batch of multiple observations
|
||||
Save-all-observations = Save all observations
|
||||
@@ -1197,8 +1182,6 @@ sound-recorder-help-Stop-moving = Stop moving
|
||||
sound-recorder-help-Try-to-isolate = Try to isolate the sound of a single organism. If you can’t, make sure to leave a note of which organism you’re recording.
|
||||
Sounds = Sounds
|
||||
Source-List = <0>(Source List: </0><1>{ $source }</1><0>)</0>
|
||||
Spam = Spam
|
||||
Spam-Examples = Commercial solicitation, links to nowhere, etc.
|
||||
Species = Species
|
||||
Species-View = Species View
|
||||
SPECIES-WITHOUT-NUMBER =
|
||||
@@ -1540,7 +1523,6 @@ You-may-have-observed-a-species-in-this-group = You may have observed a species
|
||||
You-may-have-observed-this-species = You may have observed this species
|
||||
# Description for modal that pops up if user logs in and has more than 50 observations
|
||||
You-may-notice-changes-to-how-things-look-and-flow = You may notice changes to how things look and flow. You can control your options in the settings.
|
||||
You-must-be-logged-in-to-view-messages = You must be logged in to view messages
|
||||
You-must-install-Google-Play-Services-to-sign-in-with-Google = You must install Google Play Services to sign in with Google.
|
||||
# Error message when you try to do something that requires an Internet
|
||||
# connection but such a connection is, tragically, missing
|
||||
|
||||
@@ -289,10 +289,6 @@
|
||||
"Filter-by-uploaded-between-dates": "Filter by observations uploaded between two specific dates",
|
||||
"Filter-by-uploaded-on-date": "Filter by observations uploaded on a specific date",
|
||||
"Filters": "Filters",
|
||||
"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",
|
||||
"Flagged": "Flagged",
|
||||
"Flash": "Flash",
|
||||
"Flip-camera": "Flip camera",
|
||||
@@ -512,8 +508,6 @@
|
||||
"Observers": "Observers",
|
||||
"Observers-View": "Observers View",
|
||||
"October": "October",
|
||||
"Offensive-Inappropriate": "Offensive/Inappropriate",
|
||||
"Offensive-Inappropriate-Examples": "Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of \"appropriate,\" see the FAQ.",
|
||||
"Offline-DQA-description": "The DQA may not be accurate. Check your internet connection and try again.",
|
||||
"Offline-suggestions-may-differ-from-online": "Offline suggestions may differ from online suggestions, and taxon images and common names may not load.",
|
||||
"OK": "OK",
|
||||
@@ -532,7 +526,6 @@
|
||||
"Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer": "Or, you can try to get a clearer photo by zooming in, getting closer, or trying a different angle.",
|
||||
"Organism-is-captive": "Organism is captive",
|
||||
"Organisms-that-are-identified-to-species": "Organisms that are identified to species rank or below",
|
||||
"Other": "Other",
|
||||
"OTHER-DATA": "OTHER DATA",
|
||||
"OTHER-SUGGESTIONS": "OTHER SUGGESTIONS",
|
||||
"OTHERS--notifications": "OTHERS",
|
||||
@@ -647,7 +640,6 @@
|
||||
"Ranks-ZOOSUBSECTION": "ZOOSUBSECTION",
|
||||
"Ranks-Zoosubsection": "Zoosubsection",
|
||||
"Read-more-on-Wikipedia": "Read more on Wikipedia",
|
||||
"Reason--flag": "Reason",
|
||||
"Record-a-sound": "Record a sound",
|
||||
"Record-animal-sounds": "Record animal sounds",
|
||||
"RECORD-NEW-SOUND": "RECORD NEW SOUND",
|
||||
@@ -683,7 +675,6 @@
|
||||
"Reviewed-observations-only": "Reviewed observations only",
|
||||
"Satellite--map-type": "Satellite",
|
||||
"SAVE": "SAVE",
|
||||
"Save": "Save",
|
||||
"SAVE-ALL": "SAVE ALL",
|
||||
"Save-all-observations": "Save all observations",
|
||||
"SAVE-CHANGES": "SAVE CHANGES",
|
||||
@@ -754,8 +745,6 @@
|
||||
"sound-recorder-help-Try-to-isolate": "Try to isolate the sound of a single organism. If you can’t, make sure to leave a note of which organism you’re recording.",
|
||||
"Sounds": "Sounds",
|
||||
"Source-List": "<0>(Source List: </0><1>{ $source }</1><0>)</0>",
|
||||
"Spam": "Spam",
|
||||
"Spam-Examples": "Commercial solicitation, links to nowhere, etc.",
|
||||
"Species": "Species",
|
||||
"Species-View": "Species View",
|
||||
"SPECIES-WITHOUT-NUMBER": "{ $count ->\n [one] SPECIES\n *[other] SPECIES\n}",
|
||||
@@ -928,7 +917,6 @@
|
||||
"You-may-have-observed-a-species-in-this-group": "You may have observed a species in this group",
|
||||
"You-may-have-observed-this-species": "You may have observed this species",
|
||||
"You-may-notice-changes-to-how-things-look-and-flow": "You may notice changes to how things look and flow. You can control your options in the settings.",
|
||||
"You-must-be-logged-in-to-view-messages": "You must be logged in to view messages",
|
||||
"You-must-install-Google-Play-Services-to-sign-in-with-Google": "You must install Google Play Services to sign in with Google.",
|
||||
"You-need-an-Internet-connection-to-do-that": "You need an Internet connection to do that.",
|
||||
"You-need-log-in-to-do-that": "You need to log in to do that.",
|
||||
|
||||
@@ -520,10 +520,6 @@ Filter-by-observed-on-date = Filter by observations observed on a specific date
|
||||
Filter-by-uploaded-between-dates = Filter by observations uploaded between two specific dates
|
||||
Filter-by-uploaded-on-date = Filter by observations uploaded on a specific date
|
||||
Filters = Filters
|
||||
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
|
||||
# Status when an item has been flagged
|
||||
Flagged = Flagged
|
||||
Flash = Flash
|
||||
@@ -876,8 +872,6 @@ Observers = Observers
|
||||
Observers-View = Observers View
|
||||
# Month of October
|
||||
October = October
|
||||
Offensive-Inappropriate = Offensive/Inappropriate
|
||||
Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of "appropriate," see the FAQ.
|
||||
Offline-DQA-description = The DQA may not be accurate. Check your internet connection and try again.
|
||||
Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load.
|
||||
# Generic confirmation, e.g. button on a warning alert
|
||||
@@ -904,10 +898,6 @@ Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer = Or, you can
|
||||
# Picker prompt on observation edit
|
||||
Organism-is-captive = Organism is captive
|
||||
Organisms-that-are-identified-to-species = Organisms that are identified to species rank or below
|
||||
# Generic option in a list for unanticipated cases, e.g. a choice to manually
|
||||
# enter an explanation for why you are flagging something instead of choosing
|
||||
# one of the existing options
|
||||
Other = Other
|
||||
OTHER-DATA = OTHER DATA
|
||||
OTHER-SUGGESTIONS = OTHER SUGGESTIONS
|
||||
# Tab on notifications showing notifications about content created by others.
|
||||
@@ -1039,9 +1029,6 @@ Ranks-Zoosection = Zoosection
|
||||
Ranks-ZOOSUBSECTION = ZOOSUBSECTION
|
||||
Ranks-Zoosubsection = Zoosubsection
|
||||
Read-more-on-Wikipedia = Read more on Wikipedia
|
||||
# Label for the input in the form for creating a flag that allows the user to
|
||||
# describe a reason they are creating the flag
|
||||
Reason--flag = Reason
|
||||
# Help text for the button that opens the sound recorder
|
||||
Record-a-sound = Record a sound
|
||||
# Title of screen asking for permission to access the microphone
|
||||
@@ -1102,8 +1089,6 @@ Reviewed-observations-only = Reviewed observations only
|
||||
Satellite--map-type = Satellite
|
||||
# Label for a button that persists something
|
||||
SAVE = SAVE
|
||||
# Label for a button that persists something
|
||||
Save = Save
|
||||
SAVE-ALL = SAVE ALL
|
||||
# Button that saves all observations in a batch of multiple observations
|
||||
Save-all-observations = Save all observations
|
||||
@@ -1197,8 +1182,6 @@ sound-recorder-help-Stop-moving = Stop moving
|
||||
sound-recorder-help-Try-to-isolate = Try to isolate the sound of a single organism. If you can’t, make sure to leave a note of which organism you’re recording.
|
||||
Sounds = Sounds
|
||||
Source-List = <0>(Source List: </0><1>{ $source }</1><0>)</0>
|
||||
Spam = Spam
|
||||
Spam-Examples = Commercial solicitation, links to nowhere, etc.
|
||||
Species = Species
|
||||
Species-View = Species View
|
||||
SPECIES-WITHOUT-NUMBER =
|
||||
@@ -1540,7 +1523,6 @@ You-may-have-observed-a-species-in-this-group = You may have observed a species
|
||||
You-may-have-observed-this-species = You may have observed this species
|
||||
# Description for modal that pops up if user logs in and has more than 50 observations
|
||||
You-may-notice-changes-to-how-things-look-and-flow = You may notice changes to how things look and flow. You can control your options in the settings.
|
||||
You-must-be-logged-in-to-view-messages = You must be logged in to view messages
|
||||
You-must-install-Google-Play-Services-to-sign-in-with-Google = You must install Google Play Services to sign in with Google.
|
||||
# Error message when you try to do something that requires an Internet
|
||||
# connection but such a connection is, tragically, missing
|
||||
|
||||
@@ -5,7 +5,6 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import About from "components/About";
|
||||
import Developer from "components/Developer/Developer";
|
||||
import Log from "components/Developer/Log";
|
||||
import NetworkLogging from "components/Developer/NetworkLogging.tsx";
|
||||
import UiLibrary from "components/Developer/UiLibrary";
|
||||
import UiLibraryItem from "components/Developer/UiLibraryItem";
|
||||
import Donate from "components/Donate/Donate.tsx";
|
||||
@@ -313,14 +312,6 @@ const TabStackNavigator = ( ): Node => {
|
||||
options={{ headerTitle: debugTitle }}
|
||||
|
||||
/>
|
||||
{ // eslint-disable-next-line no-undef
|
||||
__DEV__ && (
|
||||
<Stack.Screen
|
||||
name="network"
|
||||
component={NetworkLogging}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Stack.Screen
|
||||
name="UILibrary"
|
||||
component={UiLibrary}
|
||||
|
||||
@@ -126,6 +126,7 @@ export enum WILD_STATUS {
|
||||
|
||||
export enum REVIEWED {
|
||||
ALL = "ALL",
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
REVIEWED = "REVIEWED",
|
||||
UNREVIEWED = "UNREVIEWED"
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { render, screen } from "@testing-library/react-native";
|
||||
import Messages from "components/Messages/Messages";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import faker from "tests/helpers/faker";
|
||||
|
||||
const mockedNavigate = jest.fn( );
|
||||
const mockMessage = factory( "RemoteMessage", {
|
||||
subject: faker.lorem.sentence( )
|
||||
} );
|
||||
const mockUser = factory( "LocalUser" );
|
||||
|
||||
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockUser
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useNavigation: ( ) => ( {
|
||||
navigate: mockedNavigate
|
||||
} ),
|
||||
useRoute: ( ) => ( { } )
|
||||
};
|
||||
} );
|
||||
|
||||
const renderMessages = ( ) => render(
|
||||
<INatPaperProvider>
|
||||
<NavigationContainer>
|
||||
<Messages />
|
||||
</NavigationContainer>
|
||||
</INatPaperProvider>
|
||||
);
|
||||
|
||||
// We need to do some weird stuff to test results that vary based on useQuery
|
||||
// return values. We can't use jest.mock to totally change the mock
|
||||
// definition within a test, so instead we mock the module once and tell it
|
||||
// to return an object literal attribute in response to useQuery. That would
|
||||
// be a serious problem if tests get run in parallel because you can't really
|
||||
// control what that value is for that specific test. To address this, we
|
||||
// take advantage of the fact that Jest seems to run describe blocks
|
||||
// sequentially, or at least it doesn't seem to allow tests within a describe
|
||||
// block to access data from other describe blocks. Whatever's happening,
|
||||
// using a single mocked return value for useQuery within a describe block
|
||||
// seems to work.
|
||||
const MockData = { useQueryResponse: {} };
|
||||
jest.mock( "@tanstack/react-query", ( ) => ( {
|
||||
useQuery: ( ) => ( MockData.useQueryResponse )
|
||||
} ) );
|
||||
|
||||
describe( "Messages", ( ) => {
|
||||
it( "should not have accessibility errors", () => {
|
||||
const messages = <Messages />;
|
||||
expect( messages ).toBeAccessible();
|
||||
} );
|
||||
|
||||
describe( "when loading", ( ) => {
|
||||
beforeAll( ( ) => {
|
||||
MockData.useQueryResponse = {
|
||||
data: [],
|
||||
isLoading: true,
|
||||
isError: false
|
||||
};
|
||||
} );
|
||||
|
||||
it( "displays activity indicator when loading", ( ) => {
|
||||
renderMessages( );
|
||||
expect( screen.getByTestId( "Messages.activityIndicator" ) ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "when loading complete", ( ) => {
|
||||
beforeAll( ( ) => {
|
||||
MockData.useQueryResponse = {
|
||||
data: [mockMessage],
|
||||
isLoading: false,
|
||||
isError: false
|
||||
};
|
||||
} );
|
||||
|
||||
it( "displays message subject and not activity indicator when loading complete", ( ) => {
|
||||
renderMessages( );
|
||||
expect( screen.getByText( mockMessage.subject ) ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "Messages.activityIndicator" ) ).toBeNull( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -1,118 +0,0 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import FlagItemModal from "components/ObsDetails/FlagItemModal";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
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"
|
||||
} );
|
||||
|
||||
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 };
|
||||
} );
|
||||
|
||||
describe( "Flags", ( ) => {
|
||||
// it( "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( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
// expect( screen.getByText( "Flag" ) ).toBeTruthy( );
|
||||
// } );
|
||||
|
||||
// it( "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( await screen.findByTestId( "FlagItemModal" ) )
|
||||
// .toHaveProperty( "props.visible", true );
|
||||
// expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
// } );
|
||||
|
||||
it( "renders Flag Modal content", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Other" ) ).toBeTruthy( );
|
||||
expect( screen.getAllByRole( "radio" ) ).toHaveLength( 3 );
|
||||
} );
|
||||
|
||||
// it( "calls flag api when save button pressed", async ( ) => {
|
||||
// renderComponent(
|
||||
// <FlagItemModal
|
||||
// id="000"
|
||||
// itemType="foo"
|
||||
// showFlagItemModal
|
||||
// closeFlagItemModal={mockCallback}
|
||||
// onItemFlagged={mockCallback}
|
||||
// />
|
||||
// );
|
||||
// expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
// expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
// expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
// fireEvent.press( screen.queryByText( "Spam" ) );
|
||||
// expect( await screen.findByText( "Save" ) ).toBeTruthy( );
|
||||
// fireEvent.press( screen.queryByText( "Save" ) );
|
||||
// expect( await mockMutate ).toHaveBeenCalled();
|
||||
// } );
|
||||
} );
|
||||
@@ -1,66 +0,0 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import HeaderKebabMenu from "components/ObsDetails/HeaderKebabMenu";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import { Platform, Share } from "react-native";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
jest.mock( "react-native/Libraries/Share/Share", () => ( {
|
||||
share: jest.fn( () => Promise.resolve( "mockResolve" ) )
|
||||
} ) );
|
||||
|
||||
jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
|
||||
OS: "ios",
|
||||
select: jest.fn( )
|
||||
} ) );
|
||||
|
||||
const observationId = 1234;
|
||||
const url = `https://www.inaturalist.org/observations/${observationId}`;
|
||||
|
||||
describe( "HeaderKebabMenu", () => {
|
||||
it( "renders and opens kebab menu share button", async ( ) => {
|
||||
renderComponent( <HeaderKebabMenu observationId={observationId} /> );
|
||||
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
|
||||
expect( anchorButton ).toBeTruthy( );
|
||||
fireEvent.press( anchorButton );
|
||||
const shareButton = await screen.findByTestId( "MenuItem.Share" );
|
||||
expect( shareButton ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "opens native share dialog with expected url", async ( ) => {
|
||||
renderComponent( <HeaderKebabMenu observationId={observationId} /> );
|
||||
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
|
||||
expect( anchorButton ).toBeTruthy( );
|
||||
fireEvent.press( anchorButton );
|
||||
const shareButton = await screen.findByTestId( "MenuItem.Share" );
|
||||
expect( shareButton ).toBeTruthy( );
|
||||
fireEvent.press( shareButton );
|
||||
expect( Share.share ).toHaveBeenCalledTimes( 1 );
|
||||
expect( Share.share ).toHaveBeenCalledWith( { message: "", url } );
|
||||
} );
|
||||
|
||||
const defaultPlatformOS = Platform.OS;
|
||||
|
||||
describe( "on Android", ( ) => {
|
||||
// TODO change this to a less brittle and sweeping approach. Some ideas at
|
||||
// https://stackoverflow.com/questions/43161416/mocking-platform-detection-in-jest-and-react-native
|
||||
beforeAll( ( ) => {
|
||||
Platform.OS = "android";
|
||||
} );
|
||||
|
||||
afterAll( ( ) => {
|
||||
Platform.OS = defaultPlatformOS;
|
||||
} );
|
||||
|
||||
it( "opens native share dialog with expected message", async ( ) => {
|
||||
renderComponent( <HeaderKebabMenu observationId={observationId} /> );
|
||||
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
|
||||
expect( anchorButton ).toBeTruthy( );
|
||||
fireEvent.press( anchorButton );
|
||||
const shareButton = await screen.findByTestId( "MenuItem.Share" );
|
||||
expect( shareButton ).toBeTruthy( );
|
||||
fireEvent.press( shareButton );
|
||||
expect( Share.share ).toHaveBeenCalledWith( { message: url, url: "" } );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -1,103 +0,0 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import DQAVoteButtons from "components/ObsDetailsDefaultMode/MoreSection/DQAVoteButtons";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
const mockUser = factory( "RemoteUser" );
|
||||
const mockQualityMetrics = [
|
||||
{
|
||||
id: 0,
|
||||
agree: true,
|
||||
metric: "wild",
|
||||
user_id: mockUser.id
|
||||
}
|
||||
];
|
||||
|
||||
const mockVotesNeedsID = [
|
||||
factory( "RemoteVote", {
|
||||
vote_scope: "needs_id",
|
||||
user: mockUser,
|
||||
user_id: mockUser.id
|
||||
} )
|
||||
];
|
||||
|
||||
// Mock useCurrentUser hook
|
||||
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: jest.fn( ( ) => ( {
|
||||
id: mockUser.id
|
||||
} ) )
|
||||
} ) );
|
||||
|
||||
describe( "DQA Vote Buttons for wild metric", ( ) => {
|
||||
test( "renders correct DQA user vote", async ( ) => {
|
||||
renderComponent( <DQAVoteButtons
|
||||
metric="wild"
|
||||
votes={mockQualityMetrics}
|
||||
setVote={jest.fn()}
|
||||
loadingAgree={jest.fn()}
|
||||
loadingDisagree={jest.fn()}
|
||||
loadingMetric={jest.fn()}
|
||||
removeVote={jest.fn()}
|
||||
/> );
|
||||
|
||||
const button = await screen.findByTestId(
|
||||
"DQAVoteButton.UserAgree"
|
||||
);
|
||||
expect( button ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders correct DQA user vote number", async ( ) => {
|
||||
renderComponent( <DQAVoteButtons
|
||||
metric="wild"
|
||||
votes={mockQualityMetrics}
|
||||
setVote={jest.fn()}
|
||||
loadingAgree={jest.fn()}
|
||||
loadingDisagree={jest.fn()}
|
||||
loadingMetric={jest.fn()}
|
||||
removeVote={jest.fn()}
|
||||
/> );
|
||||
|
||||
const voteNumber = await screen.findByText(
|
||||
"1"
|
||||
);
|
||||
expect( voteNumber ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "DQA Vote Buttons for needs_id metric", ( ) => {
|
||||
test( "renders correct DQA user vote", async ( ) => {
|
||||
renderComponent( <DQAVoteButtons
|
||||
metric="needs_id"
|
||||
votes={mockVotesNeedsID}
|
||||
setVote={jest.fn()}
|
||||
loadingAgree={jest.fn()}
|
||||
loadingDisagree={jest.fn()}
|
||||
loadingMetric={jest.fn()}
|
||||
removeVote={jest.fn()}
|
||||
/> );
|
||||
|
||||
const button = await screen.findByTestId(
|
||||
"DQAVoteButton.UserAgree"
|
||||
);
|
||||
expect( button ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders correct DQA user vote number", async ( ) => {
|
||||
renderComponent( <DQAVoteButtons
|
||||
metric="needs_id"
|
||||
votes={mockVotesNeedsID}
|
||||
setVote={jest.fn()}
|
||||
loadingAgree={jest.fn()}
|
||||
loadingDisagree={jest.fn()}
|
||||
loadingMetric={jest.fn()}
|
||||
removeVote={jest.fn()}
|
||||
/> );
|
||||
|
||||
const voteNumber = await screen.findByText(
|
||||
"1"
|
||||
);
|
||||
expect( voteNumber ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
@@ -1,118 +0,0 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import FlagItemModal from "components/ObsDetailsDefaultMode/FlagItemModal";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
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"
|
||||
} );
|
||||
|
||||
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 };
|
||||
} );
|
||||
|
||||
describe( "Flags", ( ) => {
|
||||
// it( "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( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
// expect( screen.getByText( "Flag" ) ).toBeTruthy( );
|
||||
// } );
|
||||
|
||||
// it( "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( await screen.findByTestId( "FlagItemModal" ) )
|
||||
// .toHaveProperty( "props.visible", true );
|
||||
// expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
// } );
|
||||
|
||||
it( "renders Flag Modal content", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Other" ) ).toBeTruthy( );
|
||||
expect( screen.getAllByRole( "radio" ) ).toHaveLength( 3 );
|
||||
} );
|
||||
|
||||
// it( "calls flag api when save button pressed", async ( ) => {
|
||||
// renderComponent(
|
||||
// <FlagItemModal
|
||||
// id="000"
|
||||
// itemType="foo"
|
||||
// showFlagItemModal
|
||||
// closeFlagItemModal={mockCallback}
|
||||
// onItemFlagged={mockCallback}
|
||||
// />
|
||||
// );
|
||||
// expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
// expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
// expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
// fireEvent.press( screen.queryByText( "Spam" ) );
|
||||
// expect( await screen.findByText( "Save" ) ).toBeTruthy( );
|
||||
// fireEvent.press( screen.queryByText( "Save" ) );
|
||||
// expect( await mockMutate ).toHaveBeenCalled();
|
||||
// } );
|
||||
} );
|
||||
@@ -1,37 +0,0 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import WithdrawIDSheet from "components/ObsDetailsDefaultMode/Sheets/WithdrawIDSheet";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
const mockTaxon = factory( "RemoteTaxon", {
|
||||
name: "Plantae",
|
||||
iconic_taxon_name: "Plantae"
|
||||
} );
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
|
||||
const mockHandleClose = jest.fn();
|
||||
|
||||
describe( "WithdrawIDSheet", () => {
|
||||
it( "renders sheet correctly", async ( ) => {
|
||||
renderComponent(
|
||||
<WithdrawIDSheet
|
||||
onPressClose={mockHandleClose}
|
||||
withdrawOrRestoreIdentification={mockMutate}
|
||||
taxon={mockTaxon}
|
||||
/>
|
||||
);
|
||||
|
||||
expect( await screen.findByText( t( "WITHDRAW-ID-QUESTION" ) ) ).toBeTruthy( );
|
||||
expect( screen.getByRole(
|
||||
"button",
|
||||
{ name: t( "WITHDRAW-ID" ), disabled: false }
|
||||
) ).toBeTruthy( );
|
||||
expect( screen.getByRole(
|
||||
"button",
|
||||
{ name: t( "CANCEL" ), disabled: false }
|
||||
) ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user