From 67b9d494c672ca844c8e020f9fbac3a47faaf52a Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 24 Apr 2024 12:11:46 -0400 Subject: [PATCH] feat: add freeform select --- .../components/blocks/forms/CoBenifetForm.tsx | 1 + .../components/blocks/forms/IssuanceForm.tsx | 1 + .../components/blocks/forms/ProjectForm.tsx | 14 +- src/renderer/components/form/Field.tsx | 6 +- src/renderer/components/proxy/Select.tsx | 131 +++++++++++++----- 5 files changed, 117 insertions(+), 36 deletions(-) diff --git a/src/renderer/components/blocks/forms/CoBenifetForm.tsx b/src/renderer/components/blocks/forms/CoBenifetForm.tsx index 8c6a550f..aa7db643 100644 --- a/src/renderer/components/blocks/forms/CoBenifetForm.tsx +++ b/src/renderer/components/blocks/forms/CoBenifetForm.tsx @@ -55,6 +55,7 @@ const CoBenefitForm = forwardRef( name={`${name}[${index}].cobenefit`} label="Co-Benefit" type="picklist" + freeform={true} options={picklistOptions?.coBenefits} readonly={readonly} initialValue={coBenefit.cobenefit} diff --git a/src/renderer/components/blocks/forms/IssuanceForm.tsx b/src/renderer/components/blocks/forms/IssuanceForm.tsx index 2ecc96ad..4d85ca7c 100644 --- a/src/renderer/components/blocks/forms/IssuanceForm.tsx +++ b/src/renderer/components/blocks/forms/IssuanceForm.tsx @@ -93,6 +93,7 @@ const IssuanceForm = forwardRef( name={`${name}[${index}].verificationBody`} label="Verification Body" type="picklist" + freeform={true} options={picklistOptions?.verificationBody} readonly={readonly} initialValue={issuance.verificationBody || ''} diff --git a/src/renderer/components/blocks/forms/ProjectForm.tsx b/src/renderer/components/blocks/forms/ProjectForm.tsx index b91fd0c9..155bf89f 100644 --- a/src/renderer/components/blocks/forms/ProjectForm.tsx +++ b/src/renderer/components/blocks/forms/ProjectForm.tsx @@ -147,13 +147,16 @@ const ProjectForm: React.FC = forwardRef = forwardRef @@ -230,6 +237,7 @@ const ProjectForm: React.FC = forwardRef { } }; -const Field: React.FC = ({ name, label, type, options, readonly, initialValue, disabled = false }) => { +const Field: React.FC = ({ name, label, type, options, readonly, initialValue, disabled = false, freeform = false }) => { const { errors, setFieldValue }: FormikValues = useFormikContext(); const isError: boolean = !!get(errors, name); @@ -108,8 +109,9 @@ const Field: React.FC = ({ name, label, type, options, readonly, ini id={name} name={name} disabled={disabled} + freeform={freeform} initialValue={initialValue} - onChange={(e) => setFieldValue(name, e.target.value)} + onChange={(value) => setFieldValue(name, value)} options={options} /> ); diff --git a/src/renderer/components/proxy/Select.tsx b/src/renderer/components/proxy/Select.tsx index a60e7871..855fc0af 100644 --- a/src/renderer/components/proxy/Select.tsx +++ b/src/renderer/components/proxy/Select.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Select as FlowbiteSelect } from 'flowbite-react'; +import { PiCaretDown } from "react-icons/pi"; export type SelectOption = { label: string; @@ -11,53 +12,121 @@ interface SelectProps { initialValue?: string | number; id?: string; name: string; - onChange?: (event: React.ChangeEvent) => void; + onChange?: (value: string | number) => void; required?: boolean; disabled?: boolean; + freeform?: boolean; // Allows typing and filtering } -const Select: React.FC = ({ options, initialValue, id, name, onChange, required = false, disabled = false }) => { - // Helper function to determine the display text for each option - const getOptionLabel = (option: SelectOption): string => { - if (typeof option === 'object') { - return option.label; - } else { - return option.toString(); +const Select: React.FC = ({ + options = [], + initialValue, + id, + name, + onChange, + required = false, + disabled = false, + freeform = false +}) => { + const [inputValue, setInputValue] = useState(initialValue); + const [filteredOptions, setFilteredOptions] = useState(options); + const [dropdownVisible, setDropdownVisible] = useState(false); + + useEffect(() => { + setFilteredOptions(options); + }, [options]); + + const handleInputChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setInputValue(value); + if (freeform) { + const lowerValue = value.toLowerCase(); + setFilteredOptions( + options.filter(option => { + const optionValue = typeof option === 'object' ? option.label.toLowerCase() : option.toString().toLowerCase(); + return optionValue.includes(lowerValue); + }) + ); } + if (onChange) { + onChange(value); + } + }; + + const toggleDropdown = () => { + setDropdownVisible(!dropdownVisible); }; - // Helper function to determine the value for each option - const getOptionValue = (option: SelectOption): string | number => { - if (typeof option === 'object') { - return option.value; - } else { - return option; + const clearInput = () => { + setInputValue(''); + setFilteredOptions(options); + setDropdownVisible(false); + if (onChange) { + onChange(''); } }; + if (freeform) { + return ( +
+
+ setDropdownVisible(true)} + onBlur={() => setTimeout(() => setDropdownVisible(false), 200)} + disabled={disabled} + className="border-none form-input block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding rounded-l-lg transition ease-in-out focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" + /> + {inputValue && ( + + )} + +
+ {dropdownVisible && ( +
    + {filteredOptions.map((option, index) => ( +
  • { + setInputValue(typeof option === 'object' ? option.value : option); + setFilteredOptions(options); // reset filter on selection + setDropdownVisible(false); + if (onChange) { + onChange(typeof option === 'object' ? option.value : option); + } + }} + className="cursor-pointer px-4 py-2 hover:bg-gray-100"> + {typeof option === 'object' ? option.label : option} +
  • + ))} +
+ )} +
+ ); + } + return ( -
+
setInputValue(e.target.value)} disabled={disabled} + required={required} > - - {options && options.length > 0 ? ( - options.map((option, index) => ( - - )) - ) : ( - - )} + ))}
);