Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions ui/src/data-services/hooks/identifications/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface IdentificationFieldValues {
agreeWith?: {
identificationId?: string
predictionId?: string
}
occurrenceId: string
taxonId: string
comment?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,7 @@ import axios from 'axios'
import { API_ROUTES, API_URL } from 'data-services/constants'
import { getAuthHeader } from 'data-services/utils'
import { useUser } from 'utils/user/userContext'

interface IdentificationFieldValues {
agreeWith?: {
identificationId?: string
predictionId?: string
}
occurrenceId: string
taxonId: string
comment?: string
}
import { IdentificationFieldValues } from './types'

const convertToServerFieldValues = (
fieldValues: IdentificationFieldValues
Expand Down Expand Up @@ -49,6 +40,6 @@ export const useCreateIdentification = (onSuccess?: () => void) => {
isLoading,
isSuccess,
reset,
error,
error: error ? 'The update was rejected, please retry.' : undefined,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { SUCCESS_TIMEOUT } from 'data-services/constants'
import { useEffect, useState } from 'react'
import { IdentificationFieldValues } from './types'
import { useCreateIdentification } from './useCreateIdentification'

export const useCreateIdentifications = (
params: IdentificationFieldValues[]
) => {
const [results, setResults] = useState<PromiseSettledResult<any>[]>()
const { createIdentification, isLoading, isSuccess, reset } =
useCreateIdentification(() => {
setTimeout(() => {
reset()
}, SUCCESS_TIMEOUT)
})

const numRejected = results?.filter(
(result) => result.status === 'rejected'
).length

const error = numRejected
? results.length > 1
? `${numRejected}/${results.length} updates were rejected, please retry.`
: 'The update was rejected, please retry.'
: undefined

useEffect(() => {
setResults(undefined)
}, [params.length])

return {
isLoading,
isSuccess,
error,
createIdentifications: async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is great! The async really pays off here. I see all 16 requests fire off at once and the backend seems to respond quickly.

const promises = params
.filter((_, index) => {
if (error) {
// Only retry rejected requests
return results?.[index]?.status === 'rejected'
}

return true
})
.map((variables) => createIdentification(variables))

setResults(undefined)
const result = await Promise.allSettled(promises)
setResults(result)
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import axios from 'axios'
import { API_ROUTES, API_URL } from 'data-services/constants'
import { getAuthHeader } from 'data-services/utils'
import { useUser } from 'utils/user/userContext'

export const useDeleteIdentification = (onSuccess?: () => void) => {
const { user } = useUser()
const queryClient = useQueryClient()

const { mutateAsync, isLoading, isSuccess, error } = useMutation({
mutationFn: (id: string) =>
axios.delete(`${API_URL}/${API_ROUTES.IDENTIFICATIONS}/${id}`, {
headers: getAuthHeader(user),
}),
onSuccess: () => {
queryClient.invalidateQueries([API_ROUTES.IDENTIFICATIONS])
queryClient.invalidateQueries([API_ROUTES.OCCURRENCES])
onSuccess?.()
},
})

return { deleteIdentification: mutateAsync, isLoading, isSuccess, error }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import 'src/design-system/variables/colors.scss';
@import 'src/design-system/variables/typography.scss';

.wrapper {
bottom: 72px;
position: fixed;
border-radius: 32px;
height: 64px;
padding: 0 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
background-color: $color-generic-white;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
}

.infoLabel {
display: block;
@include paragraph-small();
font-weight: 600;
color: $color-neutral-500;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ReactNode } from 'react'
import { IconButton, IconButtonTheme } from '../icon-button/icon-button'
import { IconType } from '../icon/icon'
import styles from './bulk-action-bar.module.scss'

interface BulkActionBarProps {
children: ReactNode
selectedItems: string[]
onClear: () => void
}

export const BulkActionBar = ({
children,
selectedItems,
onClear,
}: BulkActionBarProps) => (
<div className={styles.wrapper}>
<span className={styles.infoLabel}>{selectedItems.length} selected</span>
{children}
<IconButton
icon={IconType.Cross}
theme={IconButtonTheme.Plain}
onClick={onClear}
/>
</div>
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
box-shadow: 0 0 0 2px $color-generic-black;
}

&[data-state='checked'] {
&[data-state='checked'],
&[data-state='indeterminate'] {
background-color: $color-neutral-600;
border-color: $color-neutral-600;
}
Expand Down
51 changes: 28 additions & 23 deletions ui/src/design-system/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,42 @@ export enum CheckboxTheme {
}

interface CheckboxProps {
id: string
checked?: boolean | 'indeterminate'
defaultChecked?: boolean
id?: string
label?: string
theme?: CheckboxTheme
checked?: boolean
onCheckedChange?: (checked: boolean) => void
defaultChecked?: boolean
}

export const Checkbox = ({
checked,
defaultChecked,
id,
label,
theme = CheckboxTheme.Default,
checked,
onCheckedChange,
defaultChecked,
}: CheckboxProps) => {
return (
<div className={styles.wrapper}>
<_Checkbox.Root
id={id}
className={classNames(styles.checkboxRoot, {
[styles.neutral]: theme === CheckboxTheme.Neutral,
})}
checked={checked}
defaultChecked={defaultChecked}
onCheckedChange={onCheckedChange}
>
<_Checkbox.Indicator className={styles.checkboxIndicator}>
}: CheckboxProps) => (
<div className={styles.wrapper}>
<_Checkbox.Root
checked={checked}
className={classNames(styles.checkboxRoot, {
[styles.neutral]: theme === CheckboxTheme.Neutral,
})}
defaultChecked={defaultChecked}
id={id}
onCheckedChange={onCheckedChange}
>
<_Checkbox.Indicator className={styles.checkboxIndicator}>
{checked === true && (
<Icon type={IconType.RadixCheck} theme={IconTheme.Light} />
</_Checkbox.Indicator>
</_Checkbox.Root>
)}
{checked === 'indeterminate' && (
<Icon type={IconType.RadixMinus} theme={IconTheme.Light} />
)}
</_Checkbox.Indicator>
</_Checkbox.Root>
{label && (
<label
htmlFor={id}
className={classNames(styles.label, {
Expand All @@ -52,6 +57,6 @@ export const Checkbox = ({
>
{label}
</label>
</div>
)
}
)}
</div>
)
5 changes: 5 additions & 0 deletions ui/src/design-system/components/icon/assets/radix/minus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions ui/src/design-system/components/icon/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Error from './assets/radix/error.svg?react'
import ExternalLink from './assets/radix/external-link.svg?react'
import HeartFilled from './assets/radix/heart-filled.svg?react'
import Heart from './assets/radix/heart.svg?react'
import RadixMinus from './assets/radix/minus.svg?react'
import Options from './assets/radix/options.svg?react'
import Pencil from './assets/radix/pencil.svg?react'
import Plus from './assets/radix/plus.svg?react'
Expand Down Expand Up @@ -68,6 +69,7 @@ export enum IconType {
Plus = 'plus',
RadixCheck = 'radix-check',
RadixClock = 'radix-clock',
RadixMinus = 'radix-minus',
RadixQuestionMark = 'radix-question-mark',
RadixSearch = 'radix-search',
RadixTrash = 'radix-trash',
Expand Down Expand Up @@ -119,6 +121,7 @@ const COMPONENT_MAP: { [key in IconType]: FunctionComponent } = {
[IconType.Plus]: Plus,
[IconType.RadixCheck]: RadixCheck,
[IconType.RadixClock]: RadixClock,
[IconType.RadixMinus]: RadixMinus,
[IconType.RadixQuestionMark]: RadixQuestionMark,
[IconType.RadixSearch]: RadixSearch,
[IconType.RadixTrash]: RadixTrash,
Expand Down
8 changes: 8 additions & 0 deletions ui/src/design-system/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Root = ({
onOpenChange,
}: {
children: ReactNode

open?: boolean
onOpenChange?: (open: boolean) => void
}) => (
Expand All @@ -30,6 +31,7 @@ const Content = ({
ariaCloselabel,
children,
container,
disableOutsideClose,
hideClose,
side,
style,
Expand All @@ -38,6 +40,7 @@ const Content = ({
ariaCloselabel: string
children: ReactNode
container?: HTMLElement
disableOutsideClose?: boolean
hideClose?: boolean
side?: 'top' | 'right' | 'bottom' | 'left'
style?: CSSProperties
Expand All @@ -50,6 +53,11 @@ const Content = ({
sideOffset={6}
style={style}
collisionPadding={{ bottom: 64 }}
onInteractOutside={(e) => {
if (disableOutsideClose) {
e.preventDefault()
}
}}
>
{children}
{!hideClose && (
Expand Down
5 changes: 4 additions & 1 deletion ui/src/design-system/components/select/select.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
color: $color-neutral-800;
border: 1px solid $color-neutral-300;
box-sizing: border-box;
margin-bottom: 8px;

&:not(:last-child) {
margin-bottom: 8px;
}

span {
white-space: nowrap;
Expand Down
Loading