import { useCallback, useEffect, useReducer, useRef, useState } from 'react' import { Label } from '../../../lib/networking/fragments/labelFragment' import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { ModalRoot, ModalOverlay, ModalContent, ModalTitleBar, } from '../../elements/ModalPrimitives' import { 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 { LabelAction } from '../../../lib/hooks/useSetPageLabels' import { Button } from '../../elements/Button' type AddBulkLabelsModalProps = { onOpenChange: (open: boolean) => void bulkSetLabels: (labels: Label[]) => void } export function AddBulkLabelsModal( props: AddBulkLabelsModalProps ): JSX.Element { const availableLabels = useGetLabelsQuery() const [tabCount, setTabCount] = useState(-1) const [inputValue, setInputValue] = useState('') const [tabStartValue, setTabStartValue] = useState('') const [errorMessage, setErrorMessage] = useState(undefined) const errorTimeoutRef = useRef() const [highlightLastLabel, setHighlightLastLabel] = useState(false) const [isSaving, setIsSaving] = useState(false) const labelsReducer = ( state: { labels: Label[] }, action: { type: LabelAction labels: Label[] } ) => { return { labels: action.labels, } } const [selectedLabels, dispatchLabels] = useReducer(labelsReducer, { labels: [], }) 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 trimmedValue = value.trim() const current = selectedLabels.labels ?? [] const lowerCasedValue = trimmedValue.toLowerCase() const existing = availableLabels.labels.find( (l) => l.name.toLowerCase() == lowerCasedValue ) if (lowerCasedValue.length < 1) { return } if (existing) { const isAdded = selectedLabels.labels.find( (l) => l.name.toLowerCase() == lowerCasedValue ) if (!isAdded) { dispatchLabels({ type: 'SAVE', labels: [...current, existing] }) clearInputState() } else { showMessage(`label ${trimmedValue} already added.`, 5000) } } else { const tempLabel = { id: uuidv4(), name: trimmedValue, 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.labels current.pop() dispatchLabels({ type: 'SAVE', labels: [...current] }) setHighlightLastLabel(false) } else { setHighlightLastLabel(true) } }, [highlightLastLabel, selectedLabels, dispatchLabels]) const handleSave = useCallback(() => { props.bulkSetLabels(selectedLabels.labels) props.onOpenChange(true) }, [selectedLabels]) return ( { event.preventDefault() props.onOpenChange(false) }} onEscapeKeyDown={(event) => { props.onOpenChange(false) event.preventDefault() }} > } /> ) }