import { useMemo, useState } from 'react'; import { Wrap, VStack, Flex, Box, Avatar, chakra } from '@chakra-ui/react'; import { motion } from 'framer-motion'; import { useFormContext } from 'react-hook-form'; import { Select } from '~/components'; import { useConfig, useColorValue } from '~/context'; import { useOpposingColor, useFormState } from '~/hooks'; import type { DeviceGroup, SingleOption, OptionGroup, FormData } from '~/types'; import type { TQuerySelectField, LocationCardProps } from './types'; function buildOptions(devices: DeviceGroup[]): OptionGroup[] { return devices .map(group => { const label = group.group; const options = group.locations .map( loc => ({ label: loc.name, value: loc.id, group: loc.group, data: { avatar: loc.avatar, description: loc.description, }, } as SingleOption), ) .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); return { label, options }; }) .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); } const MotionBox = motion(Box); const LocationCard = (props: LocationCardProps): JSX.Element => { const { option, onChange, defaultChecked, hasError } = props; const { label } = option; const [isChecked, setChecked] = useState(defaultChecked); function handleChange(value: SingleOption) { if (isChecked) { setChecked(false); onChange('remove', value); } else { setChecked(true); onChange('add', value); } } const bg = useColorValue('whiteAlpha.300', 'blackSolid.700'); const imageBorder = useColorValue('gray.600', 'whiteAlpha.800'); const fg = useOpposingColor(bg); const checkedBorder = useColorValue('blue.400', 'blue.300'); const errorBorder = useColorValue('red.500', 'red.300'); const borderColor = useMemo( () => hasError && isChecked ? // Highlight red when there are no overlapping query types for the locations selected. errorBorder : isChecked && !hasError ? // Highlight blue when any location is selected and there is no error. checkedBorder : // Otherwise, no border. 'transparent', [hasError, isChecked, checkedBorder, errorBorder], ); return ( { e.preventDefault(); handleChange(option); }} > {label} {option?.data?.description && ( {option.data.description as string} )} ); }; export const QueryLocation = (props: TQuerySelectField): JSX.Element => { const { onChange, label } = props; const { devices } = useConfig(); const { formState: { errors }, } = useFormContext(); const selections = useFormState(s => s.selections); const setSelection = useFormState(s => s.setSelection); const { form, filtered } = useFormState(({ form, filtered }) => ({ form, filtered })); const options = useMemo(() => buildOptions(devices), [devices]); const element = useMemo(() => { const groups = options.length; const maxOptionsPerGroup = Math.max(...options.map(opt => opt.options.length)); const showCards = groups < 5 && maxOptionsPerGroup < 6; return showCards ? 'cards' : 'select'; }, [options]); const noOverlap = useMemo( () => form.queryLocation.length > 1 && filtered.types.length === 0, [form, filtered], ); /** * Update form and state when a card selections change. * * @param action Add or remove the option. * @param option Full option object. */ function handleCardChange(action: 'add' | 'remove', option: SingleOption) { const exists = selections.queryLocation.map(q => q.value).includes(option.value); if (action === 'add' && !exists) { const toAdd = [...form.queryLocation, option.value]; const newSelections = [...selections.queryLocation, option]; setSelection('queryLocation', newSelections); onChange({ field: 'queryLocation', value: toAdd }); } else if (action === 'remove' && exists) { const index = selections.queryLocation.findIndex(v => v.value === option.value); const toRemove = [...form.queryLocation.filter(v => v !== option.value)]; setSelection( 'queryLocation', selections.queryLocation.filter((_, i) => i !== index), ); onChange({ field: 'queryLocation', value: toRemove }); } } /** * Update form and state when select element values change. * * @param options Final value. React-select determines if an option is being added or removed and * only sends back the final value. */ function handleSelectChange(options: SingleOption[] | SingleOption): void { if (Array.isArray(options)) { onChange({ field: 'queryLocation', value: options.map(o => o.value) }); setSelection('queryLocation', options); } else { onChange({ field: 'queryLocation', value: options.value }); setSelection('queryLocation', [options]); } } if (element === 'cards') { return ( {options.map(group => ( {group.label} {group.options.map(opt => { return ( ); })} ))} ); } else if (element === 'select') { return (