import { useCallback, useRef, useState, useMemo, useEffect } from 'react' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' import { styled, theme } from '../../tokens/stitches.config' import { Label } from '../../../lib/networking/fragments/labelFragment' import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' import { Check, Circle, Plus, WarningCircle } from '@phosphor-icons/react' import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' import { useRouter } from 'next/router' import { LabelsPicker } from '../../elements/LabelsPicker' import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels' export interface LabelsProvider { labels?: Label[] } type SetLabelsControlProps = { inputValue: string setInputValue: (value: string) => void clearInputState: () => void selectedLabels: Label[] dispatchLabels: LabelsDispatcher tabCount: number setTabCount: (count: number) => void tabStartValue: string setTabStartValue: (value: string) => void highlightLastLabel: boolean setHighlightLastLabel: (set: boolean) => void deleteLastLabel: () => void selectOrCreateLabel: (value: string) => void errorMessage?: string footer?: React.ReactNode } type HeaderProps = SetLabelsControlProps & { focused: boolean resetFocusedIndex: () => void } const StyledLabel = styled('label', { display: 'flex', justifyContent: 'flex-start', }) function Header(props: HeaderProps): JSX.Element { return ( { props.resetFocusedIndex() }} clearInputState={props.clearInputState} deleteLastLabel={props.deleteLastLabel} selectOrCreateLabel={props.selectOrCreateLabel} /> ) } type LabelListItemProps = { label: Label focused: boolean selected: boolean toggleLabel: (label: Label) => void } function LabelListItem(props: LabelListItemProps): JSX.Element { const ref = useRef(null) const { label, focused, selected } = props useEffect(() => { if (props.focused && ref.current) { ref.current.focus() } }, [props.focused]) return ( { event.preventDefault() props.toggleLabel(label) ref.current?.blur() }} > {label.name} {selected && ( )} ) } type FooterProps = { focused: boolean filterText: string selectedLabels: Label[] availableLabels: Label[] createEnteredLabel: () => Promise selectEnteredLabel: () => Promise } function Footer(props: FooterProps): JSX.Element { const ref = useRef(null) useEffect(() => { if (props.focused && ref.current) { ref.current.focus() } }, [props.focused]) const textMatch: 'selected' | 'available' | 'none' = useMemo(() => { const findLabel = (l: Label) => l.name.toLowerCase() == props.filterText.toLowerCase() const available = props.availableLabels.find(findLabel) const selected = props.selectedLabels.find(findLabel) if (available && !selected) { return 'available' } if (selected) { return 'selected' } return 'none' }, [props]) const trimmedLabelName = useMemo(() => { return props.filterText.trim() }, [props]) return ( {trimmedLabelName.length > 0 ? ( ) : ( )} ) } export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { const router = useRouter() const { inputValue, setInputValue, selectedLabels, setHighlightLastLabel } = props const { labels, revalidate } = useGetLabelsQuery() // Move focus through the labels list on tab or arrow up/down keys const [focusedIndex, setFocusedIndex] = useState(0) useEffect(() => { setFocusedIndex(undefined) }, [inputValue]) const isSelected = useCallback( (label: Label): boolean => { return selectedLabels.some((other) => { return other.id === label.id }) }, [selectedLabels] ) useEffect(() => { if (focusedIndex === 0) { setHighlightLastLabel(false) } }, [setHighlightLastLabel, focusedIndex]) const toggleLabel = useCallback( async (label: Label) => { let newSelectedLabels = [...props.selectedLabels] if (isSelected(label)) { newSelectedLabels = props.selectedLabels.filter((other) => { return other.id !== label.id }) } else { newSelectedLabels = [...props.selectedLabels, label] } props.dispatchLabels({ type: 'SAVE', labels: newSelectedLabels }) props.clearInputState() revalidate() }, [isSelected, props, revalidate] ) const filteredLabels = useMemo(() => { if (!labels) { return [] } return labels .filter((label) => { return label.name.toLowerCase().includes(inputValue.toLowerCase()) }) .sort((left: Label, right: Label) => { return left.name.localeCompare(right.name) }) }, [labels, inputValue]) const createLabelFromFilterText = useCallback( async (text: string) => { const trimmedLabelName = text.trim() const label = await createLabelMutation( trimmedLabelName, randomLabelColorHex(), '' ) if (label) { showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right', }) toggleLabel(label) } else { showErrorToast('Failed to create label', { position: 'bottom-right' }) } }, [toggleLabel] ) const handleKeyDown = useCallback( async (event: React.KeyboardEvent) => { const maxIndex = filteredLabels.length + 1 if (event.key === 'ArrowUp') { event.preventDefault() let newIndex = focusedIndex if (focusedIndex) { newIndex = Math.max(0, focusedIndex - 1) } else { newIndex = undefined } // If the `Create New label` button isn't visible we skip it // when navigating with the arrow keys if (focusedIndex === maxIndex && !inputValue) { newIndex = maxIndex - 2 } setFocusedIndex(newIndex) } if (event.key === 'ArrowDown') { event.preventDefault() let newIndex = focusedIndex if (focusedIndex === undefined) { newIndex = 0 } else { newIndex = Math.min(maxIndex, focusedIndex + 1) } // If the `Create New label` button isn't visible we skip it // when navigating with the arrow keys if (focusedIndex === maxIndex - 2 && !inputValue) { newIndex = maxIndex } setFocusedIndex(newIndex) } if (event.key === 'Enter') { event.preventDefault() if (focusedIndex === maxIndex) { const _filterText = inputValue setInputValue('') await createLabelFromFilterText(_filterText) return } if (focusedIndex !== undefined) { const label = filteredLabels[focusedIndex] if (label) { toggleLabel(label) } } } }, [ inputValue, setInputValue, filteredLabels, focusedIndex, createLabelFromFilterText, toggleLabel, ] ) const createEnteredLabel = useCallback(() => { const _filterText = inputValue setInputValue('') return createLabelFromFilterText(_filterText) }, [inputValue, setInputValue, createLabelFromFilterText]) const selectEnteredLabel = useCallback(() => { const label = labels.find( (l: Label) => l.name.toLowerCase() == inputValue.toLowerCase() ) if (!label) { return Promise.resolve() } return toggleLabel(label) }, [labels, inputValue, toggleLabel]) return (
setFocusedIndex(undefined)} inputValue={inputValue} setInputValue={setInputValue} selectedLabels={props.selectedLabels} dispatchLabels={props.dispatchLabels} tabCount={props.tabCount} setTabCount={props.setTabCount} tabStartValue={props.tabStartValue} setTabStartValue={props.setTabStartValue} highlightLastLabel={props.highlightLastLabel} setHighlightLastLabel={props.setHighlightLastLabel} deleteLastLabel={props.deleteLastLabel} selectOrCreateLabel={props.selectOrCreateLabel} clearInputState={props.clearInputState} /> {props.errorMessage && ( <> {props.errorMessage} )} {filteredLabels.map((label, idx) => ( ))} {props.footer ? ( props.footer ) : (