diff --git a/frontend/webapp/components/destinations/configured-destination-fields/index.tsx b/frontend/webapp/components/destinations/configured-destination-fields/index.tsx index 5c8cd899e..a813a0427 100644 --- a/frontend/webapp/components/destinations/configured-destination-fields/index.tsx +++ b/frontend/webapp/components/destinations/configured-destination-fields/index.tsx @@ -30,9 +30,12 @@ const ItemValue = styled(Text)` export const ConfiguredDestinationFields: React.FC< ConfiguredDestinationFieldsProps > = ({ details }) => { - const parseValue = (value: string) => { + const parseValue = (value: any) => { try { const parsed = JSON.parse(value); + if (typeof parsed === 'string') { + return parsed; + } if (Array.isArray(parsed)) { return parsed @@ -40,6 +43,7 @@ export const ConfiguredDestinationFields: React.FC< if (typeof item === 'object' && item !== null) { return `${item.key}: ${item.value}`; } + return item; }) .join(', '); diff --git a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx index afadab7f3..27aa87bfb 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx @@ -13,9 +13,11 @@ function ModalActionComponent({ onNext, onBack, item, + isFormValid, }: { onNext: () => void; onBack: () => void; + isFormValid?: boolean; item: DestinationTypeItem | undefined; }) { return ( @@ -33,6 +35,7 @@ function ModalActionComponent({ label: 'DONE', onClick: onNext, variant: 'primary', + disabled: !isFormValid, }, ] : [] @@ -47,6 +50,7 @@ export function AddDestinationModal({ }: AddDestinationModalProps) { const submitRef = useRef<() => void | null>(null); const [selectedItem, setSelectedItem] = useState(); + const [isFormValid, setIsFormValid] = useState(false); function handleNextStep(item: DestinationTypeItem) { setSelectedItem(item); @@ -55,8 +59,9 @@ export function AddDestinationModal({ function renderModalBody() { return selectedItem ? ( ) : ( @@ -78,6 +83,7 @@ export function AddDestinationModal({ setSelectedItem(undefined)} + isFormValid={isFormValid} item={selectedItem} /> } diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx index 5c7474c74..1e31c9dac 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx @@ -64,7 +64,7 @@ const DestinationFilterComponent: React.FC = ({ diff --git a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx index 3f59b33a1..f316b7e14 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx @@ -64,13 +64,14 @@ const NotificationNoteWrapper = styled.div` interface ConnectDestinationModalBodyProps { destination: DestinationTypeItem | undefined; onSubmitRef: React.MutableRefObject<(() => void) | null>; + onFormValidChange: (isValid: boolean) => void; } export function ConnectDestinationModalBody({ destination, onSubmitRef, + onFormValidChange, }: ConnectDestinationModalBodyProps) { - const [formData, setFormData] = useState>({}); const [destinationName, setDestinationName] = useState(''); const [showConnectionError, setShowConnectionError] = useState(false); const [dynamicFields, setDynamicFields] = useState([]); @@ -118,11 +119,12 @@ export function ConnectDestinationModalBody({ if (destination.fields && field?.name in destination.fields) { return { ...field, - initialValue: destination.fields[field.name], + value: destination.fields[field.name], }; } return field; }); + setDynamicFields(newDynamicFields); } }, [data, destination]); @@ -130,45 +132,85 @@ export function ConnectDestinationModalBody({ useEffect(() => { // Assign handleSubmit to the onSubmitRef so it can be triggered externally onSubmitRef.current = handleSubmit; - }, [formData, destinationName, exportedSignals]); + }, [dynamicFields, destinationName, exportedSignals]); + + useEffect(() => { + const isFormValid = dynamicFields.every((field) => + field.required ? field.value : true + ); + + onFormValidChange(isFormValid); + }, [dynamicFields]); function handleDynamicFieldChange(name: string, value: any) { - setFormData((prev) => ({ ...prev, [name]: value })); + setShowConnectionError(false); + setDynamicFields((prev) => { + return prev.map((field) => { + if (field.name === name) { + return { ...field, value }; + } + return field; + }); + }); } function handleSignalChange(signal: string, value: boolean) { setExportedSignals((prev) => ({ ...prev, [signal]: value })); } - async function handleSubmit() { - const fields = Object.entries(formData).map(([name, value]) => ({ - key: name, - value, + function processFormFields() { + function processFieldValue(field) { + return field.componentType === 'dropdown' + ? field.value.value + : field.value; + } + + // Prepare fields for the request body + return dynamicFields.map((field) => ({ + key: field.name, + value: processFieldValue(field), })); + } + async function handleSubmit() { + // Helper function to process field values + function processFieldValue(field) { + return field.componentType === 'dropdown' + ? field.value.value + : field.value; + } + + // Prepare fields for the request body + const fields = processFormFields(); + + // Function to store configured destination function storeConfiguredDestination() { const destinationTypeDetails = dynamicFields.map((field) => ({ title: field.title, - value: formData[field.name], + value: processFieldValue(field), })); + // Add 'Destination name' as the first item destinationTypeDetails.unshift({ title: 'Destination name', value: destinationName, }); + // Construct the configured destination object const storedDestination: ConfiguredDestination = { exportedSignals, destinationTypeDetails, type: destination?.type || '', imageUrl: destination?.imageUrl || '', - category: '', + category: '', // Could be handled in a more dynamic way if needed displayName: destination?.displayName || '', }; + // Dispatch action to store the destination dispatch(addConfiguredDestination(storedDestination)); } + // Prepare the request body const body: DestinationInput = { name: destinationName, type: destination?.type || '', @@ -176,7 +218,13 @@ export function ConnectDestinationModalBody({ fields, }; - await connectEnv(body, storeConfiguredDestination); + try { + // Await connection and store the configured destination if successful + await connectEnv(body, storeConfiguredDestination); + } catch (error) { + console.error('Failed to submit destination configuration:', error); + // Handle error (e.g., show notification or alert) + } } if (!destination) return null; @@ -194,15 +242,15 @@ export function ConnectDestinationModalBody({ actionButton={ destination.testConnectionSupported ? ( setShowConnectionError(true)} + onError={() => { + setShowConnectionError(true); + onFormValidChange(false); + }} destination={{ name: destinationName, type: destination?.type || '', exportedSignals, - fields: Object.entries(formData).map(([name, value]) => ({ - key: name, - value, - })), + fields: processFormFields(), }} /> ) : ( diff --git a/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx index 813602dd8..b935a558c 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx @@ -32,7 +32,9 @@ export function DynamicConnectDestinationFormFields({ onChange(field.name, option.value)} + onSelect={(option) => + onChange(field.name, { id: option.id, value: option.value }) + } /> ); case INPUT_TYPES.MULTI_INPUT: diff --git a/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts b/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts index 8165c1194..7f95908c5 100644 --- a/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts +++ b/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts @@ -37,7 +37,7 @@ export function useConnectDestinationForm() { title: displayName, onSelect: () => {}, options, - selectedOption: options[0], + placeholder: 'Select an option', ...componentPropertiesJson, }; diff --git a/frontend/webapp/reuseable-components/dropdown/index.tsx b/frontend/webapp/reuseable-components/dropdown/index.tsx index b43634117..1e7fafbb5 100644 --- a/frontend/webapp/reuseable-components/dropdown/index.tsx +++ b/frontend/webapp/reuseable-components/dropdown/index.tsx @@ -10,10 +10,11 @@ import { useOnClickOutside } from '@/hooks'; interface DropdownProps { options: DropdownOption[]; - selectedOption: DropdownOption | undefined; + value: DropdownOption | undefined; onSelect: (option: DropdownOption) => void; title?: string; tooltip?: string; + placeholder?: string; } const Container = styled.div` @@ -105,10 +106,11 @@ const OpenDropdownIcon = styled(Image)<{ isOpen: boolean }>` const Dropdown: React.FC = ({ options, - selectedOption, + value, onSelect, title, tooltip, + placeholder, }) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); @@ -143,7 +145,7 @@ const Dropdown: React.FC = ({ )} setIsOpen(!isOpen)}> - {selectedOption?.value} + {value?.value || placeholder} = ({ {filteredOptions.map((option) => ( handleSelect(option)} > {option.value} - {option.id === selectedOption?.id && ( + {option.id === value?.id && ( void; } @@ -55,19 +56,20 @@ const Title = styled(Text)` font-size: 14px; opacity: 0.8; line-height: 22px; - margin-bottom: 4px; `; const HeaderWrapper = styled.div` display: flex; align-items: center; gap: 6px; + margin-bottom: 4px; `; const InputList: React.FC = ({ initialValues = [''], title, tooltip, + required, onChange, }) => { const [inputs, setInputs] = useState(initialValues); @@ -102,6 +104,11 @@ const InputList: React.FC = ({ {title} + {!required && ( + + (optional) + + )} {tooltip && ( void; } @@ -23,6 +24,7 @@ const HeaderWrapper = styled.div` display: flex; align-items: center; gap: 6px; + margin-bottom: 4px; `; const Row = styled.div` @@ -61,13 +63,13 @@ const Title = styled(Text)` font-size: 14px; opacity: 0.8; line-height: 22px; - margin-bottom: 4px; `; export const KeyValueInputsList: React.FC = ({ initialKeyValuePairs = [{ key: '', value: '' }], title, tooltip, + required, onChange, }) => { const [keyValuePairs, setKeyValuePairs] = @@ -124,6 +126,11 @@ export const KeyValueInputsList: React.FC = ({ {title} + {!required && ( + + (optional) + + )} {tooltip && (