Taxonomy component, TaxonDetails (#978)

* Style taxonomy ancestors and current taxon

* Link to TaxonDetails pages

* Add children to TaxonDetails taxonomy

* Add children taxa toggle

* Display children after button tap

* Wrap text'

* Fix italic font size in DisplayTaxonName

* Add tests to Taxonomy component

* Change Taxonomy test
This commit is contained in:
Amanda Bullington
2023-12-19 11:04:30 -08:00
committed by GitHub
parent cad36a7253
commit cd18e970d7
19 changed files with 346 additions and 39 deletions

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -5,6 +5,18 @@
"path": "assets/fonts/INatIcon.ttf",
"sha1": "495181444f9a2d8275f8bd86ff4988cd54e1fcb4"
},
{
"path": "assets/fonts/Whitney Book Regular.otf",
"sha1": "9b6e4749dcfa702e3fd6cc27d5c2bef1fdb966f9"
},
{
"path": "assets/fonts/Whitney Semibold Italic.otf",
"sha1": "829391a59c4305fe4733009da64fec983091cde1"
},
{
"path": "assets/fonts/Whitney Semibold Regular.otf",
"sha1": "fc560886d72adf37cccbad5bf8c58d7e0e546027"
},
{
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
"sha1": "15854f60175a0e82b794c259431ec45ea4b40103"

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -62,6 +62,9 @@
<string>Whitney-BookItalic-Pro.otf</string>
<string>inaturalisticons.ttf</string>
<string>INatIcon.ttf</string>
<string>Whitney Semibold Regular.otf</string>
<string>Whitney Semibold Italic.otf</string>
<string>Whitney Book Regular.otf</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>

View File

@@ -5,6 +5,18 @@
"path": "assets/fonts/INatIcon.ttf",
"sha1": "495181444f9a2d8275f8bd86ff4988cd54e1fcb4"
},
{
"path": "assets/fonts/Whitney Book Regular.otf",
"sha1": "9b6e4749dcfa702e3fd6cc27d5c2bef1fdb966f9"
},
{
"path": "assets/fonts/Whitney Semibold Italic.otf",
"sha1": "829391a59c4305fe4733009da64fec983091cde1"
},
{
"path": "assets/fonts/Whitney Semibold Regular.otf",
"sha1": "fc560886d72adf37cccbad5bf8c58d7e0e546027"
},
{
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
"sha1": "15854f60175a0e82b794c259431ec45ea4b40103"

View File

@@ -7,7 +7,8 @@ import handleError from "./error";
const ANCESTOR_FIELDS = {
name: true,
preferred_common_name: true,
rank: true
rank: true,
rank_level: true
};
const PHOTO_FIELDS = {
@@ -19,6 +20,7 @@ const PHOTO_FIELDS = {
const FIELDS = {
ancestors: ANCESTOR_FIELDS,
children: ANCESTOR_FIELDS,
default_photo: {
url: true
},

View File

@@ -96,7 +96,7 @@ const DisplayTaxonName = ( {
<INatText
// eslint-disable-next-line react/no-array-index-key
key={`DisplayTaxonName-${keyBase}-${taxon.id}-${rankLevel}-${piece}-${index}`}
className={classNames( "italic", textClass() )}
className={classNames( "italic font-normal", textClass() )}
>
{text}
</INatText>

View File

@@ -13,7 +13,9 @@ import {
useWindowDimensions
} from "react-native";
import HTML, { defaultSystemFonts } from "react-native-render-html";
import useTranslation from "sharedHooks/useTranslation";
import { useTranslation } from "sharedHooks";
import Taxonomy from "./Taxonomy";
type Props = {
taxon?: Object,
@@ -25,37 +27,6 @@ const About = ( { taxon, isLoading, isError }: Props ): React.Node => {
const { width } = useWindowDimensions();
const { t } = useTranslation();
const displayTaxonomyList = React.useMemo( () => {
if ( !taxon || taxon.ancestors?.length === 0 ) {
return <View />;
}
return taxon.ancestors?.map( ( ancestor, i ) => {
const currentTaxon = `${taxon.preferred_common_name} (${taxon.name})`;
// TODO: make sure this design accounts for undefined common names
const formattedAncestor = ancestor.preferred_common_name
? `${ancestor.preferred_common_name} (${ancestor.rank} ${ancestor.name})`
: `(${ancestor.rank} ${ancestor.name})`;
const displayAncestor = (
<Body2>{formattedAncestor}</Body2>
);
const displayTaxon = (
<Body2>{currentTaxon}</Body2>
);
const lastAncestor = i === taxon.ancestors.length - 1;
return (
<View key={lastAncestor
? taxon.id
: ancestor.id}
>
{displayAncestor}
{lastAncestor && displayTaxon}
</View>
);
} );
}, [taxon] );
const openWikipedia = () => {
if ( taxon?.wikipedia_url ) {
Linking.openURL( taxon.wikipedia_url );
@@ -106,10 +77,7 @@ const About = ( { taxon, isLoading, isError }: Props ): React.Node => {
</Body2>
) }
<Heading4 className="my-3">
{t( "TAXONOMY-header" )}
</Heading4>
{displayTaxonomyList}
<Taxonomy taxon={taxon} />
</View>
);
};

View File

@@ -0,0 +1,191 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import classnames from "classnames";
import {
Body2,
Button,
Heading4,
INatIcon
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import type { Node } from "react";
import React, { useCallback, useState } from "react";
import Taxon from "realmModels/Taxon";
import { generateTaxonPieces } from "sharedHelpers/taxon";
import { useTranslation } from "sharedHooks";
type Props = {
taxon?: Object
}
const Taxonomy = ( { taxon: currentTaxon }: Props ): Node => {
const [viewChildren, setViewChildren] = useState( false );
const navigation = useNavigation( );
const { t } = useTranslation( );
const displayCommonName = ( commonName, options ) => (
<Body2 className={
classnames( "font-bold mr-1", {
"text-inatGreen": options?.isCurrentTaxon,
underline: !options?.isCurrentTaxon
} )
}
>
{commonName}
</Body2>
);
const displayScientificName = ( rank, scientificNamePieces, rankLevel, rankPiece, options ) => {
const isCurrentTaxon = options?.isCurrentTaxon;
const hasCommonName = options?.hasCommonName;
const underline = !isCurrentTaxon && !hasCommonName;
// italics part ported over from DisplayTaxonName
const scientificNameComponent = scientificNamePieces?.map( ( piece, index ) => {
const isItalics = piece !== rankPiece && (
rankLevel <= Taxon.SPECIES_LEVEL || rankLevel === Taxon.GENUS_LEVEL
);
const spaceChar = ( ( index !== scientificNamePieces.length - 1 ) )
? " "
: "";
const text = piece + spaceChar;
return (
<Body2
key={text}
className={
classnames( {
italic: isItalics,
"font-bold": underline,
"text-inatGreen": isCurrentTaxon
} )
}
>
{text}
</Body2>
);
} );
return (
<Body2 className={
classnames( {
underline,
"text-inatGreen": isCurrentTaxon,
"-ml-1 ": !hasCommonName
} )
}
>
{hasCommonName && (
<Body2 className={
classnames( {
"text-inatGreen": isCurrentTaxon
} )
}
>
(
</Body2>
)}
{rankLevel > 10 && (
<Body2
className={
classnames( {
"font-bold": !hasCommonName,
"text-inatGreen": isCurrentTaxon
} )
}
>
{`${rank} `}
</Body2>
)}
{scientificNameComponent}
{hasCommonName && (
<Body2 className={
classnames( {
"text-inatGreen": isCurrentTaxon
} )
}
>
)
</Body2>
)}
</Body2>
);
};
const renderTaxon = useCallback( ( taxon, options ) => {
const id = taxon?.id || "";
const isCurrentTaxon = options?.isCurrentTaxon;
const isChild = options?.isChild;
const {
commonName,
scientificNamePieces,
rankPiece,
rankLevel,
rank
} = generateTaxonPieces( taxon );
return (
<Pressable
accessibilityRole="button"
className="flex-row py-2 flex-wrap"
key={id}
disabled={isCurrentTaxon}
onPress={( ) => navigation.navigate( "TaxonDetails", { id } )}
accessibilityState={{
disabled: isCurrentTaxon
}}
accessibilityLabel={t( "Navigate-to-taxon-details" )}
testID={`TaxonomyRow.${id}`}
>
{isChild && (
<View className="ml-2 mr-1">
<INatIcon name="arrow-turn-down-right" size={11} />
</View>
)}
{displayCommonName( commonName, { isCurrentTaxon } )}
{displayScientificName(
rank,
scientificNamePieces,
rankLevel,
rankPiece,
{
isCurrentTaxon,
hasCommonName: commonName
}
)}
</Pressable>
);
}, [navigation, t] );
const displayTaxonomy = useCallback(
( ) => (
<>
{currentTaxon?.ancestors?.map( ancestor => renderTaxon( ancestor ) )}
{renderTaxon( currentTaxon, { isCurrentTaxon: true } )}
{viewChildren
&& currentTaxon?.children?.map( child => renderTaxon( child, {
isChild: true
} ) )}
</>
),
[currentTaxon, renderTaxon, viewChildren]
);
return (
<View className="mb-5">
<Heading4 className="my-3">
{t( "TAXONOMY-header" )}
</Heading4>
{displayTaxonomy( )}
{!viewChildren && currentTaxon?.children && (
<Button
className="mt-3"
onPress={( ) => setViewChildren( true )}
text={t( "VIEW-CHILDREN-TAXA" )}
/>
)}
</View>
);
};
export default Taxonomy;

View File

@@ -854,6 +854,8 @@ Username = Username
# Appears above the text fields
USERNAME-OR-EMAIL = USERNAME OR EMAIL
VIEW-CHILDREN-TAXA = VIEW CHILDREN TAXA
VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
View-in-browser = View in Browser

View File

@@ -565,6 +565,7 @@
"comment": "Appears above the text fields",
"val": "USERNAME OR EMAIL"
},
"VIEW-CHILDREN-TAXA": "VIEW CHILDREN TAXA",
"VIEW-DATA-QUALITY-ASSESSEMENT": "VIEW DATA QUALITY ASSESSEMENT",
"View-in-browser": "View in Browser",
"View-photo": {

View File

@@ -854,6 +854,8 @@ Username = Username
# Appears above the text fields
USERNAME-OR-EMAIL = USERNAME OR EMAIL
VIEW-CHILDREN-TAXA = VIEW CHILDREN TAXA
VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
View-in-browser = View in Browser

View File

@@ -14,7 +14,6 @@ const useAuthenticatedMutation = (
// one is expired. We *could* store the token in state with useState if
// fetching from RNSInfo becomes a performance issue
const apiToken = await getJWT( );
console.log( apiToken, "api token jwt" );
const options = {
api_token: apiToken
};

View File

@@ -70,6 +70,12 @@ module.exports = {
"Whitney-Light-Pro": ["Whitney-Light-Pro"], // Android naming convention
"Whitney-BookItalic": ["Whitney-BookItalic"],
"Whitney-BookItalic-Pro": ["Whitney-BookItalic-Pro"], // Android naming convention
"Whitney-Semibold": ["Whitney-Semibold"],
"Whitney-Semibold-Pro": ["Whitney-Semibold-Pro"], // Android naming convention
"Whitney-Semibold-Italic": ["Whitney-Semibold-Italic"],
"Whitney-Semibold-Italic-Pro": ["Whitney-Semibold-Italic-Pro"], // Android naming convention
"Whitney-Book": ["Whitney-Book"],
"Whitney-Book-Pro": ["Whitney-Book-Pro"], // Android naming convention
// selected from list of fonts already available in RN
// https://infinitbility.com/react-native-font-family-list/
"Papyrus-Condensed": ["Papyrus-Condensed"],

View File

@@ -0,0 +1,109 @@
import { fireEvent, render, screen } from "@testing-library/react-native";
import Taxonomy from "components/TaxonDetails/Taxonomy";
import initI18next from "i18n/initI18next";
import React from "react";
import factory from "tests/factory";
const capitalizeFirstLetter = s => s.charAt( 0 ).toUpperCase( ) + s.slice( 1 );
const ancestors = [
factory( "RemoteTaxon", {
name: "Fungi",
preferred_common_name: "Fungi Including Lichens",
rank: "kingdom",
rank_level: 80
} ),
factory( "RemoteTaxon", {
name: "Agaricomycetes",
preferred_common_name: "Mushrooms, Bracket Fungi, Puffballs, and Allies",
rank: "class",
rank_level: 60
} )
];
const children = [factory( "RemoteTaxon", {
name: "Amanitina",
preferred_common_name: "Amanita Subg. Amanitina",
rank_level: 15,
rank: "subgenus"
} )
];
const currentTaxon = factory( "RemoteTaxon", {
name: "Amanita",
preferred_common_name: "Amanita Mushrooms",
rank: "genus",
rank_level: 20,
ancestors,
children
} );
describe( "Taxonomy", ( ) => {
beforeAll( async ( ) => {
await initI18next( );
} );
test( "renders current taxon", ( ) => {
render( <Taxonomy taxon={currentTaxon} /> );
const rankAndName = `${capitalizeFirstLetter( currentTaxon.rank )} ${currentTaxon.name}`;
const commonName = currentTaxon.preferred_common_name;
const currentTaxonRow = screen.getByTestId( `TaxonomyRow.${currentTaxon.id}` );
expect( currentTaxonRow ).toHaveTextContent( `${commonName}(${rankAndName})` );
} );
test( "renders current taxon", ( ) => {
render( <Taxonomy taxon={currentTaxon} /> );
const name = screen.getByText( currentTaxon.name );
const commonName = screen.getByText( currentTaxon.preferred_common_name );
expect( commonName ).toBeVisible( );
expect( name ).toBeVisible( );
} );
test( "renders all ancestors", ( ) => {
render( <Taxonomy taxon={currentTaxon} /> );
currentTaxon.ancestors.forEach( ancestor => {
const ancestorName = screen.getByText( ancestor.name );
const ancestorCommonName = screen.getByText( ancestor.preferred_common_name );
expect( ancestorName ).toBeVisible( );
expect( ancestorCommonName ).toBeVisible( );
} );
} );
test( "renders button to optionally show children", ( ) => {
render( <Taxonomy taxon={currentTaxon} /> );
const buttonText = screen.getByText( /VIEW CHILDREN TAXA/ );
expect( buttonText ).toBeVisible( );
// eslint-disable-next-line testing-library/no-node-access
currentTaxon.children.forEach( child => {
const childName = screen.queryByText( child.name );
const childCommonName = screen.queryByText( child.preferred_common_name );
expect( childName ).toBeFalsy( );
expect( childCommonName ).toBeFalsy( );
} );
} );
test( "shows children when button pressed", ( ) => {
render( <Taxonomy taxon={currentTaxon} /> );
const buttonText = screen.getByText( /VIEW CHILDREN TAXA/ );
fireEvent.press( buttonText );
// eslint-disable-next-line testing-library/no-node-access
currentTaxon.children.forEach( child => {
const childName = screen.getByText( child.name );
const childCommonName = screen.getByText( child.preferred_common_name );
expect( childName ).toBeVisible( );
expect( childCommonName ).toBeVisible( );
} );
} );
} );