Files
omnivore/packages/web/components/templates/article/SetLabelsModal.tsx
2024-03-05 09:34:36 +08:00

219 lines
6.8 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { SpanBox, VStack } from '../../elements/LayoutPrimitives'
import {
ModalRoot,
ModalOverlay,
ModalContent,
ModalTitleBar,
} from '../../elements/ModalPrimitives'
import { LabelsProvider, SetLabelsControl } from './SetLabelsControl'
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
import { showSuccessToast } from '../../../lib/toastHelpers'
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
import { v4 as uuidv4 } from 'uuid'
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
import * as Dialog from '@radix-ui/react-dialog'
type SetLabelsModalProps = {
provider: LabelsProvider
onOpenChange: (open: boolean) => void
selectedLabels: Label[]
dispatchLabels: LabelsDispatcher
}
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
const [inputValue, setInputValue] = useState('')
const { selectedLabels, dispatchLabels } = props
const availableLabels = useGetLabelsQuery()
const [tabCount, setTabCount] = useState(-1)
const [tabStartValue, setTabStartValue] = useState('')
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined
)
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
const showMessage = useCallback(
(msg: string, timeout?: number) => {
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current)
errorTimeoutRef.current = undefined
}
setErrorMessage(msg)
if (timeout) {
errorTimeoutRef.current = setTimeout(() => {
setErrorMessage(undefined)
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current)
errorTimeoutRef.current = undefined
}
}, timeout)
}
},
[errorTimeoutRef]
)
useEffect(() => {
const maxLengthMessage = 'Max label length: 48 chars'
if (inputValue.length >= 48) {
showMessage(maxLengthMessage)
} else if (errorMessage === maxLengthMessage) {
setErrorMessage(undefined)
}
if (inputValue.length > 0) {
setHighlightLastLabel(false)
}
}, [errorMessage, inputValue, showMessage])
const clearInputState = useCallback(() => {
setTabCount(-1)
setInputValue('')
setTabStartValue('')
setHighlightLastLabel(false)
}, [])
const createLabelAsync = useCallback(
(newLabels: Label[], tempLabel: Label) => {
;(async () => {
const currentLabels = newLabels
const newLabel = await createLabelMutation(
tempLabel.name,
tempLabel.color
)
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
if (newLabel) {
showSuccessToast(`Created label ${newLabel.name}`, {
position: 'bottom-right',
})
if (idx !== -1) {
currentLabels[idx] = newLabel
dispatchLabels({ type: 'SAVE', labels: [...currentLabels] })
} else {
dispatchLabels({
type: 'SAVE',
labels: [...currentLabels, newLabel],
})
}
} else {
showMessage(`Error creating label ${tempLabel.name}`, 5000)
if (idx !== -1) {
currentLabels.splice(idx, 1)
dispatchLabels({ type: 'SAVE', labels: [...currentLabels] })
}
}
})()
},
[dispatchLabels, showMessage]
)
const selectOrCreateLabel = useCallback(
(value: string) => {
const current = selectedLabels ?? []
const lowerCasedValue = value.toLowerCase()
const existing = availableLabels.labels.find(
(l) => l.name.toLowerCase() == lowerCasedValue
)
if (lowerCasedValue.length < 1) {
return
}
if (existing) {
const isAdded = selectedLabels.find(
(l) => l.name.toLowerCase() == lowerCasedValue
)
if (!isAdded) {
dispatchLabels({ type: 'SAVE', labels: [...current, existing] })
clearInputState()
} else {
showMessage(`label ${value} already added.`, 5000)
}
} else {
const tempLabel = {
id: uuidv4(),
name: value,
color: randomLabelColorHex(),
description: '',
createdAt: new Date(),
}
const newLabels = [...current, tempLabel]
dispatchLabels({ type: 'TEMP', labels: newLabels })
clearInputState()
createLabelAsync(newLabels, tempLabel)
}
},
[
availableLabels,
selectedLabels,
dispatchLabels,
clearInputState,
createLabelAsync,
showMessage,
]
)
const deleteLastLabel = useCallback(() => {
if (highlightLastLabel) {
const current = selectedLabels
current.pop()
dispatchLabels({ type: 'SAVE', labels: [...current] })
setHighlightLastLabel(false)
} else {
setHighlightLastLabel(true)
}
}, [highlightLastLabel, selectedLabels, dispatchLabels])
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<Dialog.Portal>
<ModalOverlay />
<ModalContent
tabIndex={0}
css={{
border: '1px solid $grayBorder',
backgroundColor: '$thBackground',
}}
onPointerDownOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
onEscapeKeyDown={(event) => {
props.onOpenChange(false)
event.preventDefault()
event.stopPropagation()
}}
>
<VStack distribution="start" css={{ height: '100%' }}>
<SpanBox css={{ pt: '0px', px: '16px', width: '100%' }}>
<ModalTitleBar title="Labels" onOpenChange={props.onOpenChange} />
</SpanBox>
<SetLabelsControl
inputValue={inputValue}
setInputValue={setInputValue}
clearInputState={clearInputState}
selectedLabels={props.selectedLabels}
dispatchLabels={props.dispatchLabels}
tabCount={tabCount}
setTabCount={setTabCount}
tabStartValue={tabStartValue}
setTabStartValue={setTabStartValue}
highlightLastLabel={highlightLastLabel}
setHighlightLastLabel={setHighlightLastLabel}
deleteLastLabel={deleteLastLabel}
selectOrCreateLabel={selectOrCreateLabel}
errorMessage={errorMessage}
/>
</VStack>
</ModalContent>
</Dialog.Portal>
</ModalRoot>
)
}