import { useCallback, useRef, useState, useMemo, useEffect } from 'react' import Link from 'next/link' 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, PencilSimple, Plus, WarningCircle, } from 'phosphor-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' export interface LabelsProvider { labels?: Label[] } type SetLabelsControlProps = { provider: LabelsProvider inputValue: string setInputValue: (value: string) => void clearInputState: () => void selectedLabels: Label[] setSelectedLabels: (labels: Label[]) => void onLabelsUpdated?: (labels: Label[]) => void 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 } type HeaderProps = { focused: boolean resetFocusedIndex: () => void inputValue: string setInputValue: (value: string) => void clearInputState: () => void selectedLabels: Label[] setSelectedLabels: (labels: Label[]) => void tabCount: number setTabCount: (count: number) => void tabStartValue: string setTabStartValue: (value: string) => void highlightLastLabel: boolean setHighlightLastLabel: (set: boolean) => void deleteLastLabel: () => void selectOrCreateLabel: (value: string) => 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 } function Footer(props: FooterProps): JSX.Element { const ref = useRef(null) useEffect(() => { if (props.focused && ref.current) { ref.current.focus() } }, [props.focused]) return ( {props.filterText.length > 0 ? ( ) : ( )} ) } export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { const router = useRouter() const { labels, revalidate } = useGetLabelsQuery() // Move focus through the labels list on tab or arrow up/down keys const [focusedIndex, setFocusedIndex] = useState( undefined ) useEffect(() => { setFocusedIndex(undefined) }, [props.inputValue]) const isSelected = useCallback( (label: Label): boolean => { return props.selectedLabels.some((other) => { return other.id === label.id }) }, [props.selectedLabels] ) useEffect(() => { if (focusedIndex === 0) { props.setHighlightLastLabel(false) } }, [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.setSelectedLabels(newSelectedLabels) props.provider.labels = newSelectedLabels if (props.onLabelsUpdated) { props.onLabelsUpdated(newSelectedLabels) } props.clearInputState() revalidate() }, [isSelected, props, revalidate] ) const filteredLabels = useMemo(() => { if (!labels) { return [] } return labels .filter((label) => { return label.name.toLowerCase().includes(props.inputValue.toLowerCase()) }) .sort((left: Label, right: Label) => { return left.name.localeCompare(right.name) }) }, [labels, props.inputValue]) const createLabelFromFilterText = useCallback( async (text: string) => { const label = await createLabelMutation(text, randomLabelColorHex(), '') if (label) { showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right', }) toggleLabel(label) } else { showErrorToast('Failed to create label', { position: 'bottom-right' }) } }, [props.inputValue, 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 && !props.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 && !props.inputValue) { newIndex = maxIndex } setFocusedIndex(newIndex) } if (event.key === 'Enter') { event.preventDefault() if (focusedIndex === maxIndex) { router.push('/settings/labels') return } if (focusedIndex === maxIndex - 1) { const _filterText = props.inputValue props.setInputValue('') await createLabelFromFilterText(_filterText) return } if (focusedIndex !== undefined) { const label = filteredLabels[focusedIndex] if (label) { toggleLabel(label) } } } }, [ props.inputValue, filteredLabels, focusedIndex, createLabelFromFilterText, router, toggleLabel, ] ) return (
setFocusedIndex(undefined)} inputValue={props.inputValue} setInputValue={props.setInputValue} selectedLabels={props.selectedLabels} setSelectedLabels={props.setSelectedLabels} 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) => ( ))}