import React from "react"; import { flatMap, get, isArray, isNil, isObject } from "lodash"; import { useTranslations } from "next-intl"; import { Controller, FieldPath, FieldValues, useFormContext, } from "react-hook-form"; import Select, { ClassNamesConfig, GroupBase, OnChangeValue, Props, } from "react-select"; import { twMerge } from "tailwind-merge"; import { Prettify } from "@/types"; import { Field, GenericFieldProps } from "./Field"; export type BaseOption = { value: T; label: string | JSX.Element; disabled?: boolean; data?: D; }; export const classNames = ( hasError: boolean, ): ClassNamesConfig> => ({ container: () => "w-full", valueContainer: () => "text-gray-900 dark:text-white", indicatorsContainer: () => "flex items-center self-stretch shrink-0", clearIndicator: () => "pointer-events-none flex items-center pr-2 text-gray-900 dark:text-white", dropdownIndicator: () => "pointer-events-none flex items-center pr-2 text-gray-900 dark:text-white", indicatorSeparator: () => "w-px text-gray-900 dark:text-white", control: (state) => twMerge( "group flex w-full items-center justify-between rounded-lg border p-3 text-sm", "border-gray-300 bg-gray-50 text-gray-900 shadow-sm focus-within:border-primary-500 focus-within:ring-primary-500", "dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500", hasError && "ring-1 ring-error", state.isDisabled && "cursor-not-allowed", state.isFocused && "border-primary ring ring-primary ring-opacity-50", ), multiValue: () => "bg-fieldGray border rounded-lg flex space-x-1 pl-1 m-1", multiValueLabel: () => "", multiValueRemove: () => "items-center px-1 hover:text-primary", placeholder: () => "block truncate pr-2 placeholder-gray dark:placeholder-gray-400", menu: () => twMerge( "!z-10 mt-2 rounded-lg border border-gray-200 bg-white p-1", "border-gray-300 bg-gray-50 text-gray-900", "dark:border-gray-600 dark:bg-gray-700 dark:text-white", ), groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase", option: ({ isFocused, isDisabled }) => twMerge( "px-3 py-2 hover:cursor-pointer", isDisabled && "opacity-50", isFocused && "hover:bg-primary-500 hover:text-white", ), noOptionsMessage: () => "text-textGray p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm", }); export type SelectFieldProps< TFieldValues extends FieldValues = FieldValues, TFieldName extends FieldPath = FieldPath, IsMulti extends boolean = false, T = string, D = unknown, > = Prettify< GenericFieldProps & Omit< Props, IsMulti, GroupBase>>, "onChange" | "value" | "classNames" | "name" > & { required?: boolean; } >; export const SelectField = < TFieldValues extends FieldValues = FieldValues, TFieldName extends FieldPath = FieldPath, IsMulti extends boolean = false, T = string, D = unknown, >({ name, control, className, labelClassName, childrenWrapperClassName, label, hideErrorMessage, required, placeholder, options, ...selectProps }: SelectFieldProps) => { const t = useTranslations("App"); const form = useFormContext(); const { formState: { errors }, } = form; const hasError = !!get(errors, name)?.message; const isGroup = ( option: BaseOption | GroupBase>, ): option is GroupBase> => isObject(option) && "options" in option; const flattenedOptions = flatMap(options, (option) => { return isGroup(option) ? option.options : option; }); const valueToOption = (value: T): BaseOption => flattenedOptions.find((o) => o.value === value) ?? { value, label: "" }; return ( { const onSelectChange = ( newValue: OnChangeValue, IsMulti>, ) => { if (isNil(newValue)) onChange(null); else if (isArray(newValue)) { onChange(newValue.map((option) => option.value)); } else onChange((newValue as BaseOption).value); }; const optionValue = isNil(value) ? null : isArray(value) ? // @ts-ignore - this is too complex for TS to understand value.map(valueToOption) : valueToOption(value); return (