From 5dd0e2183f5919b58b3759c978e36e6be1ebb58c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 21 Jun 2023 14:57:33 +0800 Subject: [PATCH] Add missing bulk labels modal file --- .../templates/article/AddBulkLabelsModal.tsx | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 packages/web/components/templates/article/AddBulkLabelsModal.tsx diff --git a/packages/web/components/templates/article/AddBulkLabelsModal.tsx b/packages/web/components/templates/article/AddBulkLabelsModal.tsx new file mode 100644 index 000000000..f029dc1fd --- /dev/null +++ b/packages/web/components/templates/article/AddBulkLabelsModal.tsx @@ -0,0 +1,257 @@ +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 current = selectedLabels.labels ?? [] + const lowerCasedValue = value.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 ${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.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() + }} + > + + + + + + + + } + /> + + + + ) +}