Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions ui/src/components/gbif/gbif-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Command } from 'nova-ui-kit'
import { useState } from 'react'
import { useDebounce } from 'utils/useDebounce'
import { GBIFTaxon } from './types'
import { useGBIFSearch } from './useGBIFSearch'

export const GBIFSearch = ({
onTaxonChange,
rank,
taxon,
}: {
onTaxonChange: (taxon?: GBIFTaxon) => void
rank?: string
taxon?: GBIFTaxon
}) => {
const [searchString, setSearchString] = useState('')
const debouncedSearchString = useDebounce(searchString, 200)
const { data, isLoading } = useGBIFSearch({
searchString: debouncedSearchString,
rank,
})

return (
<Command.Root
shouldFilter={false}
style={{
maxHeight: 'calc(var(--radix-popover-content-available-height) - 2px)',
}}
>
<Command.Input
loading={isLoading}
placeholder="Search GBIF..."
value={searchString}
onValueChange={setSearchString}
/>
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group>
{data?.map((t) => (
<Command.Item
key={t.key}
className="h-16 pr-2"
onSelect={() => onTaxonChange(t)}
>
<Command.Taxon
label={t.canonicalName}
rank={t.rank}
selected={t.key === taxon?.key}
/>
</Command.Item>
))}
</Command.Group>
</Command.List>
</Command.Root>
)
}
58 changes: 58 additions & 0 deletions ui/src/components/gbif/gbif-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ChevronDownIcon, Loader2Icon } from 'lucide-react'
import { Button, Popover } from 'nova-ui-kit'
import { useState } from 'react'
import { GBIFSearch } from './gbif-search'
import { GBIFTaxon } from './types'

export const GBIFSelect = ({
isLoading,
onTaxonChange,
rank,
taxon,
triggerLabel,
}: {
isLoading?: boolean
onTaxonChange: (taxon?: GBIFTaxon) => void
rank?: string
taxon?: GBIFTaxon
triggerLabel: string
}) => {
const [open, setOpen] = useState(false)

return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<Button
aria-expanded={open}
className="w-full justify-between px-4 text-muted-foreground font-normal"
role="combobox"
type="button"
variant="outline"
>
<>
<span>{triggerLabel}</span>
{isLoading ? (
<Loader2Icon className="h-4 w-4 ml-2 animate-spin" />
) : (
<ChevronDownIcon className="h-4 w-4 ml-2" />
)}
</>
</Button>
</Popover.Trigger>
<Popover.Content
avoidCollisions={false}
className="w-auto p-0 overflow-hidden"
style={{ maxHeight: 'var(--radix-popover-content-available-height)' }}
>
<GBIFSearch
onTaxonChange={(taxon) => {
onTaxonChange(taxon)
setOpen(false)
}}
rank={rank}
taxon={taxon}
/>
</Popover.Content>
</Popover.Root>
)
}
5 changes: 5 additions & 0 deletions ui/src/components/gbif/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type GBIFTaxon = {
canonicalName: string
key: number
rank: string
}
63 changes: 63 additions & 0 deletions ui/src/components/gbif/useGBIFSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import axios from 'axios'
import { useEffect, useState } from 'react'
import { GBIFTaxon } from './types'

const BASE_URL = 'https://api.gbif.org/v1/species' // See docs at https://techdocs.gbif.org/en/openapi/v1/species
Copy link
Member Author

@annavik annavik Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More details about the GBIF API call in this file!

const DATASET_KEY = 'd7dddbf4-2cf0-4f39-9b2a-bb099caae36c' // GBIF Backbone Taxonomy
const HIGHER_TAXON_KEY = 216 // Insecta
const STATUS = 'ACCEPTED'
const LIMIT = 10

const getFetchUrl = ({
rank,
searchString,
}: {
rank?: string
searchString: string
}) => {
if (!searchString.length) {
return undefined
}

let url = `${BASE_URL}/search?datasetKey=${DATASET_KEY}&higherTaxonKey=${HIGHER_TAXON_KEY}&status=${STATUS}&limit=${LIMIT}&q=${searchString}`

if (rank) {
url = `${url}&rank=${rank}`
}

return url
}

export const useGBIFSearch = (params: {
rank?: string
searchString: string
}) => {
const [data, setData] = useState<GBIFTaxon[]>()
const [isLoading, setIsLoading] = useState<boolean>()
const [error, setError] = useState<Error>()
const fetchUrl = getFetchUrl(params)

useEffect(() => {
setError(undefined)

if (!fetchUrl) {
setData(undefined)
setIsLoading(false)
return
}

setIsLoading(true)
axios
.get<{ results: GBIFTaxon[] }>(fetchUrl)
.then((res) => {
setData(res.data.results)
setIsLoading(false)
})
.catch((error: Error) => {
setError(error)
setIsLoading(false)
})
}, [fetchUrl])

return { data, isLoading, error }
}
5 changes: 3 additions & 2 deletions ui/src/data-services/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ export const API_ROUTES = {
SESSIONS: 'events',
SITES: 'deployments/sites',
SPECIES: 'taxa',
TAGS: 'tags',
TAXA_LISTS: 'taxa/lists',
STORAGE: 'storage',
SUMMARY: 'status/summary',
TAGS: 'tags',
TAXA_LISTS: 'taxa/lists',
TAXA: 'taxa',
USERS: 'users',
}

Expand Down
2 changes: 1 addition & 1 deletion ui/src/data-services/hooks/entities/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface EntityFieldValues {
description?: string
name: string
name?: string
projectId: string
customFields?: { [key: string]: string | number | object | undefined }
}
2 changes: 1 addition & 1 deletion ui/src/data-services/hooks/entities/useCreateEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ export const useCreateEntity = (collection: string, onSuccess?: () => void) => {
},
})

return { createEntity: mutateAsync, isLoading, isSuccess, error }
return { createEntity: mutateAsync, isLoading, isSuccess, error, reset }
}
4 changes: 4 additions & 0 deletions ui/src/data-services/hooks/species/useSpecies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { API_ROUTES } from 'data-services/constants'
import { FetchParams } from 'data-services/types'
import { getFetchUrl } from 'data-services/utils'
import { useMemo } from 'react'
import { UserPermission } from 'utils/user/types'
import { ServerSpecies, Species } from '../../models/species'
import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'

Expand All @@ -12,6 +13,7 @@ export const useSpecies = (
): {
species?: Species[]
total: number
userPermissions?: UserPermission[]
isLoading: boolean
isFetching: boolean
error?: unknown
Expand All @@ -20,6 +22,7 @@ export const useSpecies = (

const { data, isLoading, isFetching, error } = useAuthorizedQuery<{
results: ServerSpecies[]
user_permissions?: UserPermission[]
count: number
}>({
queryKey: [API_ROUTES.SPECIES, params],
Expand All @@ -31,6 +34,7 @@ export const useSpecies = (
return {
species,
total: data?.count ?? 0,
userPermissions: data?.user_permissions,
isLoading,
isFetching,
error,
Expand Down
19 changes: 4 additions & 15 deletions ui/src/data-services/models/taxa.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RANKS } from 'utils/constants'

export type ServerTaxon = {
id: string
name: string
Expand All @@ -7,19 +9,6 @@ export type ServerTaxon = {
parents?: ServerTaxon[]
}

const SORTED_RANKS = [
'Unknown',
'ORDER',
'SUBORDER',
'SUPERFAMILY',
'FAMILY',
'SUBFAMILY',
'TRIBE',
'SUBTRIBE',
'GENUS',
'SPECIES',
'SUBSPECIES',
]
export class Taxon {
readonly id: string
readonly name: string
Expand All @@ -46,8 +35,8 @@ export class Taxon {

// TODO: Perhaps sorting should happen backend side? If so, let's remove this later.
this.ranks.sort((r1, r2) => {
const value1 = SORTED_RANKS.indexOf(r1.rank)
const value2 = SORTED_RANKS.indexOf(r2.rank)
const value1 = RANKS.indexOf(r1.rank)
const value2 = RANKS.indexOf(r2.rank)

return value1 - value2
})
Expand Down
6 changes: 4 additions & 2 deletions ui/src/pages/project/entities/details-form/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { CollectionDetailsForm } from './collection-details-form'
import { ExportDetailsForm } from './export-details-form'
import { ProcessingServiceDetailsForm } from './processing-service-details-form'
import { StorageDetailsForm } from './storage-details-form'
import { TaxonDetailsForm } from './taxon-details-form'
import { DetailsFormProps } from './types'

export const customFormMap: {
[key: string]: (props: DetailsFormProps) => JSX.Element
} = {
export: ExportDetailsForm,
storage: StorageDetailsForm,
collection: CollectionDetailsForm,
export: ExportDetailsForm,
service: ProcessingServiceDetailsForm,
storage: StorageDetailsForm,
taxon: TaxonDetailsForm,
}
Loading