From 4d54aca86bc9e5eac8f3edf993f73791a9e8dc17 Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Thu, 16 Oct 2025 14:11:23 +0530 Subject: [PATCH 1/7] Fix pre-configured props handling in forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issues Fixed ### Issue 1: Remote options not loading with pre-configured values - **Problem**: When mounting ComponentFormContainer with pre-configured props, remote options dropdowns showed "No options" even though the API returned data - **Root Cause**: queryDisabledIdx initialization used _configuredProps (empty) instead of actual configuredProps, incorrectly blocking queries. RemoteOptionsContainer also didn't sync cached query data with component state on remount - **Files**: form-context.tsx, RemoteOptionsContainer.tsx ### Issue 2: Optional props not auto-enabling when pre-configured - **Problem**: Optional fields with saved values were hidden when switching back to a previously configured component - **Root Cause**: enabledOptionalProps reset on component change, never re-enabling optional fields that had values - **File**: form-context.tsx ### Issue 3: Optional prop values lost during state sync - **Problem**: Optional field values were discarded during the state synchronization effect if the field wasn't enabled - **Root Cause**: Sync effect skipped disabled optional props entirely - **File**: form-context.tsx ## Fixes Applied ### form-context.tsx 1. Fixed queryDisabledIdx initialization to use configuredProps instead of _configuredProps - Changed dependency from _configuredProps to component.key - Ensures blocking index is calculated from actual current values including parent-passed props 2. Added useEffect to auto-enable optional props with values - Runs when component key or configurableProps/configuredProps change - Automatically enables any optional props that have values in configuredProps - Ensures optional fields with saved values are shown on mount 3. Modified sync effect to preserve optional prop values - Optional props that aren't enabled still have their values preserved - Prevents data loss during state synchronization ### RemoteOptionsContainer.tsx 1. Destructured data from useQuery return - Added data to destructured values to track query results 2. Modified queryFn to return pageable object - Changed from returning just raw data array to returning full newPageable state object - Enables proper state syncing 3. Added useEffect to sync pageable state with query data - Handles both fresh API calls and React Query cached returns - When cached data is returned, queryFn doesn't run but useEffect syncs the state - Ensures options populate correctly on component remount ## Expected Behavior After Fixes ✓ Remote option fields load correctly when mounting with pre-configured values ✓ Dropdown shows fetched options even when using cached data ✓ Optional fields with saved values are automatically enabled and visible ✓ No data loss when switching between components ✓ Smooth component switching with all values and options preserved 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../src/components/RemoteOptionsContainer.tsx | 26 ++++++++---- .../connect-react/src/hooks/form-context.tsx | 40 ++++++++++++++++--- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index f3016cdebafe9..dd0c503329ae5 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -2,7 +2,7 @@ import type { ConfigurePropOpts, PropOptionValue, } from "@pipedream/sdk"; import { useQuery } from "@tanstack/react-query"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useFormContext } from "../hooks/form-context"; import { useFormFieldContext } from "../hooks/form-field-context"; import { useFrontendClient } from "../hooks/frontend-client-context"; @@ -106,6 +106,7 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP // TODO handle error! const { + data, isFetching, refetch, } = useQuery({ queryKey: [ @@ -167,26 +168,37 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP allValues.add(value) newOptions.push(o) } - let data = pageable.data + let responseData = pageable.data if (newOptions.length) { - data = [ + responseData = [ ...pageable.data, ...newOptions, ] as RawPropOption[] - setPageable({ + const newPageable = { page: page + 1, prevContext: res.context, - data, + data: responseData, values: allValues, - }) + } + setPageable(newPageable) + return newPageable; } else { setCanLoadMore(false) + return pageable; } - return data; }, enabled: !!queryEnabled, }); + // Sync pageable state with query data to handle both fresh fetches and cached returns + // When React Query returns cached data, the queryFn doesn't run, so we need to sync + // the state here to ensure options populate correctly on remount + useEffect(() => { + if (data) { + setPageable(data); + } + }, [data]); + const showLoadMoreButton = () => { return !isFetching && !error && canLoadMore } diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index 586f6ae3981bf..03071ecc2774c 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -169,6 +169,7 @@ export const FormContextProvider = ({ }, [ component.key, ]); + // XXX pass this down? (in case we make it hash or set backed, but then also provide {add,remove} instead of set) const optionalPropIsEnabled = (prop: ConfigurableProp) => enabledOptionalProps[prop.name]; @@ -275,6 +276,28 @@ export const FormContextProvider = ({ reloadPropIdx, ]); + // Auto-enable optional props that have values in configuredProps + // This ensures optional fields with saved values are shown when mounting with pre-configured props + useEffect(() => { + const propsToEnable: Record = {}; + + for (const prop of configurableProps) { + if (prop.optional) { + const value = configuredProps[prop.name as keyof ConfiguredProps]; + if (value !== undefined) { + propsToEnable[prop.name] = true; + } + } + } + + if (Object.keys(propsToEnable).length > 0) { + setEnabledOptionalProps(prev => ({ + ...prev, + ...propsToEnable, + })); + } + }, [component.key, configurableProps, configuredProps]); + // these validations are necessary because they might override PropInput for number case for instance // so can't rely on that base control form validation const propErrors = (prop: ConfigurableProp, value: unknown): string[] => { @@ -355,12 +378,12 @@ export const FormContextProvider = ({ }; useEffect(() => { - // Initialize queryDisabledIdx on load so that we don't force users - // to reconfigure a prop they've already configured whenever the page - // or component is reloaded - updateConfiguredPropsQueryDisabledIdx(_configuredProps) + // Initialize queryDisabledIdx using actual configuredProps (includes parent-passed values in controlled mode) + // instead of _configuredProps which starts empty. This ensures that when mounting with pre-configured + // values, remote options queries are not incorrectly blocked. + updateConfiguredPropsQueryDisabledIdx(configuredProps) }, [ - _configuredProps, + component.key, ]); useEffect(() => { @@ -386,8 +409,13 @@ export const FormContextProvider = ({ if (skippablePropTypes.includes(prop.type)) { continue; } - // if prop.optional and not shown, we skip and do on un-collapse + // if prop.optional and not shown, we still preserve the value if it exists + // This prevents losing saved values for optional props that haven't been enabled yet if (prop.optional && !optionalPropIsEnabled(prop)) { + const value = configuredProps[prop.name as keyof ConfiguredProps]; + if (value !== undefined) { + newConfiguredProps[prop.name as keyof ConfiguredProps] = value; + } continue; } const value = configuredProps[prop.name as keyof ConfiguredProps]; From cde427bf0d2925c4bdbec87fbc474b5b3a9881bf Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Thu, 16 Oct 2025 15:11:05 +0530 Subject: [PATCH 2/7] fix: clear dropdown options when dependent field changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a dependent field changes (e.g., Channel Type: "Channels" → "User/Direct Message"), the Channel dropdown should replace its options instead of accumulating them. The fix uses page-based logic to determine whether to replace or append options: - page === 0 (fresh query): Replace options with new data - page > 0 (pagination): Append options to existing data When dependent fields change, the useEffect resets page to 0, which triggers the queryFn to replace options instead of appending. This prevents accumulation of options from different queries. Additionally, the allValues Set is reset on fresh queries to ensure deduplication starts fresh, not carrying over values from the previous query. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/components/RemoteOptionsContainer.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index dd0c503329ae5..a71c2b6a5171e 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -95,6 +95,14 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP setError, ] = useState<{ name: string; message: string; }>(); + // Reset pagination and error when dependent fields change. + // This ensures the next query starts fresh from page 0, triggering a data replace instead of append + useEffect(() => { + setPage(0); + setCanLoadMore(true); + setError(undefined); + }, [externalUserId, component.key, prop.name, JSON.stringify(configuredPropsUpTo)]); + const onLoadMore = () => { setPage(pageable.page) setContext(pageable.prevContext) @@ -149,8 +157,10 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP _options = options; } + // For fresh queries (page 0), start with empty set to avoid accumulating old options + // For pagination (page > 0), use existing set to dedupe across pages + const allValues = page === 0 ? new Set() : new Set(pageable.values) const newOptions = [] - const allValues = new Set(pageable.values) for (const o of _options || []) { let value: PropOptionValue; if (isString(o)) { @@ -170,10 +180,10 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP } let responseData = pageable.data if (newOptions.length) { - responseData = [ - ...pageable.data, - ...newOptions, - ] as RawPropOption[] + // Replace data on fresh queries (page 0), append on pagination (page > 0) + responseData = page === 0 + ? newOptions as RawPropOption[] + : [...pageable.data, ...newOptions] as RawPropOption[] const newPageable = { page: page + 1, prevContext: res.context, From 899e22453a2c68b72217af1b2a3f571ecccb7ff6 Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Thu, 16 Oct 2025 15:44:07 +0530 Subject: [PATCH 3/7] fix: prevent duplicate API calls from race condition in form state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a field value changes, two /configure API calls were being made: 1. First call with empty configured_props: {} 2. Second call with correct configured_props: {field: value} Root cause: In setConfiguredProp, updateConfiguredPropsQueryDisabledIdx was called synchronously, updating queryDisabledIdx state before configuredProps state update completed. This caused children to re-render twice with mismatched state. Fix: Move queryDisabledIdx update to a reactive useEffect that watches configuredProps changes. This ensures both state updates complete before children re-render, preventing the duplicate API call with stale data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/connect-react/src/hooks/form-context.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index 03071ecc2774c..3ebc581261a3b 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -386,6 +386,15 @@ export const FormContextProvider = ({ component.key, ]); + // Update queryDisabledIdx reactively when configuredProps changes. + // This prevents race conditions where queryDisabledIdx updates synchronously before + // configuredProps completes its state update, causing duplicate API calls with stale data. + useEffect(() => { + updateConfiguredPropsQueryDisabledIdx(configuredProps); + }, [ + configuredProps, + ]); + useEffect(() => { updateConfigurationErrors(configuredProps) }, [ @@ -468,9 +477,6 @@ export const FormContextProvider = ({ if (prop.reloadProps) { setReloadPropIdx(idx); } - if (prop.type === "app" || prop.remoteOptions) { - updateConfiguredPropsQueryDisabledIdx(newConfiguredProps); - } const errs = propErrors(prop, value); const newErrors = { ...errors, From eb4315a8dc62600e420b5a2e580ceee1e1c5b7fd Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Thu, 16 Oct 2025 16:15:16 +0530 Subject: [PATCH 4/7] fix: handle integer dropdown values and error states properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes to prevent field value loss and crashes: 1. Preserve label-value format for integer props When integer properties with remoteOptions (like worksheetId) are selected from dropdowns, the values are stored in label-value format: {__lv: {label, value}}. The sync effect was incorrectly deleting these values because they weren't pure numbers. Now preserves __lv format for remote option dropdowns. 2. Return proper pageable structure on error in RemoteOptionsContainer When /configure returns an error, queryFn was returning [] instead of the expected pageable object {page, data, prevContext, values}. This caused pageable.data.map() to crash. Now returns proper structure on error to prevent crashes and display error message correctly. Fixes: - Worksheet ID field no longer resets after dynamic props reload - No more crash when clearing app field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/components/RemoteOptionsContainer.tsx | 8 +++++++- packages/connect-react/src/hooks/form-context.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index a71c2b6a5171e..d5cb00d83338f 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -140,7 +140,13 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP message: errors[0], }); } - return []; + // Return proper pageable structure on error to prevent crashes + return { + page: 0, + prevContext: {}, + data: [], + values: new Set(), + }; } let _options: RawPropOption[] = [] if (options?.length) { diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index 3ebc581261a3b..9f19bf6034366 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -435,7 +435,13 @@ export const FormContextProvider = ({ } } else { if (prop.type === "integer" && typeof value !== "number") { - delete newConfiguredProps[prop.name as keyof ConfiguredProps]; + // Preserve label-value format from remote options dropdowns + // Remote options store values as {__lv: {label: "...", value: ...}} + if (!(value && typeof value === "object" && "__lv" in value)) { + delete newConfiguredProps[prop.name as keyof ConfiguredProps]; + } else { + newConfiguredProps[prop.name as keyof ConfiguredProps] = value; + } } else { newConfiguredProps[prop.name as keyof ConfiguredProps] = value; } From f853fb64864e7006d47882b071d80df4b1e2623a Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Thu, 16 Oct 2025 17:27:24 +0530 Subject: [PATCH 5/7] style: fix eslint formatting errors --- .../src/components/RemoteOptionsContainer.tsx | 24 +++++++++++++++---- .../connect-react/src/hooks/form-context.tsx | 8 +++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index d5cb00d83338f..8f93febbe73bb 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -2,7 +2,9 @@ import type { ConfigurePropOpts, PropOptionValue, } from "@pipedream/sdk"; import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; +import { + useEffect, useState, +} from "react"; import { useFormContext } from "../hooks/form-context"; import { useFormFieldContext } from "../hooks/form-field-context"; import { useFrontendClient } from "../hooks/frontend-client-context"; @@ -101,7 +103,12 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP setPage(0); setCanLoadMore(true); setError(undefined); - }, [externalUserId, component.key, prop.name, JSON.stringify(configuredPropsUpTo)]); + }, [ + externalUserId, + component.key, + prop.name, + JSON.stringify(configuredPropsUpTo), + ]); const onLoadMore = () => { setPage(pageable.page) @@ -165,7 +172,9 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP // For fresh queries (page 0), start with empty set to avoid accumulating old options // For pagination (page > 0), use existing set to dedupe across pages - const allValues = page === 0 ? new Set() : new Set(pageable.values) + const allValues = page === 0 + ? new Set() + : new Set(pageable.values) const newOptions = [] for (const o of _options || []) { let value: PropOptionValue; @@ -189,7 +198,10 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP // Replace data on fresh queries (page 0), append on pagination (page > 0) responseData = page === 0 ? newOptions as RawPropOption[] - : [...pageable.data, ...newOptions] as RawPropOption[] + : [ + ...pageable.data, + ...newOptions, + ] as RawPropOption[] const newPageable = { page: page + 1, prevContext: res.context, @@ -213,7 +225,9 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP if (data) { setPageable(data); } - }, [data]); + }, [ + data, + ]); const showLoadMoreButton = () => { return !isFetching && !error && canLoadMore diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index 9f19bf6034366..10a165357ac9d 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -291,12 +291,16 @@ export const FormContextProvider = ({ } if (Object.keys(propsToEnable).length > 0) { - setEnabledOptionalProps(prev => ({ + setEnabledOptionalProps((prev) => ({ ...prev, ...propsToEnable, })); } - }, [component.key, configurableProps, configuredProps]); + }, [ + component.key, + configurableProps, + configuredProps, + ]); // these validations are necessary because they might override PropInput for number case for instance // so can't rely on that base control form validation From 97aa7dd0bd4cb2a46ae60223649656521f9f1c27 Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Thu, 16 Oct 2025 17:29:19 +0530 Subject: [PATCH 6/7] fix: add type annotations for PropOptionValue Sets --- .../connect-react/src/components/RemoteOptionsContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index 8f93febbe73bb..e471ff4a609e4 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -152,7 +152,7 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP page: 0, prevContext: {}, data: [], - values: new Set(), + values: new Set(), }; } let _options: RawPropOption[] = [] @@ -173,7 +173,7 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP // For fresh queries (page 0), start with empty set to avoid accumulating old options // For pagination (page > 0), use existing set to dedupe across pages const allValues = page === 0 - ? new Set() + ? new Set() : new Set(pageable.values) const newOptions = [] for (const o of _options || []) { From 0a0d9f8f3995749c44606134d81af8999a3e2e23 Mon Sep 17 00:00:00 2001 From: Roopak Nijhara Date: Fri, 17 Oct 2025 15:29:20 +0530 Subject: [PATCH 7/7] fix: handle multi-select integer fields with __lv format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, multi-select integer fields (e.g., Worksheet ID(s)) displayed "[object Object]" instead of proper labels when populated with pre-configured values. This occurred because: 1. form-context.tsx only checked for single __lv objects, not arrays 2. ControlSelect.tsx tried to sanitize entire arrays instead of individual items Changes: - form-context.tsx: Check for both single __lv objects and arrays of __lv objects to preserve multi-select values during sync - ControlSelect.tsx: Extract array contents from __lv wrapper and map each item through sanitizeOption for proper rendering This completes the fix for pre-configured props handling with remote options. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/connect-react/src/components/ControlSelect.tsx | 7 ++++++- packages/connect-react/src/hooks/form-context.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/connect-react/src/components/ControlSelect.tsx b/packages/connect-react/src/components/ControlSelect.tsx index 3d7c0a3499853..dc3d41bddca1e 100644 --- a/packages/connect-react/src/components/ControlSelect.tsx +++ b/packages/connect-react/src/components/ControlSelect.tsx @@ -92,7 +92,12 @@ export function ControlSelect({ } } else if (rawValue && typeof rawValue === "object" && "__lv" in (rawValue as Record)) { // Extract the actual option from __lv wrapper and sanitize to LV - return sanitizeOption(((rawValue as Record).__lv) as T); + // Handle both single objects and arrays wrapped in __lv + const lvContent = (rawValue as Record).__lv; + if (Array.isArray(lvContent)) { + return lvContent.map((item) => sanitizeOption(item as T)); + } + return sanitizeOption(lvContent as T); } else if (!isOptionWithLabel(rawValue)) { const lvOptions = selectOptions?.[0] && isOptionWithLabel(selectOptions[0]); if (lvOptions) { diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index 10a165357ac9d..e7d422d61c113 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -441,7 +441,12 @@ export const FormContextProvider = ({ if (prop.type === "integer" && typeof value !== "number") { // Preserve label-value format from remote options dropdowns // Remote options store values as {__lv: {label: "...", value: ...}} - if (!(value && typeof value === "object" && "__lv" in value)) { + // For multi-select fields, this will be an array of __lv objects + const isLabelValue = value && typeof value === "object" && "__lv" in value; + const isArrayOfLabelValues = Array.isArray(value) && value.length > 0 && + value.every((item) => item && typeof item === "object" && "__lv" in item); + + if (!(isLabelValue || isArrayOfLabelValues)) { delete newConfiguredProps[prop.name as keyof ConfiguredProps]; } else { newConfiguredProps[prop.name as keyof ConfiguredProps] = value;